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.
This commit is contained in:
Nick Mathewson 2023-02-15 08:44:43 -05:00
parent 37fcb0ca7c
commit 5521df0909
4 changed files with 265 additions and 80 deletions

View File

@ -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

View File

@ -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<BoundedInt32<0, { i32::MAX }>>,
}
impl EstIntroExtDoS {
impl DosParams {
/// Create a new establish intro DoS extension.
pub fn new(rate_per_sec: Option<i32>, burst_per_sec: Option<i32>) -> crate::Result<Self> {
let normalize = |supplied: Option<i32>| -> crate::Result<_> {
@ -63,8 +64,12 @@ impl EstIntroExtDoS {
}
}
impl Readable for EstIntroExtDoS {
fn take_from(b: &mut Reader<'_>) -> Result<Self> {
impl Ext for DosParams {
type Id = EstIntroExtType;
fn type_id(&self) -> EstIntroExtType {
EstIntroExtType::DOS_PARAMS
}
fn take_body_from(b: &mut Reader<'_>) -> Result<Self> {
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<B: Writer + ?Sized>(&self, b: &mut B) -> EncodeResult<()> {
fn write_body_onto<B: Writer + ?Sized>(&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<u8>,
/// 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<EstIntroExtDoS>,
/// A list of extensions on this cell.
extensions: ExtList<EstablishIntroExt>,
/// 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<u8>)> = 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<EstIntroExtType>) {
self.extensions.replace_by_type(other.into());
}
// TODO hs: we'll need accessors.

View File

@ -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<T> {
/// The extensions themselves.
extensions: Vec<T>,
}
impl<T> Default for ExtList<T> {
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<u8> + Into<u8> + 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<u8> + Into<u8>;
/// 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<Self>;
/// Write the body (not the type or the length) for a single extension.
fn write_body_onto<B: Writer + ?Sized>(&self, b: &mut B) -> EncodeResult<()>;
}
impl<T: ExtGroup> Readable for ExtList<T> {
fn take_from(b: &mut Reader<'_>) -> Result<Self> {
let n_extensions = b.take_u8()?;
let extensions: Result<Vec<T>> = (0..n_extensions).map(|_| b.extract::<T>()).collect();
Ok(Self {
extensions: extensions?,
})
}
}
impl<T: ExtGroup> Writeable for ExtList<T> {
fn write_onto<B: Writer + ?Sized>(&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<T: ExtGroup> ExtList<T> {
/// 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<ID> {
/// The field type ID for this extension.
pub(super) type_id: ID,
/// The body of this extension.
pub(super) body: Vec<u8>,
}
impl<ID> UnrecognizedExt<ID> {
/// 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<Vec<u8>>) -> 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<Self> {
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<B: Writer + ?Sized>(&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<UnrecognizedExt<$type_id>> for $id {
fn from(val: UnrecognizedExt<$type_id>) -> $id {
$id :: Unrecognized(val)
}
}
}}
}
pub(super) use decl_extension_group;

View File

@ -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::<u8>::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")]