From 5c85397bb759cbecc4f55372504c2779090bd8dc Mon Sep 17 00:00:00 2001 From: Nick Mathewson Date: Wed, 15 Feb 2023 07:38:19 -0500 Subject: [PATCH 1/6] tor-cell: rename onion_service module to hs --- crates/tor-cell/src/relaycell.rs | 4 ++-- crates/tor-cell/src/relaycell/{onion_service.rs => hs.rs} | 0 crates/tor-cell/src/relaycell/msg.rs | 2 +- crates/tor-cell/tests/testvec_relaymsg.rs | 8 ++++---- 4 files changed, 7 insertions(+), 7 deletions(-) rename crates/tor-cell/src/relaycell/{onion_service.rs => hs.rs} (100%) diff --git a/crates/tor-cell/src/relaycell.rs b/crates/tor-cell/src/relaycell.rs index d6d53cd7e..94c227cb9 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/onion_service.rs b/crates/tor-cell/src/relaycell/hs.rs similarity index 100% rename from crates/tor-cell/src/relaycell/onion_service.rs rename to crates/tor-cell/src/relaycell/hs.rs diff --git a/crates/tor-cell/src/relaycell/msg.rs b/crates/tor-cell/src/relaycell/msg.rs index f81bcf0d8..25bd7e15b 100644 --- a/crates/tor-cell/src/relaycell/msg.rs +++ b/crates/tor-cell/src/relaycell/msg.rs @@ -19,7 +19,7 @@ use bitflags::bitflags; #[cfg_attr(docsrs, doc(cfg(feature = "onion-service")))] #[cfg(feature = "onion-service")] -pub use super::onion_service::{ +pub use super::hs::{ EstablishIntro, EstablishRendezvous, IntroEstablished, Introduce1, Introduce2, IntroduceAck, Rendezvous1, Rendezvous2, RendezvousEstablished, }; diff --git a/crates/tor-cell/tests/testvec_relaymsg.rs b/crates/tor-cell/tests/testvec_relaymsg.rs index 4b5062e67..04ccdeef8 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 @@ -659,8 +659,8 @@ fn test_establish_rendezvous() { #[test] fn test_establish_intro() { use tor_cell::relaycell::{ + hs::{AuthKeyType, EstIntroExtDoS, EstablishIntro}, msg::AnyRelayMsg, - onion_service::{AuthKeyType, EstIntroExtDoS, EstablishIntro}, }; let cmd = RelayCmd::ESTABLISH_INTRO; @@ -726,8 +726,8 @@ fn test_establish_intro() { #[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 From 37fcb0ca7cd6d68a328a24e71362cc3d789db15d Mon Sep 17 00:00:00 2001 From: Nick Mathewson Date: Wed, 15 Feb 2023 07:46:36 -0500 Subject: [PATCH 2/6] tor-cell: Extract establish-intro into its own module. Some of the HS message types have a lot of dependent types, like extensions and options for those extensions, and so on. Except when those extensions are portable across cell types, it makes sense to put them in their own modules. --- crates/tor-cell/src/relaycell/hs.rs | 215 +---------------- crates/tor-cell/src/relaycell/hs/est_intro.rs | 217 ++++++++++++++++++ crates/tor-cell/src/relaycell/msg.rs | 4 +- crates/tor-cell/tests/testvec_relaymsg.rs | 2 +- 4 files changed, 223 insertions(+), 215 deletions(-) create mode 100644 crates/tor-cell/src/relaycell/hs/est_intro.rs diff --git a/crates/tor-cell/src/relaycell/hs.rs b/crates/tor-cell/src/relaycell/hs.rs index da9a495b3..326b36c49 100644 --- a/crates/tor-cell/src/relaycell/hs.rs +++ b/crates/tor-cell/src/relaycell/hs.rs @@ -15,11 +15,12 @@ use super::msg::{self, Body}; use caret::caret_int; -use tor_bytes::{EncodeError, EncodeResult, Error as BytesError, Readable, Result, Writeable}; +use tor_bytes::{EncodeError, EncodeResult, Error as BytesError, Result}; use tor_bytes::{Reader, Writer}; use tor_hscrypto::RendCookie; use tor_llcrypto::pk::rsa::RsaIdentity; -use tor_units::BoundedInt32; + +pub mod est_intro; caret_int! { /// The type of the introduction point auth key @@ -29,216 +30,6 @@ caret_int! { } } -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 { 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..f9624854b --- /dev/null +++ b/crates/tor-cell/src/relaycell/hs/est_intro.rs @@ -0,0 +1,217 @@ +//! 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::AuthKeyType, msg}; + +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?) +} diff --git a/crates/tor-cell/src/relaycell/msg.rs b/crates/tor-cell/src/relaycell/msg.rs index 25bd7e15b..16c302119 100644 --- a/crates/tor-cell/src/relaycell/msg.rs +++ b/crates/tor-cell/src/relaycell/msg.rs @@ -20,8 +20,8 @@ use bitflags::bitflags; #[cfg_attr(docsrs, doc(cfg(feature = "onion-service")))] #[cfg(feature = "onion-service")] pub use super::hs::{ - EstablishIntro, EstablishRendezvous, IntroEstablished, Introduce1, Introduce2, IntroduceAck, - Rendezvous1, Rendezvous2, RendezvousEstablished, + 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/tests/testvec_relaymsg.rs b/crates/tor-cell/tests/testvec_relaymsg.rs index 04ccdeef8..d07cfadf4 100644 --- a/crates/tor-cell/tests/testvec_relaymsg.rs +++ b/crates/tor-cell/tests/testvec_relaymsg.rs @@ -659,7 +659,7 @@ fn test_establish_rendezvous() { #[test] fn test_establish_intro() { use tor_cell::relaycell::{ - hs::{AuthKeyType, EstIntroExtDoS, EstablishIntro}, + hs::{est_intro::*, AuthKeyType}, msg::AnyRelayMsg, }; From 5521df0909ff7afa2d78304c9376861dfcf7041a Mon Sep 17 00:00:00 2001 From: Nick Mathewson Date: Wed, 15 Feb 2023 08:44:43 -0500 Subject: [PATCH 3/6] tor-cell: Use a more generic mechanism for managing extensions Several HS message types have an extension list type. They all use the same framing for extensions, but each of them has separate extension types and separate extension namespaces. This commit simplifies establish_intro a little, and adds support for maintaining unrecognized extension types--at the expense of some new internal code. --- crates/tor-cell/src/relaycell/hs.rs | 12 +- crates/tor-cell/src/relaycell/hs/est_intro.rs | 80 +++---- crates/tor-cell/src/relaycell/hs/ext.rs | 213 ++++++++++++++++++ crates/tor-cell/tests/testvec_relaymsg.rs | 40 ++-- 4 files changed, 265 insertions(+), 80 deletions(-) create mode 100644 crates/tor-cell/src/relaycell/hs/ext.rs diff --git a/crates/tor-cell/src/relaycell/hs.rs b/crates/tor-cell/src/relaycell/hs.rs index 326b36c49..f05cf7a15 100644 --- a/crates/tor-cell/src/relaycell/hs.rs +++ b/crates/tor-cell/src/relaycell/hs.rs @@ -2,15 +2,6 @@ #![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}; @@ -21,6 +12,9 @@ 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 diff --git a/crates/tor-cell/src/relaycell/hs/est_intro.rs b/crates/tor-cell/src/relaycell/hs/est_intro.rs index f9624854b..2f9a0ee62 100644 --- a/crates/tor-cell/src/relaycell/hs/est_intro.rs +++ b/crates/tor-cell/src/relaycell/hs/est_intro.rs @@ -4,13 +4,14 @@ use caret::caret_int; use tor_bytes::{EncodeError, EncodeResult, Readable, Reader, Result, Writeable, Writer}; use tor_units::BoundedInt32; -use crate::relaycell::{hs::AuthKeyType, msg}; +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 - EST_INTRO_DOS_EXT = 1, + DOS_PARAMS = 1, } } @@ -29,7 +30,7 @@ caret_int! { /// An establish Introduction protocol extension #[derive(Debug, Clone)] -pub struct EstIntroExtDoS { +pub struct DosParams { /// An optional parameter indicates the rate per second of /// INTRODUCE2 cell relayed to the service. /// @@ -42,7 +43,7 @@ pub struct EstIntroExtDoS { burst_per_sec: Option>, } -impl EstIntroExtDoS { +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<_> { @@ -63,8 +64,12 @@ impl EstIntroExtDoS { } } -impl Readable for EstIntroExtDoS { - fn take_from(b: &mut Reader<'_>) -> Result { +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; @@ -85,10 +90,7 @@ impl Readable for EstIntroExtDoS { burst_per_sec, }) } -} - -impl Writeable for EstIntroExtDoS { - fn write_onto(&self, b: &mut B) -> EncodeResult<()> { + fn write_body_onto(&self, b: &mut B) -> EncodeResult<()> { let mut params = vec![]; let mut push_params = |ty, value| { if let Some(value) = value { @@ -112,6 +114,14 @@ impl Writeable for EstIntroExtDoS { } } +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)] @@ -121,11 +131,8 @@ pub struct EstablishIntro { 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, + /// 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 @@ -138,26 +145,14 @@ impl msg::Body for EstablishIntro { 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 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, - extension_dos, + extensions, handshake_auth, sig, }) @@ -166,21 +161,7 @@ impl msg::Body for EstablishIntro { 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(&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[..]); @@ -201,13 +182,18 @@ impl EstablishIntro { auth_key, handshake_auth, sig, - extension_dos: None, + extensions: Default::default(), } } /// 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); + 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. 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..99634fb64 --- /dev/null +++ b/crates/tor-cell/src/relaycell/hs/ext.rs @@ -0,0 +1,213 @@ +//! 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: +/// ``` +/// 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 declare its own variant of this trait. +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. +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/tests/testvec_relaymsg.rs b/crates/tor-cell/tests/testvec_relaymsg.rs index d07cfadf4..02bbe4101 100644 --- a/crates/tor-cell/tests/testvec_relaymsg.rs +++ b/crates/tor-cell/tests/testvec_relaymsg.rs @@ -658,16 +658,13 @@ fn test_establish_rendezvous() { #[cfg(feature = "onion-service")] #[test] fn test_establish_intro() { - use tor_cell::relaycell::{ - hs::{est_intro::*, AuthKeyType}, - msg::AnyRelayMsg, - }; + 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,26 +697,21 @@ 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")] From b4e48b0695e8a6fbb82f5d0dd4cff5b965bc5c4f Mon Sep 17 00:00:00 2001 From: Ian Jackson Date: Thu, 16 Feb 2023 13:19:04 +0000 Subject: [PATCH 4/6] Make a comment more accurate --- crates/tor-cell/src/relaycell/hs/ext.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/tor-cell/src/relaycell/hs/ext.rs b/crates/tor-cell/src/relaycell/hs/ext.rs index 99634fb64..2eb264674 100644 --- a/crates/tor-cell/src/relaycell/hs/ext.rs +++ b/crates/tor-cell/src/relaycell/hs/ext.rs @@ -37,7 +37,9 @@ impl Default for ExtList { } /// An kind of extension that can be used with some kind of HS-related message. /// -/// Each extendible message will likely declare its own variant of this trait. +/// 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; From 14417dc0981c6a1eec5adce826d1847794e1c533 Mon Sep 17 00:00:00 2001 From: Nick Mathewson Date: Thu, 16 Feb 2023 08:25:12 -0500 Subject: [PATCH 5/6] Add a TODO about combining several macros and patterns --- crates/tor-cell/src/relaycell/hs/ext.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/tor-cell/src/relaycell/hs/ext.rs b/crates/tor-cell/src/relaycell/hs/ext.rs index 2eb264674..3e8bbed14 100644 --- a/crates/tor-cell/src/relaycell/hs/ext.rs +++ b/crates/tor-cell/src/relaycell/hs/ext.rs @@ -127,6 +127,10 @@ impl UnrecognizedExt { } /// 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] )* From c4b451ffa64b6feec66a56740e62d0c98792e3f7 Mon Sep 17 00:00:00 2001 From: Nick Mathewson Date: Thu, 16 Feb 2023 09:00:07 -0500 Subject: [PATCH 6/6] Mark a quoted block as text, so doctest ignores it. --- crates/tor-cell/src/relaycell/hs/ext.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/tor-cell/src/relaycell/hs/ext.rs b/crates/tor-cell/src/relaycell/hs/ext.rs index 3e8bbed14..e2fcdd1c6 100644 --- a/crates/tor-cell/src/relaycell/hs/ext.rs +++ b/crates/tor-cell/src/relaycell/hs/ext.rs @@ -9,7 +9,7 @@ use tor_bytes::{EncodeError, EncodeResult, Readable, Reader, Result, Writeable, /// message. /// /// The common format is: -/// ``` +/// ```text /// N_EXTENSIONS [1 byte] /// N_EXTENSIONS times: /// EXT_FIELD_TYPE [1 byte]