diff --git a/crates/tor-cell/src/relaycell.rs b/crates/tor-cell/src/relaycell.rs index a432bb209..d93e8bdf3 100644 --- a/crates/tor-cell/src/relaycell.rs +++ b/crates/tor-cell/src/relaycell.rs @@ -10,9 +10,9 @@ use caret::caret_int; use rand::{CryptoRng, Rng}; pub mod extend; -pub mod msg; #[cfg(feature = "onion-service")] -pub mod onion_service; +pub mod hs; +pub mod msg; pub mod restrict; #[cfg(feature = "experimental-udp")] pub mod udp; diff --git a/crates/tor-cell/src/relaycell/hs.rs b/crates/tor-cell/src/relaycell/hs.rs new file mode 100644 index 000000000..f05cf7a15 --- /dev/null +++ b/crates/tor-cell/src/relaycell/hs.rs @@ -0,0 +1,244 @@ +//! Encoding and decoding for relay messages related to onion services. + +#![allow(dead_code, unused_variables)] // TODO hs: remove. + +// TODO hs: we'll need accessors for the useful fields in all these types. + +use super::msg::{self, Body}; +use caret::caret_int; +use tor_bytes::{EncodeError, EncodeResult, Error as BytesError, Result}; +use tor_bytes::{Reader, Writer}; +use tor_hscrypto::RendCookie; +use tor_llcrypto::pk::rsa::RsaIdentity; + +pub mod est_intro; +mod ext; + +pub use ext::UnrecognizedExt; + +caret_int! { + /// The type of the introduction point auth key + pub struct AuthKeyType(u8) { + /// Ed25519; SHA3-256 + ED25519_SHA3_256 = 2, + } +} + +/// A message sent from client to rendezvous point. +#[derive(Debug, Clone)] +pub struct EstablishRendezvous { + /// A rendezvous cookie is an arbitrary 20-byte value, + /// chosen randomly by the client. + cookie: [u8; EstablishRendezvous::COOKIE_LEN], // TODO hs: Make this a RendCookie. +} +impl EstablishRendezvous { + /// The only acceptable length of a rendezvous cookie. + pub const COOKIE_LEN: usize = 20; + + /// Construct a new establish rendezvous cell. + pub fn new(cookie: [u8; Self::COOKIE_LEN]) -> Self { + Self { cookie } + } +} +impl msg::Body for EstablishRendezvous { + fn decode_from_reader(r: &mut Reader<'_>) -> Result { + let cookie = r.extract()?; + r.take_rest(); + Ok(Self { cookie }) + } + fn encode_onto(self, w: &mut W) -> EncodeResult<()> { + w.write(&self.cookie) + } +} + +#[derive(Debug, Clone)] +/// A message sent from client to introduction point. +pub struct Introduce1(Introduce); + +impl msg::Body for Introduce1 { + fn decode_from_reader(r: &mut Reader<'_>) -> Result { + Ok(Self(Introduce::decode_from_reader(r)?)) + } + fn encode_onto(self, w: &mut W) -> EncodeResult<()> { + self.0.encode_onto(w) + } +} + +impl Introduce1 { + /// All arguments constructor + pub fn new(auth_key_type: AuthKeyType, auth_key: Vec, encrypted: Vec) -> Self { + Self(Introduce::new(auth_key_type, auth_key, encrypted)) + } +} + +#[derive(Debug, Clone)] +/// A message sent from introduction point to hidden service host. +pub struct Introduce2(Introduce); + +impl msg::Body for Introduce2 { + fn decode_from_reader(r: &mut Reader<'_>) -> Result { + Ok(Self(Introduce::decode_from_reader(r)?)) + } + fn encode_onto(self, w: &mut W) -> EncodeResult<()> { + self.0.encode_onto(w) + } +} + +impl Introduce2 { + /// All arguments constructor + pub fn new(auth_key_type: AuthKeyType, auth_key: Vec, encrypted: Vec) -> Self { + Self(Introduce::new(auth_key_type, auth_key, encrypted)) + } +} + +#[derive(Debug, Clone)] +/// A message body shared by Introduce1 and Introduce2 +struct Introduce { + /// Introduction point auth key type and the type of + /// the MAC used in `handshake_auth`. + auth_key_type: AuthKeyType, + /// The public introduction point auth key. + auth_key: Vec, + /// Up to end of relay payload. + encrypted: Vec, +} + +impl Introduce { + /// All arguments constructor + fn new(auth_key_type: AuthKeyType, auth_key: Vec, encrypted: Vec) -> Self { + Self { + auth_key_type, + auth_key, + encrypted, + } + } + /// Decode an Introduce message body from the given reader + fn decode_from_reader(r: &mut Reader<'_>) -> Result { + let legacy_key_id: RsaIdentity = r.extract()?; + if !legacy_key_id.is_zero() { + return Err(BytesError::InvalidMessage( + "legacy key id in Introduce1.".into(), + )); + } + let auth_key_type = r.take_u8()?.into(); + let auth_key_len = r.take_u16()?; + let auth_key = r.take(auth_key_len as usize)?.into(); + let n_ext = r.take_u8()?; + for _ in 0..n_ext { + let _ext_type = r.take_u8()?; + r.read_nested_u8len(|r| { + r.take_rest(); + Ok(()) + })?; + } + let encrypted = r.take_rest().into(); + Ok(Self { + auth_key_type, + auth_key, + encrypted, + }) + } + /// Encode an Introduce message body onto the given writer + fn encode_onto(self, w: &mut W) -> EncodeResult<()> { + w.write_all(&[0_u8; 20]); + w.write_u8(self.auth_key_type.get()); + w.write_u16(u16::try_from(self.auth_key.len()).map_err(|_| EncodeError::BadLengthValue)?); + w.write_all(&self.auth_key[..]); + // No Introduce1 extension for now. + w.write_u8(0_u8); + w.write_all(&self.encrypted[..]); + Ok(()) + } +} + +/// A message sent from an onion service to a rendezvous point, telling it to +/// make a connection to the client. +#[derive(Debug, Clone)] +pub struct Rendezvous1 { + /// The cookie originally sent by the client in its ESTABLISH_REND message. + cookie: RendCookie, + /// The message to send the client. + message: Vec, +} + +impl Body for Rendezvous1 { + fn decode_from_reader(r: &mut Reader<'_>) -> Result { + todo!() + } + + fn encode_onto(self, w: &mut W) -> EncodeResult<()> { + todo!() + } +} + +/// A message sent from the rendezvous point to the client, telling it about the +/// onion service's message. +#[derive(Debug, Clone)] +pub struct Rendezvous2 { + /// The message from the onion service. + message: Vec, +} + +impl Body for Rendezvous2 { + fn decode_from_reader(r: &mut Reader<'_>) -> Result { + todo!() + } + + fn encode_onto(self, w: &mut W) -> EncodeResult<()> { + todo!() + } +} + +/// Reply sent from the introduction point to the onion service, telling it that +/// an introduction point is now established. +#[derive(Debug, Clone)] +pub struct IntroEstablished { + /// The extensions included in this cell. + // + // TODO hs: we should extract this with any DOS related extensions, depending on what we + // decide to do with extension in general. + extensions: Vec, +} + +impl Body for IntroEstablished { + fn decode_from_reader(r: &mut Reader<'_>) -> Result { + todo!() + } + + fn encode_onto(self, w: &mut W) -> EncodeResult<()> { + todo!() + } +} + +/// An extension included in an [`IntroEstablished`] message. +#[derive(Debug, Clone)] +#[non_exhaustive] +pub enum IntroEstExtension { + /// An unrecognized extension. + Unrecognized(Vec), +} + +/// A reply from the introduction point to the client, telling it that its +/// introduce1 was received. +#[derive(Clone, Debug)] +pub struct IntroduceAck { + // TODO hs: use a caret enum for this. + /// The status reported for the Introduce1 message. + status_code: u16, + // TODO hs: add extensions. +} + +impl Body for IntroduceAck { + fn decode_from_reader(r: &mut Reader<'_>) -> Result { + todo!() + } + + fn encode_onto(self, w: &mut W) -> EncodeResult<()> { + todo!() + } +} + +super::msg::empty_body! { + /// Acknowledges an EstablishRendezvous message. + pub struct RendezvousEstablished {} +} diff --git a/crates/tor-cell/src/relaycell/hs/est_intro.rs b/crates/tor-cell/src/relaycell/hs/est_intro.rs new file mode 100644 index 000000000..2f9a0ee62 --- /dev/null +++ b/crates/tor-cell/src/relaycell/hs/est_intro.rs @@ -0,0 +1,203 @@ +//! Define the ESTABLISH_INTRO message and related types. + +use caret::caret_int; +use tor_bytes::{EncodeError, EncodeResult, Readable, Reader, Result, Writeable, Writer}; +use tor_units::BoundedInt32; + +use crate::relaycell::{hs::ext::*, hs::AuthKeyType, msg}; + +caret_int! { + /// The introduction protocol extension type + #[derive(Ord, PartialOrd)] + pub struct EstIntroExtType(u8) { + /// The extension used to send DoS parameters + DOS_PARAMS = 1, + } +} + +caret_int! { + /// The recognized parameter types in an establish intro + /// DoS extension. + pub struct EstIntroExtDosParamType(u8) { + /// The rate per second of INTRODUCE2 cell relayed + /// to the service. + DOS_INTRODUCE2_RATE_PER_SEC = 1, + /// The burst per second of INTRODUCE2 cell relayed + /// to the service + DOS_INTRODUCE2_BURST_PER_SEC = 2, + } +} + +/// An establish Introduction protocol extension +#[derive(Debug, Clone)] +pub struct DosParams { + /// An optional parameter indicates the rate per second of + /// INTRODUCE2 cell relayed to the service. + /// + /// Min: 0, Max: 2147483647 + rate_per_sec: Option>, + /// An optional parameter indicates the burst per second of + /// INTRODUCE2 cell relayed to the service + /// + /// Min: 0, Max: 2147483647 + burst_per_sec: Option>, +} + +impl DosParams { + /// Create a new establish intro DoS extension. + pub fn new(rate_per_sec: Option, burst_per_sec: Option) -> crate::Result { + let normalize = |supplied: Option| -> crate::Result<_> { + supplied + .map(|val| { + BoundedInt32::checked_new(val).map_err(|_| { + crate::err::Error::CantEncode( + "EST_INTRO_DOS_EXT parameter value out of bound.", + ) + }) + }) + .transpose() + }; + Ok(Self { + rate_per_sec: normalize(rate_per_sec)?, + burst_per_sec: normalize(burst_per_sec)?, + }) + } +} + +impl Ext for DosParams { + type Id = EstIntroExtType; + fn type_id(&self) -> EstIntroExtType { + EstIntroExtType::DOS_PARAMS + } + fn take_body_from(b: &mut Reader<'_>) -> Result { + let n_prams = b.take_u8()?; + let mut rate_per_sec = None; + let mut burst_per_sec = None; + for _i in 0..n_prams { + let param_to_store = match b.take_u8()?.into() { + EstIntroExtDosParamType::DOS_INTRODUCE2_RATE_PER_SEC => Some(&mut rate_per_sec), + EstIntroExtDosParamType::DOS_INTRODUCE2_BURST_PER_SEC => Some(&mut burst_per_sec), + _ => None, + }; + if let Some(param) = param_to_store { + if let Ok(rate) = i32::try_from(b.take_u64()?) { + *param = BoundedInt32::checked_new(rate).ok(); + } + } + } + Ok(Self { + rate_per_sec, + burst_per_sec, + }) + } + fn write_body_onto(&self, b: &mut B) -> EncodeResult<()> { + let mut params = vec![]; + let mut push_params = |ty, value| { + if let Some(value) = value { + params.push((ty, value)); + } + }; + push_params( + EstIntroExtDosParamType::DOS_INTRODUCE2_RATE_PER_SEC, + self.rate_per_sec, + ); + push_params( + EstIntroExtDosParamType::DOS_INTRODUCE2_BURST_PER_SEC, + self.burst_per_sec, + ); + b.write_u8(u8::try_from(params.len()).map_err(|_| EncodeError::BadLengthValue)?); + for (t, v) in params { + b.write_u8(t.get()); + b.write_u64(v.get() as u64); + } + Ok(()) + } +} + +decl_extension_group! { + /// An extension to an EstablishIntro cell. + #[derive(Debug,Clone)] + enum EstablishIntroExt [ EstIntroExtType ] { + DosParams, + } +} + +/// A hidden services establishes a new introduction point, +/// by sending an EstablishIntro message. +#[derive(Debug, Clone)] +pub struct EstablishIntro { + /// Introduction point auth key type and the type of + /// the MAC used in `handshake_auth`. + auth_key_type: AuthKeyType, + /// The public introduction point auth key. + auth_key: Vec, + /// A list of extensions on this cell. + extensions: ExtList, + /// the MAC of all earlier fields in the cell. + handshake_auth: [u8; 32], + /// A signature using `auth_key` of all contents + /// of the cell. + sig: Vec, +} + +impl msg::Body for EstablishIntro { + fn decode_from_reader(r: &mut Reader<'_>) -> Result { + let auth_key_type = r.take_u8()?.into(); + let auth_key_len = r.take_u16()?; + let auth_key = r.take(auth_key_len as usize)?.into(); + let extensions = r.extract()?; + let handshake_auth = r.extract()?; + let sig_len = r.take_u16()?; + let sig = r.take(sig_len as usize)?.into(); + Ok(EstablishIntro { + auth_key_type, + auth_key, + extensions, + handshake_auth, + sig, + }) + } + fn encode_onto(self, w: &mut W) -> EncodeResult<()> { + w.write_u8(self.auth_key_type.get()); + w.write_u16(u16::try_from(self.auth_key.len()).map_err(|_| EncodeError::BadLengthValue)?); + w.write_all(&self.auth_key[..]); + w.write(&self.extensions)?; + w.write_all(&self.handshake_auth[..]); + w.write_u16(u16::try_from(self.sig.len()).map_err(|_| EncodeError::BadLengthValue)?); + w.write_all(&self.sig[..]); + Ok(()) + } +} + +impl EstablishIntro { + /// All arguments constructor + pub fn new( + auth_key_type: AuthKeyType, + auth_key: Vec, + handshake_auth: [u8; 32], + sig: Vec, + ) -> Self { + Self { + auth_key_type, + auth_key, + handshake_auth, + sig, + extensions: Default::default(), + } + } + + /// Set EST_INTRO_DOS_EXT with given `extension_dos`. + pub fn set_extension_dos(&mut self, extension_dos: DosParams) { + self.extensions.replace_by_type(extension_dos.into()); + } + + /// Add an extension of some other type. + pub fn set_extension_other(&mut self, other: UnrecognizedExt) { + self.extensions.replace_by_type(other.into()); + } + + // TODO hs: we'll need accessors. + // + // TODO hs: we will need some way to ensure that the mac is valid and well-signed. Possibly + // we should look into using a SignatureGated (if we can reasonably do so?) +} diff --git a/crates/tor-cell/src/relaycell/hs/ext.rs b/crates/tor-cell/src/relaycell/hs/ext.rs new file mode 100644 index 000000000..e2fcdd1c6 --- /dev/null +++ b/crates/tor-cell/src/relaycell/hs/ext.rs @@ -0,0 +1,219 @@ +//! Helpers to manage lists of HS cell extensions. +// +// TODO: We might generalize this even more in the future to handle other +// similar lists in our cell protocol. + +use tor_bytes::{EncodeError, EncodeResult, Readable, Reader, Result, Writeable, Writer}; + +/// A list of extensions, represented in a common format used by many HS-related +/// message. +/// +/// The common format is: +/// ```text +/// N_EXTENSIONS [1 byte] +/// N_EXTENSIONS times: +/// EXT_FIELD_TYPE [1 byte] +/// EXT_FIELD_LEN [1 byte] +/// EXT_FIELD [EXT_FIELD_LEN bytes] +/// ``` +/// +/// It is subject to the additional restraints: +/// +/// * Each extension type SHOULD be sent only once in a message. +/// * Parties MUST ignore any occurrences all occurrences of an extension +/// with a given type after the first such occurrence. +/// * Extensions SHOULD be sent in numerically ascending order by type. +#[derive(Clone, Debug, derive_more::Deref, derive_more::DerefMut)] +pub(super) struct ExtList { + /// The extensions themselves. + extensions: Vec, +} +impl Default for ExtList { + fn default() -> Self { + Self { + extensions: Vec::new(), + } + } +} +/// An kind of extension that can be used with some kind of HS-related message. +/// +/// Each extendible message will likely define its own enum, +/// implementing this trait, +/// representing the possible extensions. +pub(super) trait ExtGroup: Readable + Writeable { + /// An identifier kind used with this sort of extension + type Id: From + Into + Eq + PartialEq + Ord + Copy; + /// The field-type id for this particular extension. + fn type_id(&self) -> Self::Id; +} +/// A single typed extension that can be used with some kind of HS-related message. +pub(super) trait Ext: Sized { + /// An identifier kind used with this sort of extension. + /// + /// Typically defined with caret_int. + type Id: From + Into; + /// The field-type id for this particular extension. + fn type_id(&self) -> Self::Id; + /// Extract the body (not the type or the length) from a single + /// extension. + fn take_body_from(b: &mut Reader<'_>) -> Result; + /// Write the body (not the type or the length) for a single extension. + fn write_body_onto(&self, b: &mut B) -> EncodeResult<()>; +} +impl Readable for ExtList { + fn take_from(b: &mut Reader<'_>) -> Result { + let n_extensions = b.take_u8()?; + let extensions: Result> = (0..n_extensions).map(|_| b.extract::()).collect(); + Ok(Self { + extensions: extensions?, + }) + } +} +impl Writeable for ExtList { + fn write_onto(&self, b: &mut B) -> EncodeResult<()> { + let n_extensions = self + .extensions + .len() + .try_into() + .map_err(|_| EncodeError::BadLengthValue)?; + b.write_u8(n_extensions); + let mut exts_sorted: Vec<&T> = self.extensions.iter().collect(); + exts_sorted.sort_by_key(|ext| ext.type_id()); + exts_sorted.iter().try_for_each(|ext| ext.write_onto(b))?; + Ok(()) + } +} +impl ExtList { + /// Insert `ext` into this list of extensions, replacing any previous + /// extension with the same field type ID. + pub(super) fn replace_by_type(&mut self, ext: T) { + self.retain(|e| e.type_id() != ext.type_id()); + self.push(ext); + } + + /// Find the first entry (if any) in this list of extensions with a given + /// type ID. + /// + /// (We do not enforce uniqueness here, since the spec explicitly says that + /// we should ignore all but the first extension of a given type.) + pub(super) fn by_type(&self, id: T::Id) -> Option<&T> { + self.iter().find(|e| e.type_id() == id) + } +} + +/// An unrecognized extension for some HS-related message. +#[derive(Clone, Debug)] +pub struct UnrecognizedExt { + /// The field type ID for this extension. + pub(super) type_id: ID, + /// The body of this extension. + pub(super) body: Vec, +} + +impl UnrecognizedExt { + /// Return a new unrecognized extension with a given ID and body. + /// + /// NOTE: nothing actually enforces that this type ID is not + /// recognized. + /// + /// NOTE: This function accepts bodies longer than 255 bytes, but + /// it is not possible to encode them. + pub fn new(type_id: ID, body: impl Into>) -> Self { + Self { + type_id, + body: body.into(), + } + } +} + +/// Declare an Extension group that takes a given identifier. +// +// TODO: This is rather similar to restrict_msg(), isn't it? Also, We use this +// pattern of (number, (cmd, length, body)*) a few of times in Tor outside the +// hs module. Perhaps we can extend and unify our code here... +macro_rules! decl_extension_group { + { + $( #[$meta:meta] )* + $v:vis enum $id:ident [ $type_id:ty ] { + $( + $(#[$cmeta:meta])* + $case:ident),* + $(,)? + } + } => {paste::paste!{ + $( #[$meta] )* + $v enum $id { + $( $(#[$cmeta])* + $case($case), + )* + /// An extension of a type we do not recognize. + Unrecognized(UnrecognizedExt<$type_id>) + } + impl Readable for $id { + fn take_from(b: &mut Reader<'_>) -> Result { + let type_id = b.take_u8()?.into(); + Ok(match type_id { + $( + $type_id::[< $case:snake:upper >] => { + Self::$case( b.read_nested_u8len(|r| $case::take_body_from(r))? ) + } + )* + other => { + Self::Unrecognized(UnrecognizedExt { + type_id, + body: b.read_nested_u8len(|r| Ok(r.take_rest().into()))?, + }) + } + }) + } + } + impl Writeable for $id { + fn write_onto(&self, b: &mut B) -> EncodeResult< +()> { + #[allow(unused)] + use std::ops::DerefMut; + match self { + $( + Self::$case(val) => { + b.write_u8(val.type_id().into()); + let mut nested = b.write_nested_u8len(); + val.write_body_onto(nested.deref_mut())?; + nested.finish()?; + } + )* + Self::Unrecognized(unrecognized) => { + b.write_u8(unrecognized.type_id.into()); + let mut nested = b.write_nested_u8len(); + nested.write_all(&unrecognized.body[..]); + nested.finish()?; + } + } + Ok(()) + } + } + impl ExtGroup for $id { + type Id = $type_id; + fn type_id(&self) -> Self::Id { + match self { + $( + Self::$case(val) => val.type_id(), + )* + Self::Unrecognized(unrecognized) => unrecognized.type_id, + } + } + } + $( + impl From<$case> for $id { + fn from(val: $case) -> $id { + $id :: $case ( val ) + } + } + )* + impl From> for $id { + fn from(val: UnrecognizedExt<$type_id>) -> $id { + $id :: Unrecognized(val) + } + } +}} +} +pub(super) use decl_extension_group; diff --git a/crates/tor-cell/src/relaycell/msg.rs b/crates/tor-cell/src/relaycell/msg.rs index 86c561c59..ae9e14f65 100644 --- a/crates/tor-cell/src/relaycell/msg.rs +++ b/crates/tor-cell/src/relaycell/msg.rs @@ -19,9 +19,9 @@ use bitflags::bitflags; #[cfg_attr(docsrs, doc(cfg(feature = "onion-service")))] #[cfg(feature = "onion-service")] -pub use super::onion_service::{ - EstablishIntro, EstablishRendezvous, IntroEstablished, Introduce1, Introduce2, IntroduceAck, - Rendezvous1, Rendezvous2, RendezvousEstablished, +pub use super::hs::{ + est_intro::EstablishIntro, EstablishRendezvous, IntroEstablished, Introduce1, Introduce2, + IntroduceAck, Rendezvous1, Rendezvous2, RendezvousEstablished, }; #[cfg_attr(docsrs, doc(cfg(feature = "experimental-udp")))] #[cfg(feature = "experimental-udp")] diff --git a/crates/tor-cell/src/relaycell/onion_service.rs b/crates/tor-cell/src/relaycell/onion_service.rs deleted file mode 100644 index da9a495b3..000000000 --- a/crates/tor-cell/src/relaycell/onion_service.rs +++ /dev/null @@ -1,459 +0,0 @@ -//! Encoding and decoding for relay messages related to onion services. - -#![allow(dead_code, unused_variables)] // TODO hs: remove. - -// TODO hs design: We need to discuss how we want to handle extensions in these -// cells: extensions are used all over the protocol, and it would be nice to be -// more clever about them. -// -// It would be neat if recognized extension types were decoded into recognized -// structures. On the other hand, it would be good to retain unrecognized -// extension types, so that other code can use them in the future without having to -// add support here. - -// TODO hs: we'll need accessors for the useful fields in all these types. - -use super::msg::{self, Body}; -use caret::caret_int; -use tor_bytes::{EncodeError, EncodeResult, Error as BytesError, Readable, Result, Writeable}; -use tor_bytes::{Reader, Writer}; -use tor_hscrypto::RendCookie; -use tor_llcrypto::pk::rsa::RsaIdentity; -use tor_units::BoundedInt32; - -caret_int! { - /// The type of the introduction point auth key - pub struct AuthKeyType(u8) { - /// Ed25519; SHA3-256 - ED25519_SHA3_256 = 2, - } -} - -caret_int! { - /// The introduction protocol extension type - pub struct EstIntroExtType(u8) { - /// The extension used to send DoS parameters - EST_INTRO_DOS_EXT = 1, - } -} - -caret_int! { - /// The recognized parameter types in an establish intro - /// DoS extension. - pub struct EstIntroExtDosParamType(u8) { - /// The rate per second of INTRODUCE2 cell relayed - /// to the service. - DOS_INTRODUCE2_RATE_PER_SEC = 1, - /// The burst per second of INTRODUCE2 cell relayed - /// to the service - DOS_INTRODUCE2_BURST_PER_SEC = 2, - } -} - -/// An establish Introduction protocol extension -#[derive(Debug, Clone)] -pub struct EstIntroExtDoS { - /// An optional parameter indicates the rate per second of - /// INTRODUCE2 cell relayed to the service. - /// - /// Min: 0, Max: 2147483647 - rate_per_sec: Option>, - /// An optional parameter indicates the burst per second of - /// INTRODUCE2 cell relayed to the service - /// - /// Min: 0, Max: 2147483647 - burst_per_sec: Option>, -} - -impl EstIntroExtDoS { - /// Create a new establish intro DoS extension. - pub fn new(rate_per_sec: Option, burst_per_sec: Option) -> crate::Result { - let normalize = |supplied: Option| -> crate::Result<_> { - supplied - .map(|val| { - BoundedInt32::checked_new(val).map_err(|_| { - crate::err::Error::CantEncode( - "EST_INTRO_DOS_EXT parameter value out of bound.", - ) - }) - }) - .transpose() - }; - Ok(Self { - rate_per_sec: normalize(rate_per_sec)?, - burst_per_sec: normalize(burst_per_sec)?, - }) - } -} - -impl Readable for EstIntroExtDoS { - fn take_from(b: &mut Reader<'_>) -> Result { - let n_prams = b.take_u8()?; - let mut rate_per_sec = None; - let mut burst_per_sec = None; - for _i in 0..n_prams { - let param_to_store = match b.take_u8()?.into() { - EstIntroExtDosParamType::DOS_INTRODUCE2_RATE_PER_SEC => Some(&mut rate_per_sec), - EstIntroExtDosParamType::DOS_INTRODUCE2_BURST_PER_SEC => Some(&mut burst_per_sec), - _ => None, - }; - if let Some(param) = param_to_store { - if let Ok(rate) = i32::try_from(b.take_u64()?) { - *param = BoundedInt32::checked_new(rate).ok(); - } - } - } - Ok(Self { - rate_per_sec, - burst_per_sec, - }) - } -} - -impl Writeable for EstIntroExtDoS { - fn write_onto(&self, b: &mut B) -> EncodeResult<()> { - let mut params = vec![]; - let mut push_params = |ty, value| { - if let Some(value) = value { - params.push((ty, value)); - } - }; - push_params( - EstIntroExtDosParamType::DOS_INTRODUCE2_RATE_PER_SEC, - self.rate_per_sec, - ); - push_params( - EstIntroExtDosParamType::DOS_INTRODUCE2_BURST_PER_SEC, - self.burst_per_sec, - ); - b.write_u8(u8::try_from(params.len()).map_err(|_| EncodeError::BadLengthValue)?); - for (t, v) in params { - b.write_u8(t.get()); - b.write_u64(v.get() as u64); - } - Ok(()) - } -} - -/// A hidden services establishes a new introduction point, -/// by sending an EstablishIntro message. -#[derive(Debug, Clone)] -pub struct EstablishIntro { - /// Introduction point auth key type and the type of - /// the MAC used in `handshake_auth`. - auth_key_type: AuthKeyType, - /// The public introduction point auth key. - auth_key: Vec, - /// An optional denial-of-service extension. - // - // TODO hs: we may want to consider making this a vector of extensions instead, - // to allow unrecognized extensions? - extension_dos: Option, - /// the MAC of all earlier fields in the cell. - handshake_auth: [u8; 32], - /// A signature using `auth_key` of all contents - /// of the cell. - sig: Vec, -} - -impl msg::Body for EstablishIntro { - fn decode_from_reader(r: &mut Reader<'_>) -> Result { - let auth_key_type = r.take_u8()?.into(); - let auth_key_len = r.take_u16()?; - let auth_key = r.take(auth_key_len as usize)?.into(); - let n_ext = r.take_u8()?; - let mut extension_dos = None; - for _ in 0..n_ext { - let ext_type: EstIntroExtType = r.take_u8()?.into(); - r.read_nested_u8len(|r| { - if ext_type == EstIntroExtType::EST_INTRO_DOS_EXT { - extension_dos.get_or_insert(r.extract()?); - } else { - r.take_rest(); - } - Ok(()) - })?; - } - let handshake_auth = r.extract()?; - let sig_len = r.take_u16()?; - let sig = r.take(sig_len as usize)?.into(); - Ok(EstablishIntro { - auth_key_type, - auth_key, - extension_dos, - handshake_auth, - sig, - }) - } - fn encode_onto(self, w: &mut W) -> EncodeResult<()> { - w.write_u8(self.auth_key_type.get()); - w.write_u16(u16::try_from(self.auth_key.len()).map_err(|_| EncodeError::BadLengthValue)?); - w.write_all(&self.auth_key[..]); - - let mut extensions: Vec<(EstIntroExtType, Vec)> = vec![]; - if let Some(extension_dos) = self.extension_dos { - let mut extension = vec![]; - extension.write(&extension_dos)?; - extensions.push((EstIntroExtType::EST_INTRO_DOS_EXT, extension)); - } - w.write_u8(u8::try_from(extensions.len()).map_err(|_| EncodeError::BadLengthValue)?); - for (t, v) in extensions { - w.write_u8(t.get()); - let mut w = w.write_nested_u8len(); - w.write(&v)?; - w.finish()?; - } - - w.write_all(&self.handshake_auth[..]); - w.write_u16(u16::try_from(self.sig.len()).map_err(|_| EncodeError::BadLengthValue)?); - w.write_all(&self.sig[..]); - Ok(()) - } -} - -impl EstablishIntro { - /// All arguments constructor - pub fn new( - auth_key_type: AuthKeyType, - auth_key: Vec, - handshake_auth: [u8; 32], - sig: Vec, - ) -> Self { - Self { - auth_key_type, - auth_key, - handshake_auth, - sig, - extension_dos: None, - } - } - - /// Set EST_INTRO_DOS_EXT with given `extension_dos`. - pub fn set_extension_dos(&mut self, extension_dos: EstIntroExtDoS) { - self.extension_dos = Some(extension_dos); - } - - // TODO hs: we'll need accessors. - // - // TODO hs: we will need some way to ensure that the mac is valid and well-signed. Possibly - // we should look into using a SignatureGated (if we can reasonably do so?) -} - -/// A message sent from client to rendezvous point. -#[derive(Debug, Clone)] -pub struct EstablishRendezvous { - /// A rendezvous cookie is an arbitrary 20-byte value, - /// chosen randomly by the client. - cookie: [u8; EstablishRendezvous::COOKIE_LEN], // TODO hs: Make this a RendCookie. -} -impl EstablishRendezvous { - /// The only acceptable length of a rendezvous cookie. - pub const COOKIE_LEN: usize = 20; - - /// Construct a new establish rendezvous cell. - pub fn new(cookie: [u8; Self::COOKIE_LEN]) -> Self { - Self { cookie } - } -} -impl msg::Body for EstablishRendezvous { - fn decode_from_reader(r: &mut Reader<'_>) -> Result { - let cookie = r.extract()?; - r.take_rest(); - Ok(Self { cookie }) - } - fn encode_onto(self, w: &mut W) -> EncodeResult<()> { - w.write(&self.cookie) - } -} - -#[derive(Debug, Clone)] -/// A message sent from client to introduction point. -pub struct Introduce1(Introduce); - -impl msg::Body for Introduce1 { - fn decode_from_reader(r: &mut Reader<'_>) -> Result { - Ok(Self(Introduce::decode_from_reader(r)?)) - } - fn encode_onto(self, w: &mut W) -> EncodeResult<()> { - self.0.encode_onto(w) - } -} - -impl Introduce1 { - /// All arguments constructor - pub fn new(auth_key_type: AuthKeyType, auth_key: Vec, encrypted: Vec) -> Self { - Self(Introduce::new(auth_key_type, auth_key, encrypted)) - } -} - -#[derive(Debug, Clone)] -/// A message sent from introduction point to hidden service host. -pub struct Introduce2(Introduce); - -impl msg::Body for Introduce2 { - fn decode_from_reader(r: &mut Reader<'_>) -> Result { - Ok(Self(Introduce::decode_from_reader(r)?)) - } - fn encode_onto(self, w: &mut W) -> EncodeResult<()> { - self.0.encode_onto(w) - } -} - -impl Introduce2 { - /// All arguments constructor - pub fn new(auth_key_type: AuthKeyType, auth_key: Vec, encrypted: Vec) -> Self { - Self(Introduce::new(auth_key_type, auth_key, encrypted)) - } -} - -#[derive(Debug, Clone)] -/// A message body shared by Introduce1 and Introduce2 -struct Introduce { - /// Introduction point auth key type and the type of - /// the MAC used in `handshake_auth`. - auth_key_type: AuthKeyType, - /// The public introduction point auth key. - auth_key: Vec, - /// Up to end of relay payload. - encrypted: Vec, -} - -impl Introduce { - /// All arguments constructor - fn new(auth_key_type: AuthKeyType, auth_key: Vec, encrypted: Vec) -> Self { - Self { - auth_key_type, - auth_key, - encrypted, - } - } - /// Decode an Introduce message body from the given reader - fn decode_from_reader(r: &mut Reader<'_>) -> Result { - let legacy_key_id: RsaIdentity = r.extract()?; - if !legacy_key_id.is_zero() { - return Err(BytesError::InvalidMessage( - "legacy key id in Introduce1.".into(), - )); - } - let auth_key_type = r.take_u8()?.into(); - let auth_key_len = r.take_u16()?; - let auth_key = r.take(auth_key_len as usize)?.into(); - let n_ext = r.take_u8()?; - for _ in 0..n_ext { - let _ext_type = r.take_u8()?; - r.read_nested_u8len(|r| { - r.take_rest(); - Ok(()) - })?; - } - let encrypted = r.take_rest().into(); - Ok(Self { - auth_key_type, - auth_key, - encrypted, - }) - } - /// Encode an Introduce message body onto the given writer - fn encode_onto(self, w: &mut W) -> EncodeResult<()> { - w.write_all(&[0_u8; 20]); - w.write_u8(self.auth_key_type.get()); - w.write_u16(u16::try_from(self.auth_key.len()).map_err(|_| EncodeError::BadLengthValue)?); - w.write_all(&self.auth_key[..]); - // No Introduce1 extension for now. - w.write_u8(0_u8); - w.write_all(&self.encrypted[..]); - Ok(()) - } -} - -/// A message sent from an onion service to a rendezvous point, telling it to -/// make a connection to the client. -#[derive(Debug, Clone)] -pub struct Rendezvous1 { - /// The cookie originally sent by the client in its ESTABLISH_REND message. - cookie: RendCookie, - /// The message to send the client. - message: Vec, -} - -impl Body for Rendezvous1 { - fn decode_from_reader(r: &mut Reader<'_>) -> Result { - todo!() - } - - fn encode_onto(self, w: &mut W) -> EncodeResult<()> { - todo!() - } -} - -/// A message sent from the rendezvous point to the client, telling it about the -/// onion service's message. -#[derive(Debug, Clone)] -pub struct Rendezvous2 { - /// The message from the onion service. - message: Vec, -} - -impl Body for Rendezvous2 { - fn decode_from_reader(r: &mut Reader<'_>) -> Result { - todo!() - } - - fn encode_onto(self, w: &mut W) -> EncodeResult<()> { - todo!() - } -} - -/// Reply sent from the introduction point to the onion service, telling it that -/// an introduction point is now established. -#[derive(Debug, Clone)] -pub struct IntroEstablished { - /// The extensions included in this cell. - // - // TODO hs: we should extract this with any DOS related extensions, depending on what we - // decide to do with extension in general. - extensions: Vec, -} - -impl Body for IntroEstablished { - fn decode_from_reader(r: &mut Reader<'_>) -> Result { - todo!() - } - - fn encode_onto(self, w: &mut W) -> EncodeResult<()> { - todo!() - } -} - -/// An extension included in an [`IntroEstablished`] message. -#[derive(Debug, Clone)] -#[non_exhaustive] -pub enum IntroEstExtension { - /// An unrecognized extension. - Unrecognized(Vec), -} - -/// A reply from the introduction point to the client, telling it that its -/// introduce1 was received. -#[derive(Clone, Debug)] -pub struct IntroduceAck { - // TODO hs: use a caret enum for this. - /// The status reported for the Introduce1 message. - status_code: u16, - // TODO hs: add extensions. -} - -impl Body for IntroduceAck { - fn decode_from_reader(r: &mut Reader<'_>) -> Result { - todo!() - } - - fn encode_onto(self, w: &mut W) -> EncodeResult<()> { - todo!() - } -} - -super::msg::empty_body! { - /// Acknowledges an EstablishRendezvous message. - pub struct RendezvousEstablished {} -} diff --git a/crates/tor-cell/tests/testvec_relaymsg.rs b/crates/tor-cell/tests/testvec_relaymsg.rs index 4b5062e67..02bbe4101 100644 --- a/crates/tor-cell/tests/testvec_relaymsg.rs +++ b/crates/tor-cell/tests/testvec_relaymsg.rs @@ -15,7 +15,7 @@ use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use hex_literal::hex; #[cfg(feature = "onion-service")] -use tor_cell::relaycell::onion_service; +use tor_cell::relaycell::hs; #[cfg(feature = "experimental-udp")] use tor_cell::relaycell::udp; @@ -631,7 +631,7 @@ fn test_establish_rendezvous() { cmd, // 20 ones "0101010101010101010101010101010101010101", - &onion_service::EstablishRendezvous::new(cookie).into(), + &hs::EstablishRendezvous::new(cookie).into(), ); // Extra bytes are ignored @@ -658,16 +658,13 @@ fn test_establish_rendezvous() { #[cfg(feature = "onion-service")] #[test] fn test_establish_intro() { - use tor_cell::relaycell::{ - msg::AnyRelayMsg, - onion_service::{AuthKeyType, EstIntroExtDoS, EstablishIntro}, - }; + use tor_cell::relaycell::hs::{est_intro::*, AuthKeyType, UnrecognizedExt}; let cmd = RelayCmd::ESTABLISH_INTRO; let auth_key_type = AuthKeyType::ED25519_SHA3_256; let auth_key = vec![0, 1, 2, 3]; - let extension_dos = EstIntroExtDoS::new(Some(1_i32), Some(2_i32)) - .expect("invalid EST_INTRO_DOS_EXT parameter(s)"); + let extension_dos = + DosParams::new(Some(1_i32), Some(2_i32)).expect("invalid EST_INTRO_DOS_EXT parameter(s)"); let handshake_auth = [1; 32]; let sig = vec![0, 1, 2, 3]; assert_eq!(Into::::into(cmd), 32); @@ -700,34 +697,29 @@ fn test_establish_intro() { // and one unknown extension let auth_key = vec![0, 1, 2, 3]; let sig = vec![0, 1, 2, 3]; - let extension_dos = EstIntroExtDoS::new(Some(1_i32), Some(2_i32)) - .expect("invalid EST_INTRO_DOS_EXT parameter(s)"); - - let body = "02 0004 00010203 - 02 01 13 02 01 0000000000000001 02 0000000000000002 02 01 00 - 0101010101010101010101010101010101010101010101010101010101010101 - 0004 00010203"; - let actual_msg = decode(cmd, &unhex(body)[..]).unwrap(); - let mut actual_bytes = vec![]; - let mut expect_bytes = vec![]; - actual_msg - .encode_onto(&mut actual_bytes) - .expect("Encode msg onto byte vector"); + let extension_dos = + DosParams::new(Some(1_i32), Some(2_i32)).expect("invalid EST_INTRO_DOS_EXT parameter(s)"); + let extension_unrecognized = UnrecognizedExt::new(2.into(), vec![0]); let mut es_intro = EstablishIntro::new(auth_key_type, auth_key, handshake_auth, sig); es_intro.set_extension_dos(extension_dos); - let expected_msg: AnyRelayMsg = es_intro.into(); - expected_msg - .encode_onto(&mut expect_bytes) - .expect("Encode msg onto byte vector"); - assert_eq!(actual_bytes, expect_bytes); + es_intro.set_extension_other(extension_unrecognized); + + msg( + cmd, + "02 0004 00010203 + 02 01 13 02 01 0000000000000001 02 0000000000000002 02 01 00 + 0101010101010101010101010101010101010101010101010101010101010101 + 0004 00010203", + &es_intro.into(), + ); } #[cfg(feature = "onion-service")] #[test] fn test_introduce() { use tor_cell::relaycell::{ + hs::{AuthKeyType, Introduce1}, msg::AnyRelayMsg, - onion_service::{AuthKeyType, Introduce1}, }; // Testing with Introduce1 only should be sufficient as long as