Merge branch 'onion-api-v0.2' into 'main'
Onion service APIs, part 1. See merge request tpo/core/arti!959
This commit is contained in:
commit
8472acf3ac
|
@ -3581,6 +3581,7 @@ dependencies = [
|
|||
"tor-bytes",
|
||||
"tor-cert",
|
||||
"tor-error",
|
||||
"tor-hscrypto",
|
||||
"tor-linkspec",
|
||||
"tor-llcrypto",
|
||||
"tor-units",
|
||||
|
@ -3885,6 +3886,15 @@ dependencies = [
|
|||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tor-hscrypto"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"rand_core 0.6.4",
|
||||
"thiserror",
|
||||
"tor-llcrypto",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tor-linkspec"
|
||||
version = "0.6.0"
|
||||
|
@ -4004,6 +4014,8 @@ dependencies = [
|
|||
"tor-cert",
|
||||
"tor-checkable",
|
||||
"tor-error",
|
||||
"tor-hscrypto",
|
||||
"tor-linkspec",
|
||||
"tor-llcrypto",
|
||||
"tor-protover",
|
||||
"visibility",
|
||||
|
|
|
@ -20,6 +20,7 @@ members = [
|
|||
"crates/tor-llcrypto",
|
||||
"crates/tor-protover",
|
||||
"crates/tor-bytes",
|
||||
"crates/tor-hscrypto",
|
||||
"crates/tor-socksproto",
|
||||
"crates/tor-checkable",
|
||||
"crates/tor-cert",
|
||||
|
|
|
@ -13,10 +13,12 @@ repository = "https://gitlab.torproject.org/tpo/core/arti.git/"
|
|||
|
||||
[features]
|
||||
default = []
|
||||
experimental = ["experimental-udp"]
|
||||
|
||||
experimental = ["experimental-udp", "onion-service"]
|
||||
# Enable experimental UDP support.
|
||||
experimental-udp = []
|
||||
onion-service = []
|
||||
onion-service = ["tor-hscrypto"] #TODO hs: rename this feature, it is not service-specific.
|
||||
|
||||
# Enable testing only API
|
||||
testing = ["experimental-udp"]
|
||||
|
||||
|
@ -32,6 +34,7 @@ tor-basic-utils = { path = "../tor-basic-utils", version = "0.5.0" }
|
|||
tor-bytes = { path = "../tor-bytes", version = "0.6.0" }
|
||||
tor-cert = { path = "../tor-cert", version = "0.6.0" }
|
||||
tor-error = { path = "../tor-error", version = "0.4.0" }
|
||||
tor-hscrypto = { path = "../tor-hscrypto", version = "0.1.0", optional = true }
|
||||
tor-linkspec = { path = "../tor-linkspec", version = "0.6.0" }
|
||||
tor-llcrypto = { path = "../tor-llcrypto", version = "0.4.0" }
|
||||
tor-units = { path = "../tor-units", version = "0.4.0" }
|
||||
|
|
|
@ -13,6 +13,7 @@ pub mod extend;
|
|||
pub mod msg;
|
||||
#[cfg(feature = "onion-service")]
|
||||
pub mod onion_service;
|
||||
pub mod restrict;
|
||||
#[cfg(feature = "experimental-udp")]
|
||||
pub mod udp;
|
||||
|
||||
|
|
|
@ -65,19 +65,34 @@ pub enum RelayMsg {
|
|||
/// UDP stream data
|
||||
#[cfg(feature = "experimental-udp")]
|
||||
Datagram(udp::Datagram),
|
||||
// No hs for now.
|
||||
|
||||
/// Establish Introduction
|
||||
#[cfg(feature = "onion-service")]
|
||||
EstablishIntro(onion_service::EstablishIntro),
|
||||
/// Establish Rendezvous
|
||||
#[cfg(feature = "onion-service")]
|
||||
EstablishRendezvous(onion_service::EstablishRendezvous),
|
||||
/// Introduce1
|
||||
/// Introduce1 (client to introduction point)
|
||||
#[cfg(feature = "onion-service")]
|
||||
Introduce1(onion_service::Introduce1),
|
||||
/// Introduce2
|
||||
/// Introduce2 (introduction point to service)
|
||||
#[cfg(feature = "onion-service")]
|
||||
Introduce2(onion_service::Introduce2),
|
||||
/// Rendezvous1 (service to rendezvous point)
|
||||
#[cfg(feature = "onion-service")]
|
||||
Rendezvous1(onion_service::Rendezvous1),
|
||||
/// Rendezvous2 (rendezvous point to client)
|
||||
#[cfg(feature = "onion-service")]
|
||||
Rendezvous2(onion_service::Rendezvous2),
|
||||
/// Acknowledgement for EstablishIntro.
|
||||
#[cfg(feature = "onion-service")]
|
||||
IntroEstablished(onion_service::IntroEstablished),
|
||||
/// Acknowledgment for EstalishRendezvous.
|
||||
#[cfg(feature = "onion-service")]
|
||||
RendEstablished,
|
||||
/// Acknowledgement for Introduce1.
|
||||
#[cfg(feature = "onion-service")]
|
||||
IntroduceAck(onion_service::IntroduceAck),
|
||||
|
||||
/// An unrecognized command.
|
||||
Unrecognized(Unrecognized),
|
||||
|
@ -133,6 +148,17 @@ impl RelayMsg {
|
|||
Introduce1(_) => RelayCmd::INTRODUCE1,
|
||||
#[cfg(feature = "onion-service")]
|
||||
Introduce2(_) => RelayCmd::INTRODUCE2,
|
||||
#[cfg(feature = "onion-service")]
|
||||
Rendezvous1(_) => RelayCmd::RENDEZVOUS1,
|
||||
#[cfg(feature = "onion-service")]
|
||||
Rendezvous2(_) => RelayCmd::RENDEZVOUS2,
|
||||
#[cfg(feature = "onion-service")]
|
||||
IntroEstablished(_) => RelayCmd::INTRO_ESTABLISHED,
|
||||
#[cfg(feature = "onion-service")]
|
||||
RendEstablished => RelayCmd::RENDEZVOUS_ESTABLISHED,
|
||||
#[cfg(feature = "onion-service")]
|
||||
IntroduceAck(_) => RelayCmd::INTRODUCE_ACK,
|
||||
|
||||
Unrecognized(u) => u.cmd(),
|
||||
}
|
||||
}
|
||||
|
@ -174,10 +200,24 @@ impl RelayMsg {
|
|||
RelayCmd::INTRODUCE1 => {
|
||||
RelayMsg::Introduce1(onion_service::Introduce1::decode_from_reader(r)?)
|
||||
}
|
||||
|
||||
// TODO hs
|
||||
// #[cfg(feature = "onion-service")]
|
||||
// RelayCmd::RENDEZVOUS1 => todo!(),
|
||||
// #[cfg(feature = "onion-service")]
|
||||
// RelayCmd::RENDEZVOUS2 => todo!(),
|
||||
// #[cfg(feature = "onion-service")]
|
||||
// RelayCmd::INTRO_ESTABLISHED => todo!(),
|
||||
// #[cfg(feature = "onion-service")]
|
||||
// RelayCmd::RENDEZVOUS_ESTABLISHED => todo!(),
|
||||
// #[cfg(feature = "onion-service")]
|
||||
// RelayCmd::INTRODUCE_ACK => todo!(),
|
||||
_ => RelayMsg::Unrecognized(Unrecognized::decode_with_cmd(c, r)?),
|
||||
})
|
||||
}
|
||||
|
||||
/// Encode the body of this message, not including command or length
|
||||
#[allow(clippy::missing_panics_doc)] // TODO hs
|
||||
pub fn encode_onto(self, w: &mut Vec<u8>) -> EncodeResult<()> {
|
||||
use RelayMsg::*;
|
||||
match self {
|
||||
|
@ -210,6 +250,17 @@ impl RelayMsg {
|
|||
Introduce1(b) => b.encode_onto(w),
|
||||
#[cfg(feature = "onion-service")]
|
||||
Introduce2(b) => b.encode_onto(w),
|
||||
#[cfg(feature = "onion-service")]
|
||||
Rendezvous1(_) => todo!(), // TODO hs
|
||||
#[cfg(feature = "onion-service")]
|
||||
Rendezvous2(_) => todo!(), // TODO hs
|
||||
#[cfg(feature = "onion-service")]
|
||||
IntroEstablished(_) => todo!(), // TODO hs
|
||||
#[cfg(feature = "onion-service")]
|
||||
RendEstablished => todo!(), // TODO hs
|
||||
#[cfg(feature = "onion-service")]
|
||||
IntroduceAck(_) => todo!(), // TODO hs
|
||||
|
||||
Unrecognized(b) => b.encode_onto(w),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,23 @@
|
|||
//! Encoding and decoding for relay messages
|
||||
//!
|
||||
//! Relay messages are sent along circuits, inside RELAY or RELAY_EARLY
|
||||
//! cells.
|
||||
//! Encoding and decoding for relay messages related to onion services.
|
||||
|
||||
#![allow(dead_code)] // 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 neeed accessors for the useful fields in all these types.
|
||||
|
||||
use super::msg;
|
||||
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;
|
||||
|
||||
|
@ -134,6 +145,9 @@ pub struct EstablishIntro {
|
|||
/// 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>,
|
||||
/// the MAC of all earlier fields in the cell.
|
||||
handshake_auth: [u8; 32],
|
||||
|
@ -221,6 +235,11 @@ impl EstablishIntro {
|
|||
pub fn set_extension_dos(&mut self, extension_dos: EstIntroExtDoS) {
|
||||
self.extension_dos = Some(extension_dos);
|
||||
}
|
||||
|
||||
// TODO hs: we'll neeed 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.
|
||||
|
@ -228,7 +247,7 @@ impl EstablishIntro {
|
|||
pub struct EstablishRendezvous {
|
||||
/// A rendezvous cookie is an arbitrary 20-byte value,
|
||||
/// chosen randomly by the client.
|
||||
cookie: [u8; EstablishRendezvous::COOKIE_LEN],
|
||||
cookie: [u8; EstablishRendezvous::COOKIE_LEN], // TODO hs: Make this a RendCookie.
|
||||
}
|
||||
impl EstablishRendezvous {
|
||||
/// The only acceptable length of a rendezvous cookie.
|
||||
|
@ -356,3 +375,49 @@ impl Introduce {
|
|||
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<u8>,
|
||||
}
|
||||
/// 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<u8>,
|
||||
}
|
||||
|
||||
/// 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<IntroEstExtension>,
|
||||
}
|
||||
|
||||
/// An extension included in an [`IntroEstablished`] message.
|
||||
#[derive(Debug, Clone)]
|
||||
#[non_exhaustive]
|
||||
pub enum IntroEstExtension {
|
||||
/// An unrecognized exension.
|
||||
Unrecognized(Vec<u8>),
|
||||
}
|
||||
|
||||
/// 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.
|
||||
}
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
//! Macros to define a restricted subset of RelayMsg.
|
||||
//!
|
||||
//! These restricted subsets are not just a matter of internal typesafety: they
|
||||
//! provide _parsing support_ for only the relevant subset of messages, ensuring
|
||||
//! that an attacker doesn't get easy access to our whole parsing surface.
|
||||
|
||||
// TODO hs: make a new macro that behaves something like this. (This will need
|
||||
// to change; it's just a sketch.)
|
||||
//
|
||||
// See arti#525 for more info.
|
||||
//
|
||||
// ```
|
||||
// restricted_msg!( pub enum DataStreamMsg {
|
||||
// Data(Data),
|
||||
// End(End),
|
||||
// }),
|
||||
// ```
|
||||
//
|
||||
// It should define a new type that behaves like RelayMsg, except that it only
|
||||
// tries to parse a message if the command is RELAY_CMD_DATA or RELAY_CMD_END.
|
||||
//
|
||||
// It would be neat if there were as little redundancy in the format as
|
||||
// possible, and we didn't have to say "Data(Data) DATA" (meaning that the Data
|
||||
// variant uses a Data object and should be used if the relay command is DATA).
|
||||
//
|
||||
// We'll need to define how the new type behaves on other commands. In some
|
||||
// cases, it should put them in a variant `Other`. In most cases, it should
|
||||
// just give an error.
|
||||
//
|
||||
// It might be neat if this could define restricted sets of ChanMsg too. If so,
|
||||
// we should move it.
|
||||
//
|
||||
// When only one relay message is valid, it might be neat to have a simpler way
|
||||
// to parse that message specifically and reject everything else.
|
|
@ -129,6 +129,7 @@ caret_int! {
|
|||
|
||||
// 08 through 09 and 0B are used for onion services. They
|
||||
// probably shouldn't be, but that's what Tor does.
|
||||
// TODO hs: Add these types.
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -173,6 +174,8 @@ pub enum CertifiedKey {
|
|||
X509Sha256Digest([u8; 32]),
|
||||
/// Some unrecognized key type.
|
||||
Unrecognized(UnrecognizedKey),
|
||||
// TODO hs: Add new alternatives here for the case that we're handling key types from
|
||||
// onion services. These will correspond to types in tor-hscrypto.
|
||||
}
|
||||
|
||||
/// A key whose type we didn't recognize.
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
[package]
|
||||
name = "tor-hscrypto"
|
||||
version = "0.1.0"
|
||||
authors = ["The Tor Project, Inc.", "Nick Mathewson <nickm@torproject.org>"]
|
||||
edition = "2021"
|
||||
rust-version = "1.60"
|
||||
license = "MIT OR Apache-2.0"
|
||||
homepage = "https://gitlab.torproject.org/tpo/core/arti/-/wikis/home"
|
||||
description = "Basic onion service cryptography types used by Aerti"
|
||||
keywords = ["tor", "arti", "cryptography"]
|
||||
categories = ["cryptography"]
|
||||
repository = "https://gitlab.torproject.org/tpo/core/arti.git/"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
[dependencies]
|
||||
rand_core = "0.6.2"
|
||||
thiserror = "1"
|
||||
tor-llcrypto = { version = "0.4.0", path = "../tor-llcrypto" }
|
||||
|
||||
[dev-dependencies]
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
# tor-hscrypto
|
||||
|
||||
`tor-hscrypto`: Basic cryptography used by onion services
|
||||
|
||||
## Overview
|
||||
|
||||
TODO hs: write me.
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
#![cfg_attr(docsrs, feature(doc_auto_cfg, doc_cfg))]
|
||||
#![doc = include_str!("../README.md")]
|
||||
// TODO hs: apply the standard warning list to this module.
|
||||
#![allow(dead_code, unused_variables)]
|
||||
|
||||
// TODO hs: Throughout this crate, only permit constant-time comparison functions.
|
||||
|
||||
pub mod ops;
|
||||
pub mod pk;
|
||||
pub mod time;
|
||||
|
||||
/// The information that a client needs to know about an onion service in
|
||||
/// order to connect to it.
|
||||
pub struct Credential {
|
||||
/// Representation for the onion service's public ID.
|
||||
///
|
||||
/// This is the same value as is expanded to an OnionIdKey.
|
||||
id: [u8; 32],
|
||||
// secret: Vec<u8> // This is not well-supported in the C Tor
|
||||
// implementation; it's not clear to me that we should build it in either?
|
||||
}
|
||||
|
||||
/// A value to identify an onion service during a given period.
|
||||
///
|
||||
/// This is computed from the onion service's public ID and the blinded ID for
|
||||
/// the current time period.
|
||||
///
|
||||
/// Given this piece of information, the original credential cannot be re-derived.
|
||||
pub struct Subcredential([u8; 32]);
|
||||
|
||||
/// Counts which revision of an onion service descriptor is which, within a
|
||||
/// given time period.
|
||||
///
|
||||
/// There can be gaps in this numbering. A descriptor with a higher-valued
|
||||
/// revision counter supersedes one with a lower revision counter.
|
||||
pub struct RevisionCounter(u64);
|
||||
|
||||
/// An opaque value used by an onion service
|
||||
// TODO hs: these values should only permit constant-time comparison.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RendCookie([u8; 20]);
|
||||
|
||||
/// A position within the onion service directory hash ring.
|
||||
// TODO: these should move to tor-netdir, I think?
|
||||
pub struct HsRingIndex([u8; 32]);
|
|
@ -0,0 +1,4 @@
|
|||
/// Compute the lightweight MAC function used in the onion service protocol.
|
||||
pub fn hs_mac(key: &[u8], msg: &[u8]) -> [u8; 32] {
|
||||
todo!() // TODO hs
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
//! Key type wrappers of various kinds used in onion services.
|
||||
//
|
||||
// NOTE: We define wrappers here as a safety net against confusing one kind of
|
||||
// key for another: without a system like this, it can get pretty hard making
|
||||
// sure that each key is used only in the right way.
|
||||
|
||||
// TODO hs: for each of these key types, we should impl AsRef<> to get at its inner type.
|
||||
// We should impl From to convert to and from the inner types.
|
||||
// TODO hs: These are so similar to one another that we probably want to define a local
|
||||
// macro that declares them as appropriate.
|
||||
|
||||
// TODO hs: Maybe we want to remove some of these types as we build the
|
||||
// implementation; for example, if we find that a single key type is visible
|
||||
// only in a single module somewhere else, it would make sense to just use the
|
||||
// underlying type.
|
||||
|
||||
use tor_llcrypto::pk::{curve25519, ed25519};
|
||||
|
||||
use crate::time::TimePeriod;
|
||||
|
||||
/// The identity of a v3 onion service.
|
||||
///
|
||||
/// This is the decoded and validated ed25519 public key that is encoded as a
|
||||
/// `${base32}.onion` address. When expanded, it is a public key whose
|
||||
/// corresponding secret key is controlled by the onion service.
|
||||
pub struct OnionId([u8; 32]);
|
||||
|
||||
/// The identity of a v3 onion service, expanded into a public key.
|
||||
///
|
||||
/// This is the decoded and validated ed25519 public key that is encoded as
|
||||
/// a `${base32}.onion` address.
|
||||
///
|
||||
/// This key is not used to sign or validate anything on its own; instead, it is
|
||||
/// used to derive a `BlindedOnionIdKey`.
|
||||
//
|
||||
// NOTE: This is called the "master" key in rend-spec-v3, but we're deprecating
|
||||
// that vocabulary generally.
|
||||
//
|
||||
// NOTE: This is a separate type from OnionId because it is about 6x larger. It
|
||||
// is an expanded form, used for doing actual cryptography.
|
||||
pub struct OnionIdKey(ed25519::PublicKey);
|
||||
|
||||
// TODO hs: implement TryFrom<OnionId> for OnionIdKey, and From<OnionIdKey> for OnionId.
|
||||
|
||||
impl OnionIdKey {
|
||||
/// Derive the blinded key and subcredential for this identity during `cur_period`.
|
||||
pub fn compute_blinded_key(
|
||||
&self,
|
||||
cur_period: &TimePeriod,
|
||||
) -> (BlindedOnionIdKey, crate::Subcredential) {
|
||||
todo!() // TODO hs. The underlying crypto is already done in tor_llcrypto::pk::keymanip
|
||||
}
|
||||
}
|
||||
|
||||
/// The "blinded" identity of a v3 onion service.
|
||||
///
|
||||
/// This key is derived via a one-way transformation from an
|
||||
/// `OnionIdKey` and the current time period.
|
||||
///
|
||||
/// It is used for two purposes: first, to compute an index into the HSDir
|
||||
/// ring, and second, to sign a `DescSigningKey`.
|
||||
pub struct BlindedOnionIdKey(ed25519::PublicKey);
|
||||
|
||||
/// A blinded onion service identity, repreesented in a compact format.
|
||||
pub struct BlindedOnionId([u8; 32]);
|
||||
|
||||
// TODO hs: implement TryFrom<BlindedOnionId> for BlinedOnionIdKey, and
|
||||
// From<BlindedOnionIdKey> for BlindedOnionId.
|
||||
|
||||
/// A key used to sign onion service descriptors.
|
||||
///
|
||||
/// It is authenticated with a `BlindedOnionIdKeys` to prove that it belongs to
|
||||
/// the right onion service, and is used in turn to sign the descriptor that
|
||||
/// tells clients what they need to know about contacting an onion service.
|
||||
///
|
||||
/// Onion services create a new `DescSigningKey` every time the
|
||||
/// `BlindedOnionIdKeys` rotates, to prevent descriptors made in one time period
|
||||
/// from being linkable to those made in another.
|
||||
///
|
||||
/// Note: we use a separate signing key here, rather than using the
|
||||
/// BlidedOnionIdKey directly, so that the secret key for the BlindedOnionIdKey
|
||||
/// can be kept offline.
|
||||
pub struct DescSigningKey(ed25519::PublicKey);
|
||||
|
||||
/// A key used to identify and authenticate an onion service at a single
|
||||
/// introduction point.
|
||||
///
|
||||
/// This key is included in the onion service's descriptor; a different one is
|
||||
/// used at each introduction point. Introduction points don't know the
|
||||
/// relation of this key to the onion service: they only recognize the same key
|
||||
/// when they see it again.
|
||||
pub struct IntroPtAuthKey(ed25519::PublicKey);
|
||||
|
||||
/// A key used in the HsNtor handshake between the client and the onion service.
|
||||
///
|
||||
/// The onion service chooses a different one of these to use with each
|
||||
/// introduction point, though it does not need to tell the introduction points
|
||||
/// about these keys.
|
||||
pub struct IntroPtEncKey(curve25519::PublicKey);
|
||||
|
||||
/// First type of client authorization key, used for the introduction protocol.
|
||||
///
|
||||
/// This is used to sign a nonce included in an extension in the encrypted
|
||||
/// portion of an introduce cell.
|
||||
pub struct ClientIntroAuthKey(ed25519::PublicKey);
|
||||
|
||||
/// Second type of client authorization key, used for onion descryptor
|
||||
/// decryption.
|
||||
///
|
||||
/// Any client who knows the secret key corresponding to this key can decrypt
|
||||
/// the inner layer of the onion service descriptor.
|
||||
pub struct ClientDescAuthKey(curve25519::PublicKey);
|
||||
|
||||
// TODO hs: For each of the above key types, we should have a correspondingly
|
||||
// named private key type. These private key types should be defined with the
|
||||
// same macros that implement the other keys.
|
||||
//
|
||||
// The names should be something like these:
|
||||
pub struct OnionIdSecretKey(ed25519::SecretKey);
|
||||
pub struct ClientDescAuthSecretKey(curve25519::StaticSecret);
|
||||
// ... and so on.
|
||||
//
|
||||
// NOTE: We'll have to use ExpandedSecretKey as the secret key
|
||||
// for BlindedOnionIdSecretKey.
|
|
@ -0,0 +1,46 @@
|
|||
//! Manipulate time periods (as used in the onion service system)
|
||||
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
/// A period of time as used in the onion service system.
|
||||
///
|
||||
/// These time periods are used to derive a different `BlindedOnionIdKey`
|
||||
/// during each period from each `OnionIdKey`.
|
||||
pub struct TimePeriod {
|
||||
/// Index of the time periods that have passed since the unix epoch.
|
||||
interval_num: u64,
|
||||
/// The length of a time period, in seconds.
|
||||
length_in_sec: u32,
|
||||
}
|
||||
|
||||
impl TimePeriod {
|
||||
/// Construct a time period of a given `length` that contains `when`.
|
||||
pub fn new(length: Duration, when: SystemTime) -> Self {
|
||||
// The algorithm here is specified in rend-spec-v3 section 2.2.1
|
||||
todo!() // TODO hs
|
||||
}
|
||||
/// Return the time period after this one.
|
||||
///
|
||||
/// Return None if this is the last representable time period.
|
||||
pub fn next(&self) -> Option<Self> {
|
||||
todo!() // TODO hs
|
||||
}
|
||||
/// Return the time period after this one.
|
||||
///
|
||||
/// Return None if this is the first representable time period.
|
||||
pub fn prev(&self) -> Option<Self> {
|
||||
todo!() // TODO hs
|
||||
}
|
||||
/// Return true if this time period contains `when`.
|
||||
pub fn contains(&self, when: SystemTime) -> bool {
|
||||
todo!() // TODO hs
|
||||
}
|
||||
/// Return a range representing the [`SystemTime`] values contained within
|
||||
/// this time period.
|
||||
///
|
||||
/// Return None if this time period contains no times that can be
|
||||
/// represented as a `SystemTime`.
|
||||
pub fn range(&self) -> Option<std::ops::Range<SystemTime>> {
|
||||
todo!() // TODO hs
|
||||
}
|
||||
}
|
|
@ -20,14 +20,18 @@ use crate::util::ct::CtByteArray;
|
|||
/// The length of an ED25519 identity, in bytes.
|
||||
pub const ED25519_ID_LEN: usize = 32;
|
||||
|
||||
/// A relay's identity, as an unchecked, unvalidated Ed25519 key.
|
||||
/// An unchecked, unvalidated Ed25519 key.
|
||||
///
|
||||
/// This key is an "identity" in the sense that it identifies (up to) one
|
||||
/// Ed25519 key. It may also represent the identity for a particular entity,
|
||||
/// such as a relay or an onion service.
|
||||
///
|
||||
/// This type is distinct from an Ed25519 [`PublicKey`] for several reasons:
|
||||
/// * We're storing it in a compact format, whereas the public key
|
||||
/// implementation might want an expanded form for more efficient key
|
||||
/// validation.
|
||||
/// * This type hasn't checked whether the bytes here actually _are_ a
|
||||
/// valid Ed25519 public key.
|
||||
/// * This type hasn't checked whether the bytes here actually _are_ a valid
|
||||
/// Ed25519 public key.
|
||||
#[derive(Clone, Copy, Hash, PartialOrd, Ord, Eq, PartialEq)]
|
||||
pub struct Ed25519Identity {
|
||||
/// A raw unchecked Ed25519 public key.
|
||||
|
|
|
@ -27,12 +27,17 @@ use crate::util::ct::CtByteArray;
|
|||
/// of its RSA public identity key.)
|
||||
pub const RSA_ID_LEN: usize = 20;
|
||||
|
||||
/// An identifier for a Tor relay, based on its legacy RSA identity
|
||||
/// key. These are used all over the Tor protocol.
|
||||
/// An identifier for an RSA key, based on SHA1 and DER.
|
||||
///
|
||||
/// Note that for modern purposes, you should almost always identify a
|
||||
/// relay by its [`Ed25519Identity`](crate::pk::ed25519::Ed25519Identity)
|
||||
/// instead of by this kind of identity key.
|
||||
/// These are used (for legacy purposes) all over the Tor protocol.
|
||||
///
|
||||
/// This object is an "identity" in the sense that it identifies (up to) one RSA
|
||||
/// key. It may also represent the identity for a particular entity, such as a
|
||||
/// relay or a directory authority.
|
||||
///
|
||||
/// Note that for modern purposes, you should almost always identify a relay by
|
||||
/// its [`Ed25519Identity`](crate::pk::ed25519::Ed25519Identity) instead of by
|
||||
/// this kind of identity key.
|
||||
#[derive(Clone, Copy, Hash, Ord, PartialOrd, Eq, PartialEq)]
|
||||
pub struct RsaIdentity {
|
||||
/// SHA1 digest of a DER encoded public key.
|
||||
|
@ -57,7 +62,7 @@ impl fmt::Debug for RsaIdentity {
|
|||
}
|
||||
|
||||
impl safelog::Redactable for RsaIdentity {
|
||||
/// Warning: This displays 16 bits of the ed25519 identity, which is
|
||||
/// Warning: This displays 16 bits of the RSA identity, which is
|
||||
/// enough to narrow down a public relay by a great deal.
|
||||
fn display_redacted(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "${}…", hex::encode(&self.id.as_ref()[..1]))
|
||||
|
|
|
@ -15,7 +15,7 @@ repository = "https://gitlab.torproject.org/tpo/core/arti.git/"
|
|||
default = []
|
||||
|
||||
full = ["routerdesc", "ns_consensus"]
|
||||
experimental = ["build_docs", "experimental-api"]
|
||||
experimental = ["build_docs", "experimental-api", "onion-client", "onion-service"]
|
||||
|
||||
# Enable code to build the objects that represent different network documents.
|
||||
build_docs = ["rand"]
|
||||
|
@ -27,6 +27,13 @@ routerdesc = []
|
|||
# Enable the "ns consensus" document type, which some relays cache and serve.
|
||||
ns_consensus = []
|
||||
|
||||
# Client-side and service-side support for onion services.
|
||||
# Experimental: not covered by semver guarantees.
|
||||
# TODO hs: mark these as part of "full" once they are done and stable.
|
||||
onion-client = ["onion-common"]
|
||||
onion-service = ["onion-common", "rand"]
|
||||
onion-common = ["tor-hscrypto", "tor-linkspec"]
|
||||
|
||||
# Enable experimental APIs that are not yet officially supported.
|
||||
#
|
||||
# These APIs are not covered by semantic versioning. Using this
|
||||
|
@ -64,6 +71,8 @@ tor-bytes = { path = "../tor-bytes", version = "0.6.0" }
|
|||
tor-cert = { path = "../tor-cert", version = "0.6.0" }
|
||||
tor-checkable = { path = "../tor-checkable", version = "0.4.0" }
|
||||
tor-error = { path = "../tor-error", version = "0.4.0" }
|
||||
tor-hscrypto = { path = "../tor-hscrypto", version = "0.1.0", optional = true }
|
||||
tor-linkspec = { path = "../tor-linkspec", version = "0.6.0", optional = true }
|
||||
tor-llcrypto = { path = "../tor-llcrypto", version = "0.4.0" }
|
||||
tor-protover = { path = "../tor-protover", version = "0.4.0" }
|
||||
visibility = { version = "0.0.1", optional = true }
|
||||
|
|
|
@ -32,6 +32,8 @@
|
|||
use crate::util::intern::InternCache;
|
||||
|
||||
pub mod authcert;
|
||||
#[cfg(feature = "onion-common")]
|
||||
pub mod hsdesc;
|
||||
pub mod microdesc;
|
||||
pub mod netstatus;
|
||||
|
||||
|
|
|
@ -0,0 +1,186 @@
|
|||
//! Implementation for onion service descriptors.
|
||||
//!
|
||||
//! An onion service descriptor is a document generated by an onion service and
|
||||
//! uploaded to one or more HsDir nodes for clients to later download. It tells
|
||||
//! the onion service client where to find the current introduction points for
|
||||
//! the onion service, and how to connect to them.
|
||||
//!
|
||||
//! An onion service descriptor is more complicated than most other
|
||||
//! documentation types, because it is partially encrypted.
|
||||
|
||||
#![allow(dead_code, unused_variables, clippy::missing_panics_doc)] // TODO hs: remove.
|
||||
mod desc_enc;
|
||||
|
||||
use std::time::SystemTime;
|
||||
|
||||
use crate::Result;
|
||||
pub use desc_enc::DecryptionError;
|
||||
use tor_checkable::{signed, timed};
|
||||
use tor_hscrypto::pk::{
|
||||
BlindedOnionId, ClientDescAuthKey, ClientDescAuthSecretKey, IntroPtAuthKey, IntroPtEncKey,
|
||||
OnionId,
|
||||
};
|
||||
use tor_linkspec::LinkSpec;
|
||||
use tor_llcrypto::pk::curve25519;
|
||||
|
||||
/// Metadata about an onion service descriptor, as stored at an HsDir.
|
||||
///
|
||||
/// This object is parsed from the outermost layer of an onion service
|
||||
/// descriptor, and used on the HsDir to maintain its index. It does not
|
||||
/// include the inner layers' information about introduction points, since the
|
||||
/// HsDir cannot decrypt those without knowing the onion service's un-blinded
|
||||
/// identity.
|
||||
///
|
||||
/// The HsDir caches this value, along with the original text of the descriptor.
|
||||
pub struct StoredHsDescMeta {
|
||||
/// The blinded onion identity for this descriptor. (This is the only
|
||||
/// identity that the HsDesc knows.)
|
||||
blinded_id: BlindedOnionId,
|
||||
|
||||
/// Information about the expiration and revision counter for this
|
||||
/// descriptor.
|
||||
idx_info: IndexInfo,
|
||||
}
|
||||
|
||||
/// An unchecked StoredHsDescMeta: parsed, but not checked for liveness or validity.
|
||||
pub type UncheckedStoredHsDescMeta =
|
||||
timed::TimerangeBound<signed::SignatureGated<StoredHsDescMeta>>;
|
||||
|
||||
/// Information about how long to hold a given onion service descriptor, and
|
||||
/// when to replace it.
|
||||
struct IndexInfo {
|
||||
/// The lifetime in minutes that this descriptor should be held after it is
|
||||
/// received.
|
||||
desc_lifetime: u16,
|
||||
/// The expiration time on the signing key certificate included in this
|
||||
/// descriptor.
|
||||
signing_cert_expires: SystemTime,
|
||||
/// The revision counter on this descriptor: higher values should replace
|
||||
/// older ones.
|
||||
revision: u64,
|
||||
}
|
||||
|
||||
/// A decrypted, decoded onion service descriptor.
|
||||
///
|
||||
/// This object includes information from both the outer (plaintext) layer of
|
||||
/// the descriptor, and the inner (encrypted) layers. It tells the client the
|
||||
/// information it needs to contact the onion service, including necessary
|
||||
/// introduction points and public keys.
|
||||
pub struct HsDesc {
|
||||
/// The real onion identity for this onion service.
|
||||
id: OnionId,
|
||||
|
||||
/// Information about the expiration and revision counter for this
|
||||
/// descriptor.
|
||||
idx_info: IndexInfo,
|
||||
|
||||
/// The public key (if any) for the private key that we used to decrypt this descriptor.
|
||||
decrypted_with_id: Option<ClientDescAuthKey>,
|
||||
|
||||
/// A list of recognized CREATE handshakes that this onion service supports.
|
||||
// TODO hs: this should probably be an enum, not a string
|
||||
create2_formats: Vec<String>,
|
||||
|
||||
/// A list of authentication types that this onion service supports.
|
||||
// TODO hs: this should probably be an enum, not a string
|
||||
auth_required: Vec<String>, // TODO hs
|
||||
|
||||
/// If true, this a "single onion service" and is not trying to keep its own location private.
|
||||
is_single_onion_service: bool,
|
||||
|
||||
/// One or more introduction points used to contact the onion service.
|
||||
intro_points: Vec<IntroPointDesc>,
|
||||
}
|
||||
|
||||
/// An unchecked HsDesc: parsed, but not checked for liveness or validity.
|
||||
pub type UncheckedHsDesc = timed::TimerangeBound<signed::SignatureGated<HsDesc>>;
|
||||
|
||||
/// Information in an onion service descriptor about a single
|
||||
/// introduction point.
|
||||
pub struct IntroPointDesc {
|
||||
/// A list of link specifiers needed to extend a circuit to the introduction point.
|
||||
///
|
||||
/// These can include public keys and network addresses.
|
||||
//
|
||||
// TODO hs: perhaps we should make certain link specifiers mandatory? That
|
||||
// would make it possible for IntroPointDesc to implement CircTarget.
|
||||
link_specifiers: Vec<LinkSpec>,
|
||||
|
||||
/// The key used to extand a circuit to the introduction point, using the
|
||||
/// ntor or ntor3 handshakes.
|
||||
ntor_onion_key: curve25519::PublicKey,
|
||||
|
||||
/// A key used to identify the onion service at this introduction point.
|
||||
auth_key: IntroPtAuthKey,
|
||||
|
||||
/// The key used to encrypt a handshake _to the onion service_ when using this
|
||||
/// introdution point.
|
||||
hs_enc_key: IntroPtEncKey,
|
||||
}
|
||||
|
||||
/// An onion service after it has been parsed by the client, but not yet decrypted.
|
||||
pub struct EncryptedHsDesc {
|
||||
/// The real onion identity for this onion service.
|
||||
id: OnionId,
|
||||
|
||||
/// Information about the expiration and revision counter for this
|
||||
/// descriptor.
|
||||
idx_info: IndexInfo,
|
||||
|
||||
/// An encrypted string describing the actual introduction points for this onion service.
|
||||
encrypted: Vec<u8>,
|
||||
}
|
||||
|
||||
/// An unchecked HsDesc: parsed, but not checked for liveness or validity.
|
||||
pub type UncheckedEncryptedHsDesc = timed::TimerangeBound<signed::SignatureGated<EncryptedHsDesc>>;
|
||||
|
||||
impl StoredHsDescMeta {
|
||||
// TODO hs: needs accessor functions too. (Let's not use public fields; we
|
||||
// are likely to want to mess with the repr of these types.)
|
||||
|
||||
/// Parse the outermost layer of the descryptor in `input`, and return the
|
||||
/// resulting metadata (if possible).
|
||||
pub fn parse(input: &str) -> Result<UncheckedStoredHsDescMeta> {
|
||||
todo!() // TODO hs
|
||||
}
|
||||
}
|
||||
|
||||
impl HsDesc {
|
||||
// TODO hs: needs accessor functions too. (Let's not use public fields; we
|
||||
// are likely to want to mess with the repr of these types.)
|
||||
|
||||
/// Parse the outermost layer of the descriptor in `input`, and validate
|
||||
/// that its identity is consistent with `blinded_onion_id`.
|
||||
///
|
||||
/// On success, the caller will get a wrapped object which they must
|
||||
/// validate and then decrypt.
|
||||
pub fn parse(
|
||||
input: &str,
|
||||
blinded_onion_id: &BlindedOnionId,
|
||||
) -> Result<UncheckedEncryptedHsDesc> {
|
||||
todo!() // TODO hs
|
||||
}
|
||||
}
|
||||
|
||||
impl EncryptedHsDesc {
|
||||
/// Attempt to decrypt both layers of encryption in this onion service
|
||||
/// descriptor.
|
||||
///
|
||||
/// If `using_key` is provided, we use it to decrypt the inner layer;
|
||||
/// otherwise, we require that the inner layer is encrypted using the "no
|
||||
/// client authorization" method.
|
||||
//
|
||||
// TODO hs: I'm not sure that taking `using_key` as an argument is correct. Instead, maybe
|
||||
// we should take a keystore trait? Or a function from &ClientDescAuthKey to &ClientDescAuthSecretKey?
|
||||
pub fn decrypt(
|
||||
self,
|
||||
using_key: Option<(&ClientDescAuthKey, &ClientDescAuthSecretKey)>,
|
||||
) -> Result<UncheckedHsDesc> {
|
||||
todo!() // TODO hs desc
|
||||
}
|
||||
}
|
||||
|
||||
// TODO hs: Define a HsDescBuilder structure, but it should not create an HsDesc directly.
|
||||
// Instead, it should make something that is _like_ an HsDesc but with extra client keys,
|
||||
// full certificates and so on. Then, define a function taking the correct set of private
|
||||
// keys and using them to encode, encrypt, and sign the built HsDesc.
|
|
@ -0,0 +1,47 @@
|
|||
//! Types and functions for onion service descriptor encryption.
|
||||
//!
|
||||
//! TODO hs: It's possible that this should move to tor-netdoc.
|
||||
|
||||
use rand::CryptoRng;
|
||||
use tor_hscrypto::{pk::BlindedOnionId, RevisionCounter, Subcredential};
|
||||
|
||||
/// Parameters for encrypting or decrypting part of an onion service descriptor.
|
||||
///
|
||||
/// The algorithm is as described in section `[HS-DESC-ENCRYPTION-KEYS]` of
|
||||
/// rend-spec-v3.txt
|
||||
pub(super) struct HsDescEncryption<'a> {
|
||||
/// First half of the "SECRET_DATA" field.
|
||||
pub(super) blinded_id: &'a BlindedOnionId,
|
||||
/// Second half of the "SECRET_DATA" field.
|
||||
pub(super) encryption_cookie: Option<&'a DescEncryptionCookie>,
|
||||
/// The "subcredential" of the onion service.
|
||||
pub(super) subcredential: &'a Subcredential,
|
||||
/// The current revision of the onion service descriptor being decrypted.
|
||||
pub(super) revision: RevisionCounter,
|
||||
/// A personalization string.
|
||||
pub(super) string_const: &'a [u8],
|
||||
}
|
||||
|
||||
/// A value used in deriving the encryption key for the inner layer of onion
|
||||
/// service encryption.
|
||||
pub(super) struct DescEncryptionCookie([u8; 32]);
|
||||
|
||||
impl<'a> HsDescEncryption<'a> {
|
||||
/// Encrypt a given bytestring using these encryption parameters.
|
||||
pub(super) fn encrypt<R: CryptoRng>(&self, rng: &mut R, data: &[u8]) -> Vec<u8> {
|
||||
todo!() // TODO hs
|
||||
}
|
||||
/// Decrypt a given bytestring that was first encrypted using these
|
||||
/// encryption parameters.
|
||||
pub(super) fn decrypt(&self, data: &[u8]) -> Result<Vec<u8>, DecryptionError> {
|
||||
todo!() // TODO hs
|
||||
}
|
||||
}
|
||||
|
||||
/// An error that occurs when decrypting an onion service decryptor.
|
||||
///
|
||||
/// This error is deliberately uninformative, to avoid side channels.
|
||||
#[non_exhaustive]
|
||||
#[derive(Clone, Debug, thiserror::Error)]
|
||||
#[error("Unable to decrypt onion service decryptor.")]
|
||||
pub struct DecryptionError {}
|
|
@ -292,8 +292,12 @@ pub struct SignatureGroup {
|
|||
signatures: Vec<Signature>,
|
||||
}
|
||||
|
||||
// TODO hs: Lower this type to tor-llcrypto: It is relied upon by various crypto
|
||||
// things in onion services, and may later be used elsewhere too.
|
||||
//
|
||||
/// A shared-random value produced by the directory authorities.
|
||||
#[allow(dead_code)]
|
||||
// TODO hs: This should have real accessors, not this 'visible/visibility' hack.
|
||||
#[cfg_attr(
|
||||
feature = "dangerous-expose-struct-fields",
|
||||
visible::StructFields(pub),
|
||||
|
@ -311,6 +315,8 @@ struct SharedRandVal {
|
|||
/// that this value isn't predictable before it first becomes
|
||||
/// live, and that a hostile party could not have forced it to
|
||||
/// have any more than a small number of possible random values.
|
||||
//
|
||||
// TODO hs-client: This should become [u8; 32] if we get approval to nail it down in the spec.
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "dangerous-expose-struct-fields")))]
|
||||
value: Vec<u8>,
|
||||
}
|
||||
|
|
|
@ -0,0 +1,180 @@
|
|||
# How do onion services work?
|
||||
|
||||
Here's an interaction diagram, since I hear you like those. (It's in something
|
||||
called "mermaid", but apparently gitlab can render that, and so can rustdoc
|
||||
(with the appropriate plugin).)
|
||||
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant Service
|
||||
participant IntroPt
|
||||
participant HsDir
|
||||
participant RendPt
|
||||
participant Client
|
||||
note over IntroPt: Chosen randomly by Service
|
||||
Service-->>IntroPt: EstablishIntro: Sign(IntroIdKey, CircBinding)
|
||||
IntroPt-->>Service: IntroEstablished.
|
||||
note over HsDir: At position chosen by hash ring
|
||||
Service-->>HsDir: HTTP POST blinded_id Descriptor(Sign(BlindId, Seq, Enc(IntroPt, IntroIdKey, IntroEncKey)))
|
||||
HsDir-->>Service: 200 OK
|
||||
note over Client: Client decides to connect to `id.onion`
|
||||
note over RendPt: Chosen randomly by Client
|
||||
par
|
||||
Client-->>RendPt: EstablishRendezvous(RendCookie)
|
||||
RendPt-->>Client: RendezvousEstablished
|
||||
and
|
||||
Client-->>HsDir: HTTP GET BlindId
|
||||
HsDir-->>Client: 200 OK Descriptor(...)
|
||||
end
|
||||
Client-->>IntroPt: Introduce1(IntroIdKey, Enc(RendPt, RendCookie, NtorHandshake1]))
|
||||
IntroPt-->>Client: IntroduceAck
|
||||
IntroPt-->>Service: Introduce2(IntroIdKey, [...])
|
||||
Service-->>RendPt: Rendezvous1([RendCookie, NtorHandshake2])
|
||||
RendPt-->>Client: Rendezvous2(NtorHandshake2)
|
||||
```
|
||||
|
||||
Note 1: All communications are done over anonymous Tor circuits.
|
||||
|
||||
Note 2: In reality, each onion service establishes multiple introduction points
|
||||
at a time, and uploads its descriptor to multiple HsDirs. The diagram above is
|
||||
simplified.
|
||||
|
||||
Note 3: The actual data formats are slightly simplified here; in reality, there
|
||||
is typically room for extensions, more kinds of keys, and so on.
|
||||
|
||||
|
||||
|
||||
Before we begin:
|
||||
|
||||
* The onion service has made an ed25519 "identity key" that is encoded in its
|
||||
address as `id.onion`.
|
||||
* It has computed, for the current time period, a "blinded identity key" that
|
||||
can be derived through a one-way process from the original ID key and the
|
||||
current time period.
|
||||
* It has made, for the current time period, a "descriptor signing key" that is
|
||||
authenticated via a certificate issued by the blinded identity key.
|
||||
|
||||
|
||||
Now we get to the communications steps in the diagram:
|
||||
|
||||
1. The onion service wants to advertise itself. It chooses a random
|
||||
introduction point on the network. For this introduction point, it creates a
|
||||
random ed25519 "introduction id key" (to identify it with this introduction
|
||||
point), and a random curve25519 "introduction encryption key" (for clients to
|
||||
use when talking to it via this introduction point).
|
||||
|
||||
The onion service tells the introduction point to make a given circuit an
|
||||
introduction circuit for the given "introduction id key". It authenticates
|
||||
this method with the introduction ID key itself. To prevent replays, the
|
||||
message also includes a "circuit binding" token derived from when the circuit
|
||||
was constructed.
|
||||
|
||||
2. The introduction point replies to report success.
|
||||
|
||||
Note that the introduction point knows only the introduction id key for its
|
||||
associated circuit; it does not know any other keys.
|
||||
|
||||
3. The onion service generates a descriptor and uploads it to a HsDir. The
|
||||
HsDir is determined by a position on the hash ring derived from the blinded key identity, the current time period, and the current shared random value.
|
||||
|
||||
The descriptor is signed by the descriptor signing key. It contains:
|
||||
* The blinded identity key.
|
||||
* The certificate signed by the the blinded identity key, certifying
|
||||
the descriptor signing key.
|
||||
* A sequence number to prevent rollbacks
|
||||
* A section encrypted with a symmetric key derived from the service's true identity, containing:
|
||||
* A list of introduction points and their associated keys.
|
||||
|
||||
|
||||
4. The HsDir verifies that the certificate and descriptor are well-signed, and
|
||||
that the correctly proves ownership of the descriptor using the blinded
|
||||
identity key. It verifies that it does not have any other descriptor for the
|
||||
same blinded identity key with a higher sequence number. Finally, the HsDir
|
||||
stores the descriptor indexed at the blinded id, replying with 200 OK.
|
||||
|
||||
Note that the HSDir has learned only the blinded id for the service. Unless
|
||||
it knows the true identity from some other mechanism, it cannot determine the
|
||||
introduction points or their associated keys.
|
||||
|
||||
At this point the onion service is running; it keeps its circuit open to the
|
||||
introduction point and waits to hear more.
|
||||
|
||||
Now a client comes along. It is told to connect to `id.onion`, and computes the
|
||||
blinded id key (and the corresponding location on the hash ring) using the same
|
||||
logic as the service.
|
||||
|
||||
5. If the client does not already have one ready, it opens a circuit to a
|
||||
randomly chosen rendezvous point, and tells it to wait for another party
|
||||
presenting the same rendezvous cookie.
|
||||
|
||||
6. The rendezvous point replies with "rendezvous established."
|
||||
|
||||
7. In parallel with steps 5 and 6 if needed, the client connects to the HsDir and
|
||||
fetches the descriptor, indexed by the blinded ID.
|
||||
|
||||
8. The HsDir replies with the latest descriptor for the given service.
|
||||
|
||||
The client validates the descriptor, decrypts the signed portion of the
|
||||
descriptor, and learns the service's current introduction points and keys.
|
||||
|
||||
9. The client contacts the introduction point, and sends it an Introduce1 message.
|
||||
The outer portion of the cell just says which onion service (by introduction
|
||||
id key) should receive the inner portion; the inner portion is encrypted
|
||||
using the introduction encryption key (which the introduction point does not
|
||||
know).
|
||||
|
||||
The encrypted inner portion contains:
|
||||
* the rendezvous cookie
|
||||
* information about the rendezvous point
|
||||
* the start of a cryptographic handshake with the service.
|
||||
|
||||
10. The introduction point acknowledges the client.
|
||||
|
||||
11. The introduction point delivers the inner portion of the Introduce1 message
|
||||
to the service, on the circuit that was established in step 1.
|
||||
|
||||
12. The service decrypts and validates the material from the client, to learn
|
||||
the rendezvous point and the rendezvous cookie. (It also computes the
|
||||
second part of the cryptographic handshake, along with a set of associated
|
||||
key material.)
|
||||
|
||||
The service checks for replays. It needs to maintain a separate replay cache for
|
||||
each introduction id key.
|
||||
|
||||
The service builds a circuit to the rendezvous point and presents the
|
||||
rendezvous cookie, along with its half of the handshake.
|
||||
|
||||
13. If the cookie matches n a circuit that the rendezvous point knows about,
|
||||
it relays the service's handshake back to the client, over the circuit that the
|
||||
client established in step 5.
|
||||
|
||||
The rendezvous point also now joins the client's circuit from step 5 and
|
||||
the service's circuit from step 12, so that future messages will be relayed
|
||||
between them.
|
||||
|
||||
Now, at last, the client has received the service's handshake over the expected
|
||||
circuit. The client complets the handshake, to get a set of shared encryption
|
||||
keys with the service. Now the client and service have a pair of joined
|
||||
circuits, and a set of encryption material to use to communicate.
|
||||
|
||||
|
||||
## Well hang on, what about authentication?
|
||||
|
||||
There are two places where authentication can be added to the protocol above.
|
||||
|
||||
The first instance is when encrypting the descriptor: The service can encrypt
|
||||
the list of introduction points in the descriptor so that only a client holding
|
||||
one of several curve25519 keys can decrypt it. This requires storage space
|
||||
proportional to the number of supported clients, so the upper limit is bounded.
|
||||
|
||||
The second instance is when receiving the introduce2 request: The client can
|
||||
include a random value in the introduce2 message, signed with an ed25519 signing
|
||||
key, to prove its identity.
|
||||
|
||||
> Aside: It would be a good thing to have a better authentication method that
|
||||
> signs more of the introduce2 request (like the rendezvous cookie, the intro
|
||||
> point public key, the X value from the ntor handshake, etc) in order to make
|
||||
> replays less feasible.
|
||||
|
Loading…
Reference in New Issue