Merge branch 'establish_intro_v2' into 'main'
Implement circuit binding and start on intro-point establisher logic Closes #953 and #993 See merge request tpo/core/arti!1472
This commit is contained in:
commit
118ed81d82
|
@ -4653,6 +4653,7 @@ dependencies = [
|
|||
"safelog",
|
||||
"serde",
|
||||
"signature 1.6.4",
|
||||
"subtle",
|
||||
"thiserror",
|
||||
"tor-basic-utils",
|
||||
"tor-bytes",
|
||||
|
@ -4683,7 +4684,9 @@ dependencies = [
|
|||
"futures",
|
||||
"rand_core 0.6.4",
|
||||
"thiserror",
|
||||
"tor-cell",
|
||||
"tor-circmgr",
|
||||
"tor-error",
|
||||
"tor-hscrypto",
|
||||
"tor-keymgr",
|
||||
"tor-linkspec",
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
ADDED: establish_intro functions now take any impl<Into<HsMacKey>>.
|
|
@ -3,9 +3,10 @@
|
|||
use caret::caret_int;
|
||||
use tor_bytes::{EncodeError, EncodeResult, Readable, Reader, Result, Writeable, Writer};
|
||||
use tor_error::bad_api_usage;
|
||||
use tor_hscrypto::ops::{hs_mac, HS_MAC_LEN};
|
||||
use tor_hscrypto::ops::{HsMacKey, HS_MAC_LEN};
|
||||
use tor_llcrypto::{
|
||||
pk::ed25519::{self, Ed25519Identity, ED25519_SIGNATURE_LEN},
|
||||
traits::ShortMac as _,
|
||||
util::ct::CtByteArray,
|
||||
};
|
||||
use tor_units::BoundedInt32;
|
||||
|
@ -279,10 +280,10 @@ impl EstablishIntroDetails {
|
|||
/// The MAC key is derived from the circuit handshake between the onion
|
||||
/// service and the introduction point. The Ed25519 keypair must match the
|
||||
/// one given as the auth_key for this body.
|
||||
pub fn sign_and_encode(
|
||||
pub fn sign_and_encode<'a>(
|
||||
self,
|
||||
keypair: &ed25519::Keypair,
|
||||
mac_key: &[u8],
|
||||
mac_key: impl Into<HsMacKey<'a>>,
|
||||
) -> crate::Result<Vec<u8>> {
|
||||
use tor_llcrypto::pk::ed25519::Signer;
|
||||
if Ed25519Identity::from(&keypair.public) != self.auth_key {
|
||||
|
@ -292,7 +293,8 @@ impl EstablishIntroDetails {
|
|||
let mut output = Vec::new();
|
||||
|
||||
output.write(&self)?;
|
||||
let mac = hs_mac(mac_key, &output[..]);
|
||||
let mac_key: HsMacKey<'_> = mac_key.into();
|
||||
let mac = mac_key.mac(&output[..]);
|
||||
output.write(&mac)?;
|
||||
let signature = {
|
||||
let mut signed_material = Vec::from(SIG_PREFIX);
|
||||
|
@ -346,20 +348,17 @@ impl EstablishIntro {
|
|||
///
|
||||
/// On success, return the [`EstablishIntroDetails`] describing how to function
|
||||
/// as an introduction point for this service. On failure, return an error.
|
||||
pub fn check_and_unwrap(
|
||||
pub fn check_and_unwrap<'a>(
|
||||
self,
|
||||
mac_key: &[u8],
|
||||
mac_key: impl Into<HsMacKey<'a>>,
|
||||
) -> std::result::Result<EstablishIntroDetails, EstablishIntroSigError> {
|
||||
use tor_llcrypto::pk::ValidatableSignature;
|
||||
// There is a timing side-channel here where, if an attacker wants, they
|
||||
// could tell which of the two fields was incorrect. But that shouldn't
|
||||
// be exploitable for anything.
|
||||
//
|
||||
// TODO use subtle here anyway, perhaps?
|
||||
if hs_mac(mac_key, &self.mac_plaintext) != self.handshake_auth {
|
||||
return Err(EstablishIntroSigError::Invalid);
|
||||
}
|
||||
if !self.sig.is_valid() {
|
||||
|
||||
let mac_key: HsMacKey<'_> = mac_key.into();
|
||||
let mac_okay = mac_key.validate(&self.mac_plaintext, &self.handshake_auth);
|
||||
let sig_okay = self.sig.is_valid();
|
||||
|
||||
if !(bool::from(mac_okay) & sig_okay) {
|
||||
return Err(EstablishIntroSigError::Invalid);
|
||||
}
|
||||
|
||||
|
|
|
@ -13,7 +13,14 @@ repository = "https://gitlab.torproject.org/tpo/core/arti.git/"
|
|||
|
||||
[features]
|
||||
default = []
|
||||
full = ["safelog/full", "tor-basic-utils/full", "tor-bytes/full", "tor-llcrypto/full", "tor-units/full", "tor-error/full"]
|
||||
full = [
|
||||
"safelog/full",
|
||||
"tor-basic-utils/full",
|
||||
"tor-bytes/full",
|
||||
"tor-llcrypto/full",
|
||||
"tor-units/full",
|
||||
"tor-error/full",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
data-encoding = "2.3.1" # want MSVC i686 build fix, data-encoding/issues/33
|
||||
|
@ -26,6 +33,7 @@ rand_core = "0.6.2"
|
|||
safelog = { path = "../safelog", version = "0.3.2" }
|
||||
serde = { version = "1.0.103", features = ["derive"] }
|
||||
signature = "1"
|
||||
subtle = "2"
|
||||
thiserror = "1"
|
||||
tor-basic-utils = { path = "../tor-basic-utils", version = "0.7.3" }
|
||||
tor-bytes = { version = "0.7.2", path = "../tor-bytes" }
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
ADDED: HsMacKey type.
|
|
@ -1,5 +1,4 @@
|
|||
//! Mid-level cryptographic operations used in the onion service protocol.
|
||||
|
||||
use tor_llcrypto::d::Sha3_256;
|
||||
use tor_llcrypto::util::ct::CtByteArray;
|
||||
|
||||
|
@ -27,6 +26,22 @@ pub fn hs_mac(key: &[u8], msg: &[u8]) -> CtByteArray<HS_MAC_LEN> {
|
|||
a.into()
|
||||
}
|
||||
|
||||
/// Reference to a slice of bytes usable to compute the [`hs_mac`] function.
|
||||
#[derive(Copy, Clone, derive_more::From)]
|
||||
pub struct HsMacKey<'a>(&'a [u8]);
|
||||
|
||||
impl<'a> tor_llcrypto::traits::ShortMac<HS_MAC_LEN> for HsMacKey<'a> {
|
||||
fn mac(&self, input: &[u8]) -> CtByteArray<HS_MAC_LEN> {
|
||||
hs_mac(self.0, input)
|
||||
}
|
||||
|
||||
fn validate(&self, input: &[u8], mac: &[u8; HS_MAC_LEN]) -> subtle::Choice {
|
||||
use subtle::ConstantTimeEq;
|
||||
let m = hs_mac(self.0, input);
|
||||
m.as_ref().ct_eq(mac)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
// @@ begin test lint list maintained by maint/add_warning @@
|
||||
|
|
|
@ -29,13 +29,15 @@ async-trait = "0.1.54"
|
|||
futures = "0.3.14"
|
||||
rand_core = "0.6.2"
|
||||
thiserror = "1"
|
||||
tor-cell = { version = "0.12.1", path = "../tor-cell", features = ["hs"] }
|
||||
tor-circmgr = { version = "0.10.0", path = "../tor-circmgr", features = ["hs-service"] }
|
||||
tor-error = { version = "0.5.3", path = "../tor-error" }
|
||||
tor-hscrypto = { version = "0.3.0", path = "../tor-hscrypto" }
|
||||
tor-keymgr = { version = "0.2.0", path = "../tor-keymgr" }
|
||||
tor-linkspec = { version = "0.8.1", path = "../tor-linkspec" }
|
||||
tor-llcrypto = { version = "0.5.2", path = "../tor-llcrypto" }
|
||||
tor-netdir = { version = "0.9.3", path = "../tor-netdir" }
|
||||
tor-proto = { version = "0.12.0", path = "../tor-proto" }
|
||||
tor-proto = { version = "0.12.0", path = "../tor-proto", features = ["hs-service", "send-control-msg"] }
|
||||
tor-rtcompat = { version = "0.9.1", path = "../tor-rtcompat" }
|
||||
|
||||
[dev-dependencies]
|
||||
|
|
|
@ -8,9 +8,21 @@
|
|||
|
||||
use std::sync::Arc;
|
||||
|
||||
use futures::channel::mpsc;
|
||||
use futures::{
|
||||
channel::{mpsc, oneshot},
|
||||
StreamExt as _,
|
||||
};
|
||||
use tor_cell::relaycell::{
|
||||
hs::est_intro::EstablishIntroDetails,
|
||||
msg::{AnyRelayMsg, IntroEstablished, Introduce2},
|
||||
RelayMsg as _,
|
||||
};
|
||||
use tor_circmgr::hspool::HsCircPool;
|
||||
use tor_netdir::{NetDirProvider, Relay};
|
||||
use tor_error::{internal, into_internal};
|
||||
use tor_hscrypto::pk::HsIntroPtSessionIdKeypair;
|
||||
use tor_linkspec::CircTarget;
|
||||
use tor_netdir::{NetDir, NetDirProvider, Relay};
|
||||
use tor_proto::circuit::{ConversationInHandler, MetaCellDisposition};
|
||||
use tor_rtcompat::Runtime;
|
||||
|
||||
use crate::RendRequest;
|
||||
|
@ -36,12 +48,43 @@ impl Drop for IptEstablisher {
|
|||
}
|
||||
}
|
||||
|
||||
/// An error from trying to create in introduction point establisher.
|
||||
///
|
||||
/// TODO HSS: This is probably too narrow a definition; do something else
|
||||
/// instead.
|
||||
/// An error from trying to work with an IptEstablisher.
|
||||
#[derive(Clone, Debug, thiserror::Error)]
|
||||
pub(crate) enum IptError {}
|
||||
pub(crate) enum IptError {
|
||||
/// We couldn't get a network directory to use when building circuits.
|
||||
#[error("No network directory available")]
|
||||
NoNetdir(#[source] tor_netdir::Error),
|
||||
|
||||
/// The network directory provider is shutting down without giving us the
|
||||
/// netdir we asked for.
|
||||
#[error("Network directory provider is shutting down")]
|
||||
NetdirProviderShutdown,
|
||||
|
||||
/// We encountered an error while building a circuit to an intro point.
|
||||
#[error("Unable to build circuit to introduction point")]
|
||||
BuildCircuit(#[source] tor_circmgr::Error),
|
||||
|
||||
/// We encountered an error while building and signing our establish_intro
|
||||
/// message.
|
||||
#[error("Unable to construct signed ESTABLISH_INTRO message")]
|
||||
CreateEstablishIntro(#[source] tor_cell::Error),
|
||||
|
||||
/// We encountered an error while sending our establish_intro
|
||||
/// message.
|
||||
#[error("Unable to send an ESTABLISH_INTRO message")]
|
||||
SendEstablishIntro(#[source] tor_proto::Error),
|
||||
|
||||
/// We did not receive an INTRO_ESTABLISHED message like we wanted.
|
||||
#[error("Did not receive INTRO_ESTABLISHED message")]
|
||||
// TODO HSS: I'd like to receive more information here. What happened
|
||||
// instead? But the information might be in the MsgHandler, might be in the
|
||||
// Circuit,...
|
||||
ReceiveAck,
|
||||
|
||||
/// We encountered a programming error.
|
||||
#[error("Internal error")]
|
||||
Bug(#[from] tor_error::Bug),
|
||||
}
|
||||
|
||||
impl IptEstablisher {
|
||||
/// Try to set up, and maintain, an IPT at `Relay`
|
||||
|
@ -112,3 +155,197 @@ pub(crate) struct IptStatus {
|
|||
/// retired based on having processed too many requests.
|
||||
pub(crate) wants_to_retire: Result<(), IptWantsToRetire>,
|
||||
}
|
||||
|
||||
tor_cell::restricted_msg! {
|
||||
/// An acceptable message to receive from an introduction point.
|
||||
enum IptMsg : RelayMsg {
|
||||
IntroEstablished,
|
||||
Introduce2,
|
||||
}
|
||||
}
|
||||
|
||||
/// Try, once, to make a circuit to a single relay and establish an introduction
|
||||
/// point there.
|
||||
///
|
||||
/// Does not retry. Does not time out except via `HsCircPool`.
|
||||
async fn establish_intro_once<R, T>(
|
||||
pool: Arc<HsCircPool<R>>,
|
||||
netdir_provider: Arc<dyn NetDirProvider>,
|
||||
target: T,
|
||||
ipt_sid_keypair: &HsIntroPtSessionIdKeypair,
|
||||
// TODO HSS: Take other extensions. Ideally we would take not only
|
||||
// DoSParams, but a full IntoIterator<Item=EstablishIntroExt> or ExtList.
|
||||
// But both of those types are private in tor-cell. We should consider
|
||||
// whether that's what we want.
|
||||
) -> Result<(), IptError>
|
||||
where
|
||||
R: Runtime,
|
||||
T: CircTarget,
|
||||
{
|
||||
let circuit = {
|
||||
let netdir =
|
||||
wait_for_netdir(netdir_provider.as_ref(), tor_netdir::Timeliness::Timely).await?;
|
||||
let kind = tor_circmgr::hspool::HsCircKind::SvcIntro;
|
||||
pool.get_or_launch_specific(netdir.as_ref(), kind, target)
|
||||
.await
|
||||
.map_err(IptError::BuildCircuit)?
|
||||
// note that netdir is dropped here, to avoid holding on to it any
|
||||
// longer than necessary.
|
||||
};
|
||||
let intro_pt_hop = circuit
|
||||
.last_hop_num()
|
||||
.map_err(into_internal!("Somehow built a circuit with no hops!?"))?;
|
||||
|
||||
let establish_intro = {
|
||||
let ipt_sid_id = ipt_sid_keypair.as_ref().public.into();
|
||||
let details = EstablishIntroDetails::new(ipt_sid_id);
|
||||
let circuit_binding_key = circuit
|
||||
.binding_key(intro_pt_hop)
|
||||
.ok_or(internal!("No binding key for introduction point!?"))?;
|
||||
if false {
|
||||
// TODO HSS: Insert extensions as needed.
|
||||
}
|
||||
let body: Vec<u8> = details
|
||||
.sign_and_encode(ipt_sid_keypair.as_ref(), circuit_binding_key.hs_mac())
|
||||
.map_err(IptError::CreateEstablishIntro)?;
|
||||
|
||||
// TODO HSS: This is ugly, but it is the sensible way to munge the above
|
||||
// body into a format that AnyRelayCell will accept without doing a
|
||||
// redundant parse step.
|
||||
//
|
||||
// One alternative would be allowing start_conversation to take an `impl
|
||||
// RelayMsg` rather than an AnyRelayMsg.
|
||||
//
|
||||
// Or possibly, when we feel like it, we could rename one or more of
|
||||
// these "Unrecognized"s to Unparsed or Uninterpreted. If we do that, however, we'll
|
||||
// potentially face breaking changes up and down our crate stack.
|
||||
AnyRelayMsg::Unrecognized(tor_cell::relaycell::msg::Unrecognized::new(
|
||||
tor_cell::relaycell::RelayCmd::ESTABLISH_INTRO,
|
||||
body,
|
||||
))
|
||||
};
|
||||
|
||||
let (established_tx, established_rx) = oneshot::channel();
|
||||
let (introduce_tx, introduce_rx) = mpsc::unbounded();
|
||||
|
||||
let handler = IptMsgHandler {
|
||||
established_tx: Some(established_tx),
|
||||
introduce_tx,
|
||||
};
|
||||
let conversation = circuit
|
||||
.start_conversation(Some(establish_intro), handler, intro_pt_hop)
|
||||
.await
|
||||
.map_err(IptError::SendEstablishIntro)?;
|
||||
// At this point, we have `await`ed for the Conversation to exist, so we know
|
||||
// that the message was sent. We have to wait for any actual `established`
|
||||
// message, though.
|
||||
|
||||
let established = established_rx.await.map_err(|_| IptError::ReceiveAck)?;
|
||||
|
||||
// TODO HSS: handle all the extension data in the established field.
|
||||
|
||||
// TODO HSS: Return the introduce_rx stream along with any related types.
|
||||
// Or should we have taken introduce_tx as an argument? (@diziet endorses
|
||||
// the "take it as an argument" idea.)
|
||||
|
||||
// TODO HSS: Return the circuit too, of course.
|
||||
|
||||
// TODO HSS: How shall we know if the other side has closed the circuit? We could wait
|
||||
// for introduce_rx.next() to yield None, but that will only work if we use
|
||||
// one mpsc::Sender per circuit...
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Get a NetDir from `provider`, waiting until one exists.
|
||||
///
|
||||
/// TODO: perhaps this function would be more generally useful if it were not here?
|
||||
async fn wait_for_netdir(
|
||||
provider: &dyn NetDirProvider,
|
||||
timeliness: tor_netdir::Timeliness,
|
||||
) -> Result<Arc<NetDir>, IptError> {
|
||||
if let Ok(nd) = provider.netdir(timeliness) {
|
||||
return Ok(nd);
|
||||
}
|
||||
|
||||
let mut stream = provider.events();
|
||||
loop {
|
||||
// We need to retry `provider.netdir()` before waiting for any stream events, to
|
||||
// avoid deadlock.
|
||||
//
|
||||
// TODO HSS: propagate _some_ possible errors here.
|
||||
if let Ok(nd) = provider.netdir(timeliness) {
|
||||
return Ok(nd);
|
||||
}
|
||||
match stream.next().await {
|
||||
Some(_) => {}
|
||||
None => {
|
||||
return Err(IptError::NetdirProviderShutdown);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// MsgHandler type to implement a conversation with an introduction point.
|
||||
///
|
||||
/// This, like all MsgHandlers, is installed at the circuit's reactor, and used
|
||||
/// to handle otherwise unrecognized message types.
|
||||
#[derive(Debug)]
|
||||
struct IptMsgHandler {
|
||||
/// A oneshot sender used to report our IntroEstablished message.
|
||||
///
|
||||
/// If this is None, then we already sent an IntroEstablished and we shouldn't
|
||||
/// send any more.
|
||||
established_tx: Option<oneshot::Sender<IntroEstablished>>,
|
||||
/// A channel used to report Introduce2 messages.
|
||||
//
|
||||
// TODO HSS: I don't like having this be unbounded, but `handle_msg` can't
|
||||
// block. On the other hand maybe we can just discard excessive introduce2
|
||||
// messages if we're under high load? I think that's what C tor does,
|
||||
// especially when under DoS conditions.
|
||||
//
|
||||
// See discussion at
|
||||
// https://gitlab.torproject.org/tpo/core/arti/-/merge_requests/1465#note_2928349
|
||||
introduce_tx: mpsc::UnboundedSender<Introduce2>,
|
||||
}
|
||||
|
||||
impl tor_proto::circuit::MsgHandler for IptMsgHandler {
|
||||
fn handle_msg(
|
||||
&mut self,
|
||||
conversation: ConversationInHandler<'_, '_, '_>,
|
||||
any_msg: AnyRelayMsg,
|
||||
) -> tor_proto::Result<MetaCellDisposition> {
|
||||
// TODO HSS: Implement rate-limiting.
|
||||
//
|
||||
// TODO HSS: Is CircProto right or should this be a new error type?
|
||||
let msg: IptMsg = any_msg.try_into().map_err(|m: AnyRelayMsg| {
|
||||
tor_proto::Error::CircProto(format!("Invalid message type {}", m.cmd()))
|
||||
})?;
|
||||
|
||||
if match msg {
|
||||
IptMsg::IntroEstablished(established) => match self.established_tx.take() {
|
||||
Some(tx) => tx.send(established).map_err(|_| ()),
|
||||
None => {
|
||||
return Err(tor_proto::Error::CircProto(
|
||||
"Received a redundant INTRO_ESTABLISHED".into(),
|
||||
));
|
||||
}
|
||||
},
|
||||
IptMsg::Introduce2(introduce2) => {
|
||||
if self.established_tx.is_some() {
|
||||
return Err(tor_proto::Error::CircProto(
|
||||
"Received an INTRODUCE2 message before INTRO_ESTABLISHED".into(),
|
||||
));
|
||||
}
|
||||
self.introduce_tx.unbounded_send(introduce2).map_err(|_| ())
|
||||
}
|
||||
} == Err(())
|
||||
{
|
||||
// If the above return an error, we failed to send. That means that
|
||||
// we need to close the circuit, since nobody is listening on the
|
||||
// other end of the tx.
|
||||
return Ok(MetaCellDisposition::CloseCirc);
|
||||
}
|
||||
|
||||
Ok(MetaCellDisposition::Consumed)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
ADDED: traits module.
|
|
@ -43,4 +43,6 @@
|
|||
pub mod cipher;
|
||||
pub mod d;
|
||||
pub mod pk;
|
||||
#[cfg(feature = "hsv3-service")] // TODO HSS: Remove this feature gate
|
||||
pub mod traits;
|
||||
pub mod util;
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
//! Cryptographic traits for general use throughout Arti.
|
||||
|
||||
use subtle::Choice;
|
||||
|
||||
/// A simple trait to describe a keyed message authentication code.
|
||||
///
|
||||
/// Unlike RustCrypto's
|
||||
/// [`crypto_mac::Mac`](https://docs.rs/crypto-mac/latest/crypto_mac/trait.Mac.html),
|
||||
/// this trait does not support incremental processing.
|
||||
pub trait ShortMac<const MAC_LEN: usize> {
|
||||
/// Calculate a message authentication code for `input` using this key.
|
||||
fn mac(&self, input: &[u8]) -> crate::util::ct::CtByteArray<MAC_LEN>;
|
||||
|
||||
/// Check whether `mac` is a valid message authentication code for `input`
|
||||
/// using this key.
|
||||
fn validate(&self, input: &[u8], mac: &[u8; MAC_LEN]) -> Choice;
|
||||
}
|
|
@ -14,3 +14,6 @@ ADDED: `ClientCirc::start_conversation()` to eventually replace
|
|||
BREAKING: `ClientCirc::allow_stream_requests` is now async
|
||||
BREAKING: `IncomingStream::discard` now takes `mut self` instead of `self` and
|
||||
returns a `Result<(), Bug>`
|
||||
ADDED: `ClientCirc::binding_key`
|
||||
|
||||
|
||||
|
|
|
@ -57,12 +57,14 @@ use crate::circuit::reactor::{
|
|||
CircuitHandshake, CtrlMsg, Reactor, RECV_WINDOW_INIT, STREAM_READER_BUFFER,
|
||||
};
|
||||
pub use crate::circuit::unique_id::UniqId;
|
||||
pub use crate::crypto::binding::CircuitBinding;
|
||||
use crate::crypto::cell::HopNum;
|
||||
use crate::stream::{
|
||||
AnyCmdChecker, DataCmdChecker, DataStream, ResolveCmdChecker, ResolveStream, StreamParameters,
|
||||
StreamReader,
|
||||
};
|
||||
use crate::{Error, ResolveError, Result};
|
||||
use educe::Educe;
|
||||
use tor_cell::{
|
||||
chancell::{self, msg::AnyChanMsg, CircId},
|
||||
relaycell::msg::{AnyRelayMsg, Begin, Resolve, Resolved, ResolvedVal},
|
||||
|
@ -171,7 +173,8 @@ pub struct ClientCirc {
|
|||
}
|
||||
|
||||
/// Mutable state shared by [`ClientCirc`] and [`Reactor`].
|
||||
#[derive(Debug)]
|
||||
#[derive(Educe)]
|
||||
#[educe(Debug)]
|
||||
struct MutableState {
|
||||
/// Information about this circuit's path.
|
||||
///
|
||||
|
@ -179,6 +182,16 @@ struct MutableState {
|
|||
/// client code; when we need to add a hop (which is less frequent) we use
|
||||
/// [`Arc::make_mut()`].
|
||||
path: Arc<path::Path>,
|
||||
|
||||
/// Circuit binding keys [q.v.][`CircuitBinding`] information for each hop
|
||||
/// in the circuit's path.
|
||||
///
|
||||
/// NOTE: Right now, there is a `CircuitBinding` for every hop. There's a
|
||||
/// fair chance that this will change in the future, and I don't want other
|
||||
/// code to assume that a `CircuitBinding` _must_ exist, so I'm making this
|
||||
/// an `Option`.
|
||||
#[educe(Debug(ignore))]
|
||||
binding: Vec<Option<CircuitBinding>>,
|
||||
}
|
||||
|
||||
/// A ClientCirc that needs to send a create cell and receive a created* cell.
|
||||
|
@ -342,6 +355,25 @@ impl ClientCirc {
|
|||
&self.channel
|
||||
}
|
||||
|
||||
/// Return the cryptographic material used to prove knowledge of a shared
|
||||
/// secret with with `hop`.
|
||||
///
|
||||
/// See [`CircuitBinding`] for more information on how this is used.
|
||||
///
|
||||
/// Return None if we have no circuit binding information for the hop, or if
|
||||
/// the hop does not exist.
|
||||
pub fn binding_key(&self, hop: HopNum) -> Option<CircuitBinding> {
|
||||
self.mutable
|
||||
.lock()
|
||||
.expect("poisoned lock")
|
||||
.binding
|
||||
.get::<usize>(hop.into())
|
||||
.cloned()
|
||||
.flatten()
|
||||
// NOTE: I'm not thrilled to have to copy this information, but we use
|
||||
// it very rarely, so it's not _that_ bad IMO.
|
||||
}
|
||||
|
||||
/// Start an ad-hoc protocol exchange to the specified hop on this circuit
|
||||
///
|
||||
/// To use this:
|
||||
|
@ -591,11 +623,13 @@ impl ClientCirc {
|
|||
seed: impl handshake::KeyGenerator,
|
||||
params: CircParameters,
|
||||
) -> Result<()> {
|
||||
let (outbound, inbound) = protocol.construct_layers(role, seed)?;
|
||||
use self::handshake::BoxedClientLayer;
|
||||
|
||||
let BoxedClientLayer { fwd, back, binding } = protocol.construct_layers(role, seed)?;
|
||||
|
||||
let (tx, rx) = oneshot::channel();
|
||||
let message = CtrlMsg::ExtendVirtual {
|
||||
cell_crypto: (outbound, inbound),
|
||||
cell_crypto: (fwd, back, binding),
|
||||
params,
|
||||
done: tx,
|
||||
};
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
// that can wait IMO until we have a second circuit creation mechanism for use
|
||||
// with onion services.
|
||||
|
||||
use crate::crypto::binding::CircuitBinding;
|
||||
use crate::crypto::cell::{
|
||||
ClientLayer, CryptInit, InboundClientLayer, OutboundClientLayer, Tor1Hsv3RelayCrypto,
|
||||
};
|
||||
|
@ -41,6 +42,17 @@ pub enum HandshakeRole {
|
|||
Responder,
|
||||
}
|
||||
|
||||
/// A set of type-erased cryptographic layers to use for a single hop at a
|
||||
/// client.
|
||||
pub(crate) struct BoxedClientLayer {
|
||||
/// The outbound cryptographic layer to use for this hop
|
||||
pub(crate) fwd: Box<dyn OutboundClientLayer + Send>,
|
||||
/// The inbound cryptogarphic layer to use for this hop
|
||||
pub(crate) back: Box<dyn InboundClientLayer + Send>,
|
||||
/// A circuit binding key for this hop.
|
||||
pub(crate) binding: Option<CircuitBinding>,
|
||||
}
|
||||
|
||||
impl RelayProtocol {
|
||||
/// Construct the cell-crypto layers that are needed for a given set of
|
||||
/// circuit hop parameters.
|
||||
|
@ -48,21 +60,22 @@ impl RelayProtocol {
|
|||
self,
|
||||
role: HandshakeRole,
|
||||
keygen: impl KeyGenerator,
|
||||
) -> Result<(
|
||||
Box<dyn OutboundClientLayer + Send>,
|
||||
Box<dyn InboundClientLayer + Send>,
|
||||
)> {
|
||||
) -> Result<BoxedClientLayer> {
|
||||
match self {
|
||||
RelayProtocol::HsV3 => {
|
||||
let seed_needed = Tor1Hsv3RelayCrypto::seed_len();
|
||||
let seed = keygen.expand(seed_needed)?;
|
||||
let layer = Tor1Hsv3RelayCrypto::initialize(&seed)?;
|
||||
let (fwd, back) = layer.split();
|
||||
let (fwd, back, binding) = layer.split();
|
||||
let (fwd, back) = match role {
|
||||
HandshakeRole::Initiator => (fwd, back),
|
||||
HandshakeRole::Responder => (back, fwd),
|
||||
};
|
||||
Ok((Box::new(fwd), Box::new(back)))
|
||||
Ok(BoxedClientLayer {
|
||||
fwd: Box::new(fwd),
|
||||
back: Box::new(back),
|
||||
binding: Some(binding),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ use crate::circuit::unique_id::UniqId;
|
|||
use crate::circuit::{
|
||||
sendme, streammap, CircParameters, Create2Wrap, CreateFastWrap, CreateHandshakeWrap,
|
||||
};
|
||||
use crate::crypto::binding::CircuitBinding;
|
||||
use crate::crypto::cell::{
|
||||
ClientLayer, CryptInit, HopNum, InboundClientCrypt, InboundClientLayer, OutboundClientCrypt,
|
||||
OutboundClientLayer, RelayCellBody, Tor1RelayCrypto,
|
||||
|
@ -134,6 +135,7 @@ pub(super) enum CtrlMsg {
|
|||
cell_crypto: (
|
||||
Box<dyn OutboundClientLayer + Send>,
|
||||
Box<dyn InboundClientLayer + Send>,
|
||||
Option<CircuitBinding>,
|
||||
),
|
||||
/// A set of parameters used to configure this hop.
|
||||
params: CircParameters,
|
||||
|
@ -490,11 +492,12 @@ where
|
|||
debug!("{}: Handshake complete; circuit extended.", self.unique_id);
|
||||
|
||||
// If we get here, it succeeded. Add a new hop to the circuit.
|
||||
let (layer_fwd, layer_back) = layer.split();
|
||||
let (layer_fwd, layer_back, binding) = layer.split();
|
||||
reactor.add_hop(
|
||||
path::HopDetail::Relay(self.peer_id.clone()),
|
||||
Box::new(layer_fwd),
|
||||
Box::new(layer_back),
|
||||
Some(binding),
|
||||
&self.params,
|
||||
);
|
||||
Ok(MetaCellDisposition::ConversationFinished)
|
||||
|
@ -640,7 +643,8 @@ impl Reactor {
|
|||
let crypto_out = OutboundClientCrypt::new();
|
||||
let (control_tx, control_rx) = mpsc::unbounded();
|
||||
let path = Arc::new(path::Path::default());
|
||||
let mutable = Arc::new(Mutex::new(MutableState { path }));
|
||||
let binding = Vec::new();
|
||||
let mutable = Arc::new(Mutex::new(MutableState { path, binding }));
|
||||
|
||||
let (reactor_closed_tx, reactor_closed_rx) = oneshot::channel();
|
||||
|
||||
|
@ -937,7 +941,14 @@ impl Reactor {
|
|||
|
||||
let fwd = Box::new(DummyCrypto::new(fwd_lasthop));
|
||||
let rev = Box::new(DummyCrypto::new(rev_lasthop));
|
||||
self.add_hop(path::HopDetail::Relay(dummy_peer_id), fwd, rev, params);
|
||||
let binding = None;
|
||||
self.add_hop(
|
||||
path::HopDetail::Relay(dummy_peer_id),
|
||||
fwd,
|
||||
rev,
|
||||
binding,
|
||||
params,
|
||||
);
|
||||
let _ = done.send(Ok(()));
|
||||
}
|
||||
|
||||
|
@ -991,13 +1002,14 @@ impl Reactor {
|
|||
|
||||
debug!("{}: Handshake complete; circuit created.", self.unique_id);
|
||||
|
||||
let (layer_fwd, layer_back) = layer.split();
|
||||
let (layer_fwd, layer_back, binding) = layer.split();
|
||||
let peer_id = self.channel.target().clone();
|
||||
|
||||
self.add_hop(
|
||||
path::HopDetail::Relay(peer_id),
|
||||
Box::new(layer_fwd),
|
||||
Box::new(layer_back),
|
||||
Some(binding),
|
||||
params,
|
||||
);
|
||||
Ok(())
|
||||
|
@ -1062,6 +1074,7 @@ impl Reactor {
|
|||
peer_id: path::HopDetail,
|
||||
fwd: Box<dyn OutboundClientLayer + 'static + Send>,
|
||||
rev: Box<dyn InboundClientLayer + 'static + Send>,
|
||||
binding: Option<CircuitBinding>,
|
||||
params: &CircParameters,
|
||||
) {
|
||||
let hop = crate::circuit::reactor::CircHop::new(params.initial_send_window());
|
||||
|
@ -1070,6 +1083,7 @@ impl Reactor {
|
|||
self.crypto_out.add_layer(fwd);
|
||||
let mut mutable = self.mutable.lock().expect("poisoned lock");
|
||||
Arc::make_mut(&mut mutable.path).push_hop(peer_id);
|
||||
mutable.binding.push(binding);
|
||||
}
|
||||
|
||||
/// Handle a RELAY cell on this circuit with stream ID 0.
|
||||
|
@ -1382,13 +1396,13 @@ impl Reactor {
|
|||
params,
|
||||
done,
|
||||
} => {
|
||||
let (outbound, inbound) = cell_crypto;
|
||||
let (outbound, inbound, binding) = cell_crypto;
|
||||
|
||||
// TODO HS: Perhaps this should describe the onion service, or
|
||||
// describe why the virtual hop was added, or something?
|
||||
let peer_id = path::HopDetail::Virtual;
|
||||
|
||||
self.add_hop(peer_id, outbound, inbound, ¶ms);
|
||||
self.add_hop(peer_id, outbound, inbound, binding, ¶ms);
|
||||
let _ = done.send(Ok(()));
|
||||
}
|
||||
CtrlMsg::BeginStream {
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
//! * `handshake` implements the ntor handshake.
|
||||
//! * `ll` provides building blocks for other parts of the protocol.
|
||||
|
||||
pub(crate) mod binding;
|
||||
pub(crate) mod cell;
|
||||
pub(crate) mod handshake;
|
||||
pub(crate) mod ll;
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
//! Types related to binding messages to specific circuits
|
||||
|
||||
#[cfg(feature = "hs-service")]
|
||||
use tor_hscrypto::ops::HsMacKey;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
/// Number of bytes of circuit binding material negotiated per circuit hop.
|
||||
pub(crate) const CIRC_BINDING_LEN: usize = 20;
|
||||
|
||||
/// Cryptographic information used to bind a message to a specific circuit.
|
||||
///
|
||||
/// This information is used in some of our protocols (currently only the onion
|
||||
/// services protocol) to prove that a given message was referring to a specific
|
||||
/// hop on a specific circuit, and was not replayed from another circuit.
|
||||
///
|
||||
/// In `tor-spec` and `rend-spec`, this value is called `KH`.
|
||||
#[derive(Clone)]
|
||||
pub struct CircuitBinding(
|
||||
// We use a Box here to avoid moves that would bypass the zeroize-on-drop
|
||||
// semantics.
|
||||
//
|
||||
// (This is not super-critical, since the impact of leaking one of these
|
||||
// keys is slight, but it's best not to leak them at all.)
|
||||
Box<Zeroizing<[u8; CIRC_BINDING_LEN]>>,
|
||||
);
|
||||
|
||||
impl From<[u8; CIRC_BINDING_LEN]> for CircuitBinding {
|
||||
fn from(value: [u8; CIRC_BINDING_LEN]) -> Self {
|
||||
Self(Box::new(Zeroizing::new(value)))
|
||||
}
|
||||
}
|
||||
|
||||
impl CircuitBinding {
|
||||
/// Return a view of this key suitable for computing the MAC function used
|
||||
/// to authenticate onion services' ESTABLISH_INTRODUCE messages.
|
||||
///
|
||||
/// Note that this is not a general-purpose MAC; please avoid adding new
|
||||
/// users of it. See notes on [`hs_mac`](tor_hscrypto::ops::hs_mac) for
|
||||
/// more information.
|
||||
#[cfg(feature = "hs-service")]
|
||||
pub fn hs_mac(&self) -> HsMacKey<'_> {
|
||||
HsMacKey::from(self.dangerously_into_bytes())
|
||||
}
|
||||
|
||||
/// Return a view of this key as a byte-slice.
|
||||
///
|
||||
/// This is potentially dangerous, since we don't want to expose this
|
||||
/// information: We only want to use it as a MAC key.
|
||||
fn dangerously_into_bytes(&self) -> &[u8] {
|
||||
&(**self.0)[..]
|
||||
}
|
||||
}
|
|
@ -36,6 +36,8 @@ use tor_error::internal;
|
|||
|
||||
use generic_array::GenericArray;
|
||||
|
||||
use super::binding::CircuitBinding;
|
||||
|
||||
/// Type for the body of a relay cell.
|
||||
#[derive(Clone, derive_more::From, derive_more::Into)]
|
||||
pub(crate) struct RelayCellBody(BoxedCellBody);
|
||||
|
@ -75,8 +77,8 @@ where
|
|||
B: InboundClientLayer,
|
||||
{
|
||||
/// Consume this ClientLayer and return a paired forward and reverse
|
||||
/// crypto layer.
|
||||
fn split(self) -> (F, B);
|
||||
/// crypto layer, and a [`CircuitBinding`] object
|
||||
fn split(self) -> (F, B, CircuitBinding);
|
||||
}
|
||||
|
||||
/// Represents a relay's view of the crypto state on a given circuit.
|
||||
|
@ -259,6 +261,8 @@ pub(crate) type Tor1Hsv3RelayCrypto =
|
|||
/// I am calling this design `tor1`; it does not have a generally recognized
|
||||
/// name.
|
||||
pub(crate) mod tor1 {
|
||||
use crate::crypto::binding::CIRC_BINDING_LEN;
|
||||
|
||||
use super::*;
|
||||
use cipher::{KeyIvInit, StreamCipher};
|
||||
use digest::Digest;
|
||||
|
@ -298,11 +302,13 @@ pub(crate) mod tor1 {
|
|||
fwd: CryptState<SC, D>,
|
||||
/// State for en/decrypting cells sent towards the client.
|
||||
back: CryptState<SC, D>,
|
||||
/// A circuit binding key.
|
||||
binding: CircuitBinding,
|
||||
}
|
||||
|
||||
impl<SC: StreamCipher + KeyIvInit, D: Digest + Clone> CryptInit for CryptStatePair<SC, D> {
|
||||
fn seed_len() -> usize {
|
||||
SC::KeySize::to_usize() * 2 + D::OutputSize::to_usize() * 2
|
||||
SC::KeySize::to_usize() * 2 + D::OutputSize::to_usize() * 2 + CIRC_BINDING_LEN
|
||||
}
|
||||
fn initialize(seed: &[u8]) -> Result<Self> {
|
||||
// This corresponds to the use of the KDF algorithm as described in
|
||||
|
@ -319,6 +325,10 @@ pub(crate) mod tor1 {
|
|||
let bdinit = &seed[dlen..dlen * 2]; // This is Db in the spec.
|
||||
let fckey = &seed[dlen * 2..dlen * 2 + keylen]; // This is Kf in the spec.
|
||||
let bckey = &seed[dlen * 2 + keylen..dlen * 2 + keylen * 2]; // this is Kb in the spec.
|
||||
let binding_key: &[u8; CIRC_BINDING_LEN] = &seed
|
||||
[dlen * 2 + keylen * 2..dlen * 2 + keylen * 2 + CIRC_BINDING_LEN]
|
||||
.try_into()
|
||||
.expect("Unable to convert a 20-byte slice to a 20-byte array!?");
|
||||
let fwd = CryptState {
|
||||
cipher: SC::new(fckey.try_into().expect("Wrong length"), &Default::default()),
|
||||
digest: D::new().chain_update(fdinit),
|
||||
|
@ -329,7 +339,8 @@ pub(crate) mod tor1 {
|
|||
digest: D::new().chain_update(bdinit),
|
||||
last_digest_val: GenericArray::default(),
|
||||
};
|
||||
Ok(CryptStatePair { fwd, back })
|
||||
let binding = CircuitBinding::from(*binding_key);
|
||||
Ok(CryptStatePair { fwd, back, binding })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -338,8 +349,8 @@ pub(crate) mod tor1 {
|
|||
SC: StreamCipher,
|
||||
D: Digest + Clone,
|
||||
{
|
||||
fn split(self) -> (CryptState<SC, D>, CryptState<SC, D>) {
|
||||
(self.fwd, self.back)
|
||||
fn split(self) -> (CryptState<SC, D>, CryptState<SC, D>, CircuitBinding) {
|
||||
(self.fwd, self.back, self.binding)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -484,7 +495,7 @@ mod test {
|
|||
cc_in: &mut InboundClientCrypt,
|
||||
pair: Tor1RelayCrypto,
|
||||
) {
|
||||
let (outbound, inbound) = pair.split();
|
||||
let (outbound, inbound, _) = pair.split();
|
||||
cc_out.add_layer(Box::new(outbound));
|
||||
cc_in.add_layer(Box::new(inbound));
|
||||
}
|
||||
|
@ -573,12 +584,13 @@ mod test {
|
|||
use digest::XofReader;
|
||||
use digest::{ExtendableOutput, Update};
|
||||
|
||||
const K1: &[u8; 72] =
|
||||
b" 'My public key is in this signed x509 object', said Tom assertively.";
|
||||
const K2: &[u8; 72] =
|
||||
b"'Let's chart the pedal phlanges in the tomb', said Tom cryptographically";
|
||||
const K3: &[u8; 72] =
|
||||
b" 'Segmentation fault bugs don't _just happen_', said Tom seethingly.";
|
||||
// (The ....s at the end here are the KH ca)
|
||||
const K1: &[u8; 92] =
|
||||
b" 'My public key is in this signed x509 object', said Tom assertively. (N-PREG-VIRYL)";
|
||||
const K2: &[u8; 92] =
|
||||
b"'Let's chart the pedal phlanges in the tomb', said Tom cryptographically. (PELCG-GBR-TENCU)";
|
||||
const K3: &[u8; 92] =
|
||||
b" 'Segmentation fault bugs don't _just happen_', said Tom seethingly. (P-GUVAT-YL)";
|
||||
|
||||
const SEED: &[u8;108] = b"'You mean to tell me that there's a version of Sha-3 with no limit on the output length?', said Tom shakily.";
|
||||
|
||||
|
|
Loading…
Reference in New Issue