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 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387
//! Implementation for the introduce-and-rendezvous handshake.
use super::*;
// These imports just here, because they have names unsuitable for importing widely.
use tor_cell::relaycell::{
hs::intro_payload::{IntroduceHandshakePayload, OnionKey},
msg::{Introduce2, Rendezvous1},
};
use tor_linkspec::{decode::Strictness, verbatim::VerbatimLinkSpecCircTarget};
use tor_proto::{
circuit::handshake::{
self,
hs_ntor::{self, HsNtorHkdfKeyGenerator},
},
stream::{IncomingStream, IncomingStreamRequestFilter},
};
/// An error produced while trying to process an introduction request we have
/// received from a client via an introduction point.
#[derive(Debug, Clone, thiserror::Error)]
#[allow(clippy::enum_variant_names)]
#[non_exhaustive]
pub enum IntroRequestError {
/// The handshake (e.g. hs_ntor) in the Introduce2 message was invalid and
/// could not be completed.
#[error("Introduction handshake was invalid")]
InvalidHandshake(#[source] tor_proto::Error),
/// The decrypted payload of the Introduce2 message could not be parsed.
#[error("Could not parse INTRODUCE2 payload")]
InvalidPayload(#[source] tor_bytes::Error),
/// We weren't able to build a ChanTarget from the Introduce2 message.
#[error("Invalid link specifiers in INTRODUCE2 payload")]
InvalidLinkSpecs(#[source] tor_linkspec::decode::ChanTargetDecodeError),
/// We weren't able to obtain the subcredentials for decrypting the Introduce2 message.
#[error("Could not obtain subcredentials")]
Subcredentials(#[source] crate::FatalError),
}
impl HasKind for IntroRequestError {
fn kind(&self) -> tor_error::ErrorKind {
use tor_error::ErrorKind as EK;
use IntroRequestError as E;
match self {
E::InvalidHandshake(e) => e.kind(),
E::InvalidPayload(_) => EK::RemoteProtocolViolation,
E::InvalidLinkSpecs(_) => EK::RemoteProtocolViolation,
E::Subcredentials(e) => e.kind(),
}
}
}
/// An error produced while trying to connect to a rendezvous point and open a
/// session with a client.
#[derive(Debug, Clone, thiserror::Error)]
#[non_exhaustive]
pub enum EstablishSessionError {
/// We couldn't get a timely network directory in order to build our
/// chosen circuits.
#[error("Network directory not available")]
NetdirUnavailable(#[source] tor_netdir::Error),
/// Got an onion key with an unrecognized type (not ntor).
#[error("Received an unsupported type of onion key")]
UnsupportedOnionKey,
/// Unable to build a circuit to the rendezvous point.
#[error("Could not establish circuit to rendezvous point")]
RendCirc(#[source] RetryError<tor_circmgr::Error>),
/// Encountered a failure while trying to add a virtual hop to the circuit.
#[error("Could not add virtual hop to circuit")]
VirtualHop(#[source] tor_proto::Error),
/// We encountered an error while configuring the virtual hop to send us
/// BEGIN messages.
#[error("Could not configure circuit to allow BEGIN messages")]
AcceptBegins(#[source] tor_proto::Error),
/// We encountered an error while sending the rendezvous1 message.
#[error("Could not send RENDEZVOUS1 message")]
SendRendezvous(#[source] tor_proto::Error),
/// The client sent us a rendezvous point with an impossible set of identities.
///
/// (For example, it gave us `(Ed1, Rsa1)`, but in the network directory `Ed1` is
/// associated with `Rsa2`.)
#[error("Impossible combination of identities for rendezvous point")]
ImpossibleIds(#[source] tor_netdir::RelayLookupError),
/// An internal error occurred.
#[error("Internal error")]
Bug(#[from] tor_error::Bug),
}
impl HasKind for EstablishSessionError {
fn kind(&self) -> tor_error::ErrorKind {
use tor_error::ErrorKind as EK;
use EstablishSessionError as E;
match self {
E::NetdirUnavailable(e) => e.kind(),
E::UnsupportedOnionKey => EK::RemoteProtocolViolation,
EstablishSessionError::RendCirc(e) => {
tor_circmgr::Error::summarized_error_kind(e.sources())
}
EstablishSessionError::VirtualHop(e) => e.kind(),
EstablishSessionError::AcceptBegins(e) => e.kind(),
EstablishSessionError::SendRendezvous(e) => e.kind(),
EstablishSessionError::ImpossibleIds(_) => EK::RemoteProtocolViolation,
EstablishSessionError::Bug(e) => e.kind(),
}
}
}
/// A decrypted request from an onion service client which we can
/// choose to answer (or not).
///
/// This corresponds to a processed INTRODUCE2 message.
///
/// To accept this request, call its
/// [`establish_session`](IntroRequest::establish_session) method.
/// To reject this request, simply drop it.
#[derive(educe::Educe)]
#[educe(Debug)]
pub(crate) struct IntroRequest {
/// The introduce2 message itself. We keep this in case we want to look at
/// the outer header.
req: Introduce2,
/// The key generator we'll use to derive our shared keys with the client when
/// creating a virtual hop.
#[educe(Debug(ignore))]
key_gen: HsNtorHkdfKeyGenerator,
/// The RENDEZVOUS1 message we'll send to the rendezvous point.
///
/// (The rendezvous point will in turn send this to the client as a RENDEZVOUS2.)
rend1_msg: Rendezvous1,
/// The decrypted and parsed body of the introduce2 message.
intro_payload: IntroduceHandshakePayload,
/// The (in progress) ChanTarget that we'll use to build a circuit target
/// for connecting to the rendezvous point.
chan_target: OwnedChanTargetBuilder,
}
/// An open session with a single client.
///
/// (We consume this type and take ownership of its members later in
/// [`RendRequest::accept()`](crate::req::RendRequest::accept).)
pub(crate) struct OpenSession {
/// A stream of incoming BEGIN requests.
pub(crate) stream_requests: BoxStream<'static, IncomingStream>,
/// Our circuit with the client in question.
///
/// See `RendRequest::accept()` for more information on the life cycle of
/// this circuit.
pub(crate) circuit: Arc<ClientCirc>,
}
/// Dyn-safe trait to represent a `HsCircPool`.
///
/// We need this so that we can hold an `Arc<HsCircPool<R>>` in
/// `RendRequestContext` without needing to parameterize on R.
#[async_trait]
pub(crate) trait RendCircConnector: Send + Sync {
async fn get_or_launch_specific(
&self,
netdir: &tor_netdir::NetDir,
kind: HsCircKind,
target: VerbatimLinkSpecCircTarget<OwnedCircTarget>,
) -> tor_circmgr::Result<Arc<ClientCirc>>;
}
#[async_trait]
impl<R: Runtime> RendCircConnector for HsCircPool<R> {
async fn get_or_launch_specific(
&self,
netdir: &tor_netdir::NetDir,
kind: HsCircKind,
target: VerbatimLinkSpecCircTarget<OwnedCircTarget>,
) -> tor_circmgr::Result<Arc<ClientCirc>> {
HsCircPool::get_or_launch_specific(self, netdir, kind, target).await
}
}
/// Filter callback used to enforce early requirements on streams.
#[derive(Clone, Debug)]
pub(crate) struct RequestFilter {
/// Largest number of streams we will accept on a circuit at a time.
//
// TODO: Conceivably, this should instead be a
// watch::Receiver<Arc<OnionServiceConfig>>, so we can re-check the latest
// value of the setting every time. Instead, we currently only copy this
// setting when an intro request is accepted.
pub(crate) max_concurrent_streams: usize,
}
impl IncomingStreamRequestFilter for RequestFilter {
fn disposition(
&mut self,
_ctx: &tor_proto::stream::IncomingStreamRequestContext<'_>,
circ: &tor_proto::circuit::ClientCircSyncView<'_>,
) -> tor_proto::Result<tor_proto::stream::IncomingStreamRequestDisposition> {
if circ.n_open_streams() >= self.max_concurrent_streams {
// TODO: We may want to have a way to send back an END message as
// well and not tear down the circuit.
Ok(tor_proto::stream::IncomingStreamRequestDisposition::CloseCircuit)
} else {
Ok(tor_proto::stream::IncomingStreamRequestDisposition::Accept)
}
}
}
impl IntroRequest {
/// Try to decrypt an incoming Introduce2 request, using the set of keys provided.
pub(crate) fn decrypt_from_introduce2(
req: Introduce2,
context: &RendRequestContext,
) -> Result<Self, IntroRequestError> {
use IntroRequestError as E;
let mut rng = rand::thread_rng();
// We need the subcredential for the *current time period* in order to do the hs_ntor
// handshake. But that can change over time. We will instead use KeyMgr::get_matching to
// find all current subcredentials.
let subcredentials = context
.compute_subcredentials()
.map_err(IntroRequestError::Subcredentials)?;
let (key_gen, rend1_body, msg_body) = hs_ntor::server_receive_intro(
&mut rng,
&context.kp_hss_ntor,
&context.kp_hs_ipt_sid,
&subcredentials[..],
req.encoded_header(),
req.encrypted_body(),
)
.map_err(E::InvalidHandshake)?;
let intro_payload: IntroduceHandshakePayload = {
let mut r = tor_bytes::Reader::from_slice(&msg_body);
r.extract().map_err(E::InvalidPayload)?
// Note: we _do not_ call `should_be_exhausted` here, since we
// explicitly expect the payload of an introduce2 message to be
// padded to hide its size.
};
// We build the OwnedChanTargetBuilder now, so that we can detect any
// problems here earlier.
let chan_target = OwnedChanTargetBuilder::from_encoded_linkspecs(
Strictness::Standard,
intro_payload.link_specifiers(),
)
.map_err(E::InvalidLinkSpecs)?;
let rend1_msg = Rendezvous1::new(*intro_payload.cookie(), rend1_body);
Ok(IntroRequest {
req,
key_gen,
rend1_msg,
intro_payload,
chan_target,
})
}
/// Try to accept this client's request.
///
/// To do so, we open a circuit to the client's chosen rendezvous point,
/// send it a RENDEZVOUS1 message, and wait for incoming BEGIN messages from
/// the client.
pub(crate) async fn establish_session(
self,
filter: RequestFilter,
hs_pool: Arc<dyn RendCircConnector>,
provider: Arc<dyn NetDirProvider>,
) -> Result<OpenSession, EstablishSessionError> {
use EstablishSessionError as E;
// Find a netdir. Note that we _won't_ try to wait or retry if the
// netdir isn't there: we probably can't answer this user's request.
let netdir = provider
.netdir(tor_netdir::Timeliness::Timely)
.map_err(E::NetdirUnavailable)?;
// Try to construct a CircTarget for rendezvous point based on the
// intro_payload.
let rend_point = {
// TODO: We might have checked for a recognized onion key type earlier.
let ntor_onion_key = match self.intro_payload.onion_key() {
OnionKey::NtorOnionKey(ntor_key) => ntor_key,
_ => return Err(E::UnsupportedOnionKey),
};
let mut bld = OwnedCircTarget::builder();
*bld.chan_target() = self.chan_target;
// TODO (#1223): This block is very similar to circtarget_from_pieces in
// relay_info.rs.
// Is there a clean way to refactor this?
let protocols = {
let chan_target = bld.chan_target().build().map_err(into_internal!(
"from_encoded_linkspecs gave an invalid output"
))?;
match netdir
.by_ids_detailed(&chan_target)
.map_err(E::ImpossibleIds)?
{
Some(relay) => relay.protovers().clone(),
None => netdir.relay_protocol_status().required_protocols().clone(),
}
};
bld.protocols(protocols);
bld.ntor_onion_key(*ntor_onion_key);
VerbatimLinkSpecCircTarget::new(
bld.build()
.map_err(into_internal!("Failed to construct a valid circtarget"))?,
self.intro_payload.link_specifiers().into(),
)
};
let max_n_attempts = netdir.params().hs_service_rendezvous_failures_max;
let mut circuit = None;
let mut retry_err: RetryError<tor_circmgr::Error> =
RetryError::in_attempt_to("Establish a circuit to a rendezvous point");
// Open circuit to rendezvous point.
for _attempt in 1..=max_n_attempts.into() {
match hs_pool
.get_or_launch_specific(&netdir, HsCircKind::SvcRend, rend_point.clone())
.await
{
Ok(circ) => {
circuit = Some(circ);
break;
}
Err(e) => {
retry_err.push(e);
// Note that we do not sleep on errors: if there is any
// error that will be solved by waiting, it would probably
// require waiting too long to satisfy the client.
}
}
}
let circuit = circuit.ok_or_else(|| E::RendCirc(retry_err))?;
// We'll need parameters to extend the virtual hop.
let params = circparameters_from_netparameters(netdir.params());
// We won't need the netdir any longer; stop holding the reference.
drop(netdir);
let last_real_hop = circuit
.last_hop_num()
.map_err(into_internal!("Circuit with no final hop"))?;
// Add a virtual hop.
circuit
.extend_virtual(
handshake::RelayProtocol::HsV3,
handshake::HandshakeRole::Responder,
self.key_gen,
params,
)
.await
.map_err(E::VirtualHop)?;
let virtual_hop = circuit
.last_hop_num()
.map_err(into_internal!("Circuit with no virtual hop"))?;
// Accept begins from that virtual hop
let stream_requests = circuit
.allow_stream_requests(&[tor_cell::relaycell::RelayCmd::BEGIN], virtual_hop, filter)
.await
.map_err(E::AcceptBegins)?
.boxed();
// Send the RENDEZVOUS1 message.
circuit
.send_raw_msg(self.rend1_msg.into(), last_real_hop)
.await
.map_err(E::SendRendezvous)?;
Ok(OpenSession {
stream_requests,
circuit,
})
}
}