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:
Nick Mathewson 2023-01-06 18:32:36 +00:00
commit 8472acf3ac
22 changed files with 879 additions and 20 deletions

12
Cargo.lock generated
View File

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

View File

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

View File

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

View File

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

View File

@ -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),
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,8 @@
# tor-hscrypto
`tor-hscrypto`: Basic cryptography used by onion services
## Overview
TODO hs: write me.

View File

@ -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]);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

180
doc/dev/hs-overview.md Normal file
View File

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