Merge branch 'onion-proto-apis' into 'main'

Draft APIs for onion services in tor-proto

See merge request tpo/core/arti!970
This commit is contained in:
Nick Mathewson 2023-01-24 17:28:16 +00:00
commit 1834579460
7 changed files with 252 additions and 20 deletions

View File

@ -14,9 +14,13 @@ repository = "https://gitlab.torproject.org/tpo/core/arti.git/"
[features]
default = []
full = ["tokio"]
experimental = ["hs", "ntor_v3"]
hs = []
experimental = ["experimental-api", "onion-client", "onion-service", "ntor_v3"]
ntor_v3 = []
onion-client = ["onion-common"]
onion-service = ["onion-common"]
onion-common = []
experimental-api = []
# Enable testing-only APIs. APIs under this feature are not
# covered by semver.
testing = []

View File

@ -41,6 +41,8 @@
pub(crate) mod celltypes;
pub(crate) mod halfcirc;
mod halfstream;
#[cfg(feature = "onion-common")]
pub mod handshake;
mod path;
pub(crate) mod reactor;
pub(crate) mod sendme;
@ -228,6 +230,92 @@ impl ClientCirc {
&self.channel
}
/// Send a control message to the final hop on this circuit.
///
/// Note that it is quite possible to use this function to violate the tor
/// protocol; most users of this API will not need to call it. It is used
/// to implement most of the onion service handshake.
///
/// (This function is not yet implemented. Right now it will always panic.)
//
// TODO hs: rename this. "control_messages" is kind of ambiguous; we use
// "control" for a lot of other things. We say "meta" elsewhere in the
// reactor code, but "meta messages" just sounds odd.
#[allow(clippy::missing_panics_doc, unused_variables)] // TODO hs remove
#[cfg(feature = "experimental-api")]
pub async fn send_control_message(&self, msg: RelayMsg) -> Result<()> {
todo!() // TODO hs
}
/// Begin accepting 'control' messages from the final hop on this circuit,
/// and return an asynchronous stream of any such messages that arrive.
///
/// A "control" message is a message without a stream ID that `tor-proto`
/// does not handle on its own. (The messages that `tor-proto` can handle
/// are DESTROY, DATA, SENDME, ...) Ordinarily, any unexpected control
/// message will cause the circuit to exit with an error.
///
/// There can only be one stream of this type created on a given circuit at
/// a time. If a such a stream already exists, this method will return an
/// error.
///
/// The caller should be sure to close the circuit if a command that _it_
/// doesn't recognize shows up.
///
/// (This function is not yet implemented; right now, it will always panic.)
//
// TODO hs: Possibly this function (and send_control_message) should use
// HopNum or similar to indicate which hop we're talking to, rather than
// just doing "the last hop".
//
// TODO hs: There is possibly some kind of type trickery we could do here so
// that the stream would return a chosen type that implements
// `TryFrom<RelayMsg>` or something like that. Not sure whether that's a
// good idea.
//
// TODO hs: Perhaps the stream here should yield a different type. Ian
// thinks maybe we should store a callback instead.
//
// TODO hs: rename this. "control_messages" is kind of ambiguous; we use
// "control" for a lot of other things. We say "meta" elsewhere in the
// reactor code, but "meta messages" just sounds odd.
#[cfg(feature = "experimental-api")]
#[allow(clippy::missing_panics_doc, unused_variables)] // TODO hs remove
pub fn receive_control_messages(
&self,
) -> Result<impl futures::Stream<Item = Box<chancell::RawCellBody>>> {
if false {
return Ok(futures::stream::empty()); // TODO hs remove; this is just here for type inference.
}
todo!() // TODO hs implement.
}
/// Tell this circuit to begin allowing the final hop of the circuit to try
/// to create new Tor streams, and to return those pending requests in an
/// asynchronous stream.
///
/// Ordinarily, these requests are rejected.
///
/// There can only be one stream of this type created on a given circuit at
/// a time. If a such a stream already exists, this method will return an
/// error.
///
/// (This function is not yet implemented; right now, it will always panic.)
///
/// Only onion services (and eventually) exit relays should call this
/// method.
#[cfg(feature = "onion-service")]
#[allow(clippy::missing_panics_doc, unused_variables)] // TODO hs remove
pub fn allow_stream_requests(
&self,
allow_commands: &[tor_cell::relaycell::RelayCmd],
) -> Result<impl futures::Stream<Item = crate::stream::IncomingStream>> {
if false {
return Ok(futures::stream::empty()); // TODO hs remove; this is just here for type inference.
}
todo!() // TODO hs implement.
}
/// Extend the circuit via the ntor handshake to a new target last
/// hop.
pub async fn extend_ntor<Tg>(&self, target: &Tg, params: &CircParameters) -> Result<()>
@ -266,6 +354,36 @@ impl ClientCirc {
Ok(())
}
/// Extend this circuit by a single, "virtual" hop.
///
/// This is used to implement onion services: the client and the service
/// both build a circuit to a single rendezvous point, and tell the
/// rendezvous point to relay traffic between their two circuits. Having
/// completed a [`handshake`] out of band[^1], the parties each extend their
/// circuits by a single "virtual" encryption hop that represents their
/// shared cryptographic context.
///
/// Once a circuit has been extended in this way, it is an error to try to
/// extend it in any other way.
///
/// [^1]: Technically, the handshake is only _mostly_ out of band: the
/// client sends their half of the handshake in an ` message, and the
/// service's response is inline in its `RENDEZVOUS2` message.
//
// TODO hs: let's try to enforce the "you can't extend a circuit again once
// it has been extended this way" property. We could do that with internal
// state, or some kind of a type state pattern.
#[cfg(feature = "onion-common")]
#[allow(clippy::missing_panics_doc, unused_variables)]
pub async fn extend_virtual(
&self,
protocol: handshake::RelayProtocol,
role: handshake::HandshakeRole,
seed: impl handshake::KeyGenerator,
) -> Result<()> {
todo!() // TODO hs implement
}
/// Helper, used to begin a stream.
///
/// This function allocates a stream ID, and sends the message

View File

@ -0,0 +1,37 @@
//! Features for manual invocation of Tor's cryptographic circuit handshakes.
//!
//! These features are used to implement onion services, by giving the onion
//! service code more direct control over the lower-level pieces of the protocol.
// Here we re-export some key types from our cryptographic code, for use when we
// implement our onion handshake.
//
// TODO hs: it might be neat, someday, to clean this all up so that the types
// and functions in hs_ntor are all methods on a set of related traits. But
// that can wait IMO until we have a second circuit creation mechanism for use
// with ntor.
pub use crate::crypto::handshake::hs_ntor;
pub use crate::crypto::handshake::KeyGenerator;
/// The relay protocol to use when extending a circuit manually with
/// [`Circuit::extend_virtual`](crate::circuit::ClientCirc::extend_virtual).
//
// NOTE: These correspond internally to implementations of
// crate::crypto::cell::ClientLayer.
#[derive(Copy, Clone, Debug)]
#[non_exhaustive]
pub enum RelayProtocol {
/// A variation of Tor's original protocol, using AES-256 and SHA-3.
HsV3,
}
/// What role we are playing in a handshake.
#[derive(Copy, Clone, Debug)]
#[non_exhaustive]
pub enum HandshakeRole {
/// We are the party initiating the handshake.
Initiator,
/// We are the party responding to the handshake.
Responder,
}

View File

@ -11,8 +11,8 @@
//! Currently, this module implements only the "ntor" handshake used
//! for circuits on today's Tor.
pub(crate) mod fast;
#[cfg(feature = "hs")]
pub(crate) mod hs_ntor;
#[cfg(feature = "onion-common")]
pub mod hs_ntor;
pub(crate) mod ntor;
#[cfg(feature = "ntor_v3")]
pub(crate) mod ntor_v3;
@ -72,7 +72,8 @@ pub(crate) trait ServerHandshake {
/// Typically, it wraps a KDF function, and some seed key material.
///
/// It can only be used once.
pub(crate) trait KeyGenerator {
#[allow(unreachable_pub)] // This is only exported depending on enabled features.
pub trait KeyGenerator {
/// Consume the key
fn expand(self, keylen: usize) -> Result<SecretBuf>;
}

View File

@ -15,14 +15,14 @@
//! This module is a work in progress, and is not actually used anywhere yet
//! or tested: please expect the API to change.
//!
//! This module is available only when the `hs` feature is enabled.
//! This module is available only when the `onion-common` feature is enabled.
//
// TODO hs: go through this code carefully and make sure that its APIs and
// behavior are still what we want.
// We want to use the exact variable names from the rend-spec-v3.txt proposal.
// This means that we allow variables to be named x (privkey) and X (pubkey).
#![allow(non_snake_case)]
// This module is still unused: so allow some dead code for now.
#![allow(dead_code)]
#![allow(unreachable_pub)]
use crate::crypto::handshake::KeyGenerator;
use crate::crypto::ll::kdf::{Kdf, ShakeKdf};
@ -52,7 +52,7 @@ type MacTag = [u8; 32];
/// The AUTH_INPUT_MAC from the HS Ntor protocol
type AuthInputMac = MacTag;
/// The Service's subcredential
pub type Subcredential = [u8; 32];
pub type Subcredential = [u8; 32]; // TODO hs: use tor-hscrypto version instead.
/// The key generator used by the HS ntor handshake. Implements the simple key
/// expansion protocol specified in section "Key expansion" of rend-spec-v3.txt .
@ -82,24 +82,24 @@ impl KeyGenerator for HsNtorHkdfKeyGenerator {
pub struct HsNtorClientInput {
/// Introduction point encryption key (aka B)
/// (found in the HS descriptor)
pub B: curve25519::PublicKey,
B: curve25519::PublicKey,
/// Introduction point authentication key (aka AUTH_KEY)
/// (found in the HS descriptor)
pub auth_key: ed25519::PublicKey,
auth_key: ed25519::PublicKey,
/// Service subcredential
pub subcredential: Subcredential,
subcredential: Subcredential,
/// The plaintext that should be encrypted into ENCRYPTED_DATA It's
/// structure is irrelevant for this crate, but can be found in section
/// \[PROCESS_INTRO2\] of the spec
pub plaintext: Vec<u8>,
plaintext: Vec<u8>,
/// The data of the INTRODUCE1 cell from the beginning and up to the start
/// of the ENCRYPTED_DATA. It's used to compute the MAC at the end of the
/// INTRODUCE1 cell.
pub intro_cell_data: Vec<u8>,
intro_cell_data: Vec<u8>,
}
impl HsNtorClientInput {
@ -259,20 +259,20 @@ where
/// The input required to enter the HS Ntor protocol as a service
pub struct HsNtorServiceInput {
/// Introduction point encryption privkey
pub b: curve25519::StaticSecret,
b: curve25519::StaticSecret,
/// Introduction point encryption pubkey
pub B: curve25519::PublicKey,
B: curve25519::PublicKey,
/// Introduction point authentication key (aka AUTH_KEY)
pub auth_key: ed25519::PublicKey,
auth_key: ed25519::PublicKey,
/// Our subcredential
pub subcredential: Subcredential,
subcredential: Subcredential,
/// The data of the INTRODUCE1 cell from the beginning and up to the start
/// of the ENCRYPTED_DATA. Will be used to verify the MAC at the end of the
/// INTRODUCE1 cell.
pub intro_cell_data: Vec<u8>,
intro_cell_data: Vec<u8>,
}
impl HsNtorServiceInput {

View File

@ -10,11 +10,16 @@
//! There is no fairness, rate-limiting, or flow control.
mod data;
#[cfg(feature = "onion-service")]
mod incoming;
mod params;
mod raw;
mod resolve;
pub use data::{DataReader, DataStream, DataWriter};
#[cfg(feature = "onion-service")]
#[cfg_attr(docsrs, doc(cfg(feature = "onion-service")))]
pub use incoming::{IncomingStream, IncomingStreamRequest};
pub use params::StreamParameters;
pub use raw::StreamReader;
pub use resolve::ResolveStream;

View File

@ -0,0 +1,67 @@
//! Functionality for incoming streams, opened from the other side of a circuit.
#![allow(
dead_code,
unused_variables,
clippy::missing_panics_doc,
clippy::needless_pass_by_value
)] // TODO hs remove
use super::DataStream;
/// A pending request from the other end of the circuit for us to open a new
/// stream.
///
/// Exits, directory caches, and onion services expect to receive these; others
/// do not.
///
/// On receiving one of these objects, the party handling it should accept it or
/// reject it. If it is dropped without being explicitly handled, a reject
/// message will be sent anyway.
#[derive(Debug)]
pub struct IncomingStream {
/// The message that the client sent us to begin the stream.
request: IncomingStreamRequest,
/// The information that we'll use to wire up the stream, if it is accepted.
stream: crate::circuit::StreamTarget,
}
/// A message that can be sent to begin a stream.
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum IncomingStreamRequest {
/// A begin cell, which requests a new data stream.
Begin(tor_cell::relaycell::msg::Begin),
// TODO: Eventually, add a BeginDir variant
// TODO: eventually, add a Resolve variant.
}
impl IncomingStream {
/// Return the underlying message that was used to try to begin this stream.
pub fn request(&self) -> IncomingStreamRequest {
todo!()
}
/// Accept this stream as a new [`DataStream`], and send the client a
/// message letting them know the stream was accepted.
pub fn accept_data(self, message: tor_cell::relaycell::msg::Connected) -> DataStream {
todo!()
}
/// Reject this request and send an error message to the client.
pub fn reject(self, message: tor_cell::relaycell::msg::End) {
todo!() // TODO hs
}
/// Ignore this request without replying to the client.
///
/// (If you drop an [`IncomingStream`] without calling `accept_data`,
/// `reject`, or this method, the drop handler will cause it to be
/// rejected.)
pub fn discard(self) {
todo!() // TODO hs
}
}
// TODO hs: dropping an IncomingStream without accepting or rejecting it should
// cause it to call `reject`.