diff --git a/crates/tor-cell/semver.md b/crates/tor-cell/semver.md new file mode 100644 index 000000000..0727a621b --- /dev/null +++ b/crates/tor-cell/semver.md @@ -0,0 +1 @@ +ADDED: New type for payload of an ht-ntor handshake. diff --git a/crates/tor-cell/src/relaycell/hs.rs b/crates/tor-cell/src/relaycell/hs.rs index d2dcef968..f8c7a0ec3 100644 --- a/crates/tor-cell/src/relaycell/hs.rs +++ b/crates/tor-cell/src/relaycell/hs.rs @@ -15,6 +15,7 @@ use tor_llcrypto::pk::rsa::RsaIdentity; pub mod est_intro; mod ext; +pub mod intro_payload; pub use ext::UnrecognizedExt; diff --git a/crates/tor-cell/src/relaycell/hs/intro_payload.rs b/crates/tor-cell/src/relaycell/hs/intro_payload.rs new file mode 100644 index 000000000..62f47d4a3 --- /dev/null +++ b/crates/tor-cell/src/relaycell/hs/intro_payload.rs @@ -0,0 +1,142 @@ +//! Implementation for the encrypted portion of an INTRODUCE message. +//! +//! TODO HS: maybe rename this module. +//! +//! TODO HS: Maybe this doesn't belong in tor-cell. + +use super::ext::{decl_extension_group, ExtGroup, ExtList, UnrecognizedExt}; +use caret::caret_int; +use tor_bytes::{EncodeError, EncodeResult, Error, Readable, Reader, Result, Writeable, Writer}; +use tor_hscrypto::RendCookie; +use tor_linkspec::UnparsedLinkSpec; + +caret_int! { + /// Type code for an extension in an [`IntroduceHandshakePayload`]. + #[derive(Ord,PartialOrd)] + pub struct IntroPayloadExtType(u8) { + } +} + +decl_extension_group! { + /// An extension to an [`IntroduceHandshakePayload`]. + /// + /// (Currently, no extensions of this type are recognized) + #[derive(Debug,Clone)] + enum IntroPayloadExt [ IntroPayloadExtType ] { + } +} + +caret_int! { + /// An enumeration value to identify a type of onion key. + struct OnionKeyType(u8) { + NTOR = 0x01, + } +} + +/// An onion key provided in an IntroduceHandshakePayload. +/// +/// TODO HS: Is there a logical type somewhere else to coalesce this with? +#[derive(Clone, Debug)] +#[non_exhaustive] +pub enum OnionKey { + /// A key usable with the ntor or ntor-v3 handshake. + NtorOnionKey(tor_llcrypto::pk::curve25519::PublicKey), + // There is no "unknown" variant for this type, since we don't support any + // other key types yet. +} + +impl Readable for OnionKey { + fn take_from(r: &mut Reader<'_>) -> Result { + let kind: OnionKeyType = r.take_u8()?.into(); + r.read_nested_u16len(|r_inner| match kind { + OnionKeyType::NTOR => Ok(OnionKey::NtorOnionKey(r_inner.extract()?)), + _ => Err(Error::InvalidMessage( + format!("Unrecognized onion key type {kind}").into(), + )), + }) + } +} + +impl Writeable for OnionKey { + fn write_onto(&self, w: &mut B) -> EncodeResult<()> { + match self { + OnionKey::NtorOnionKey(key) => { + w.write_u8(OnionKeyType::NTOR.into()); + let mut w_inner = w.write_nested_u16len(); + w_inner.write(key)?; + w_inner.finish()?; + } + } + Ok(()) + } +} + +/// The plaintext of the encrypted portion of an INTRODUCE message. +/// +/// This is not a RelayMsg itself; it is instead used as the payload for an +/// `hs-ntor` handshake, which is passed to the onion service in `Introduce[12]` +/// message. +/// +/// This payload is sent from a client to the onion service to tell it how to reach +/// the client's chosen rendezvous point. +#[derive(Clone, Debug)] +pub struct IntroduceHandshakePayload { + /// The rendezvous cookie to use at the rendezvous point. + cookie: RendCookie, + /// A list of extensions to this payload + extensions: ExtList, + /// The onion key to use when extending a circuit to the rendezvous point. + onion_key: OnionKey, + /// A list of link specifiers to identify the rendezvous point. + link_specifiers: Vec, +} + +impl Readable for IntroduceHandshakePayload { + fn take_from(r: &mut Reader<'_>) -> Result { + let cookie = r.extract()?; + let extensions = r.extract()?; + let onion_key = r.extract()?; + let n_link_specifiers = r.take_u8()?; + let link_specifiers = r.extract_n(n_link_specifiers.into())?; + Ok(Self { + cookie, + extensions, + onion_key, + link_specifiers, + }) + } +} + +impl Writeable for IntroduceHandshakePayload { + fn write_onto(&self, w: &mut B) -> EncodeResult<()> { + w.write(&self.cookie)?; + w.write(&self.extensions)?; + w.write(&self.onion_key)?; + w.write_u8( + self.link_specifiers + .len() + .try_into() + .map_err(|_| EncodeError::BadLengthValue)?, + ); + self.link_specifiers.iter().try_for_each(|ls| w.write(ls))?; + + Ok(()) + } +} + +impl IntroduceHandshakePayload { + /// Construct a new [`IntroduceHandshakePayload`] + pub fn new( + cookie: RendCookie, + onion_key: OnionKey, + link_specifiers: Vec, + ) -> Self { + let extensions = ExtList::default(); + Self { + cookie, + extensions, + onion_key, + link_specifiers, + } + } +} diff --git a/crates/tor-cell/tests/testvec_relaymsg.rs b/crates/tor-cell/tests/testvec_relaymsg.rs index 9dc37beeb..e5cbe712b 100644 --- a/crates/tor-cell/tests/testvec_relaymsg.rs +++ b/crates/tor-cell/tests/testvec_relaymsg.rs @@ -857,3 +857,51 @@ fn test_intro_established() { let intro_est = IntroEstablished::new(); msg(cmd, "00", &intro_est.into()) } + +#[cfg(feature = "hs")] +#[test] +fn testvec_intro_payload() { + use tor_bytes::{Reader, Writer}; + use tor_cell::relaycell::hs::intro_payload::*; + use tor_linkspec::UnparsedLinkSpec; + + let cookie = hex!("1EFFEACE9BE629B357ADA359071A7912DB828A5B").into(); + let onion_key = OnionKey::NtorOnionKey( + hex!("7D73D007977A08CD1ABAD50F6B836C718700D687E000728C357ABC7CE3C8334D") + .try_into() + .unwrap(), + ); + let link_specifiers = vec![ + UnparsedLinkSpec::new(0, hex!("7F000001138A")), + UnparsedLinkSpec::new(2, hex!("E48664DBCCEF9650B5D0E7B60E6DE9BCED2FB91E")), + UnparsedLinkSpec::new( + 3, + hex!("3FF84AA4B21453D20106BD4EDDA919386BF67D541CAA78F38BE6A08C2B3D0C4F"), + ), + ]; + let expected = IntroduceHandshakePayload::new(cookie, onion_key, link_specifiers); + + // Taken from a modified Tor client on a chutney network. + let encoded = hex!( + "1EFFEACE9BE629B357ADA359071A7912DB828A5B + 00 + 01 0020 7D73D007977A08CD1ABAD50F6B836C718700D687E000728C357ABC7CE3C8334D + 03 00 06 7F000001138A + 02 14 E48664DBCCEF9650B5D0E7B60E6DE9BCED2FB91E + 03 20 3FF84AA4B21453D20106BD4EDDA919386BF67D541CAA78F38BE6A08C2B3D0C4F + 000000000000000000000000" + ); + + let padding_len = { + let mut r = Reader::from_slice(&encoded[..]); + let got: IntroduceHandshakePayload = r.extract().unwrap(); + assert_eq!(format!("{got:?}"), format!("{expected:?}")); + r.remaining() + }; + + let mut v = Vec::new(); + v.write(&expected).unwrap(); + // There is padding so we can't do "eq" directly. + assert_eq!(&v[..], &encoded[..v.len()]); + assert_eq!(v.len(), encoded.len() - padding_len); +}