Skip to main content

rings_node/onion/circuit/
mod.rs

1//! Encrypted onion circuit data plane.
2//!
3//! Security model: forward layers are wrapped from exit to entry with the selected hop session
4//! public keys. Each relay decrypts exactly one ElGamal-AEAD layer and learns only the immediate
5//! next hop plus an opaque inner layer. Backward frames carry a client-encrypted AEAD payload and
6//! relays forward them with local return state.
7
8mod cell;
9mod codec;
10mod crypto;
11mod limiter;
12mod protocol;
13mod reducer;
14mod send_outbox;
15mod shell;
16
17#[cfg(test)]
18mod tests;
19
20use bytes::Bytes;
21pub use cell::OnionCellBucket;
22pub use codec::OnionCircuitEvent;
23pub use crypto::encode_initial_forward;
24#[cfg(rings_browser)]
25pub(crate) use crypto::encode_initial_forward_link;
26pub use crypto::route_first_hop;
27pub(crate) use crypto::send_backward;
28#[cfg(rings_native)]
29pub(crate) use crypto::OnionCircuitPath;
30pub use protocol::OnionCircuitCapabilities;
31pub use protocol::OnionCircuitProtocol;
32pub use reducer::OnionCircuitEffect;
33pub use reducer::OnionCircuitState;
34use rings_core::dht::Did;
35use rings_core::ecc::elgamal::impls::secp256k1::AeadCiphertext;
36use rings_core::ecc::PublicKey;
37use rings_core::message::MessageVerification;
38pub(crate) use send_outbox::OnionLinkSender;
39use serde::Deserialize;
40use serde::Serialize;
41pub use shell::OnionCircuitExitFrame;
42pub use shell::OnionCircuitHandler;
43pub use shell::OnionCircuitShell;
44
45use super::OnionServiceName;
46use crate::error::Result;
47
48/// Immediate authenticated overlay link for one already sealed circuit cell.
49#[derive(Clone, Copy, Debug, Eq, PartialEq)]
50pub(crate) struct OnionLink {
51    peer: Did,
52    recipient: PublicKey<33>,
53}
54
55impl OnionLink {
56    const fn new(peer: Did, recipient: PublicKey<33>) -> Self {
57        Self { peer, recipient }
58    }
59}
60
61/// Namespace used by route-aware onion circuit messages.
62pub const ONION_CIRCUIT_NAMESPACE: &str = "onion-circuit";
63
64/// Security mode implemented by the current circuit wire format.
65#[derive(Clone, Copy, Debug, Eq, PartialEq)]
66pub enum OnionCircuitSecurity {
67    /// Layered ElGamal-AEAD forward frames with client-encrypted backward payloads.
68    LayeredAead,
69}
70
71/// Current circuit security mode.
72pub const ONION_CIRCUIT_SECURITY: OnionCircuitSecurity = OnionCircuitSecurity::LayeredAead;
73
74/// Maximum route length encoded by local clients and maximum relay hop-budget value accepted per
75/// decrypted layer.
76pub const MAX_ONION_CIRCUIT_HOPS: u8 = 8;
77
78pub(super) const MAX_ONION_RELAY_CIRCUITS: usize = 1024;
79pub(super) const ONION_RELAY_RETURN_TTL_MS: u128 = 120_000;
80pub(super) const ONION_FORWARD_PAYLOAD_TTL_MS: u128 = 120_000;
81pub(super) const ONION_FORWARD_EXPIRY_QUANTUM_MS: u128 = 30_000;
82/// Maximum authenticated lifetime accepted by an exit after receipt.
83///
84/// Law: replay witnesses live for this same interval, so no still-valid forward layer can outlive
85/// the nonce that proves its one-shot exit effect was already consumed.
86pub(super) const ONION_FORWARD_MAX_VALIDITY_MS: u128 =
87    ONION_FORWARD_PAYLOAD_TTL_MS + ONION_FORWARD_EXPIRY_QUANTUM_MS;
88pub(super) const ONION_CRYPTO_LIMIT_WINDOW_MS: u128 = 60_000;
89pub(super) const MAX_ONION_CRYPTO_OPS_PER_WINDOW: u32 = 4096;
90pub(super) const MAX_ONION_CRYPTO_OPS_GLOBAL_PER_WINDOW: u32 = 8192;
91pub(super) const MAX_ONION_CRYPTO_BYTES_PER_WINDOW: u64 = 256 * 1024 * 1024;
92pub(super) const MAX_ONION_CRYPTO_BYTES_GLOBAL_PER_WINDOW: u64 = 512 * 1024 * 1024;
93pub(super) const MAX_ONION_CRYPTO_PEERS: usize = 64;
94pub(super) const ONION_AEAD_NAMESPACE: &str = "rings-node:onion-circuit:v1";
95
96/// Opaque application payload carried over a route-aware onion circuit.
97///
98/// The circuit layer knows only the service label and authenticated bytes. TCP, HTTPS, or future
99/// adapters own their own payload algebra outside the encrypted circuit core.
100#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
101pub struct OnionCircuitPayload {
102    /// Canonical application service selected from the onion-exit registry.
103    pub service: OnionServiceName,
104    /// Adapter-owned payload bytes.
105    pub body: Bytes,
106}
107
108impl OnionCircuitPayload {
109    /// Build an opaque circuit payload for one already-validated application service.
110    pub fn new(service: OnionServiceName, body: impl Into<Bytes>) -> Self {
111        Self {
112            service,
113            body: body.into(),
114        }
115    }
116
117    /// Build an opaque circuit payload from an untrusted service string.
118    pub fn try_new(service: impl AsRef<str>, body: impl Into<Bytes>) -> Result<Self> {
119        Ok(Self::new(OnionServiceName::parse(service)?, body))
120    }
121
122    /// Return the canonical service selected by this payload.
123    pub fn service(&self) -> &str {
124        self.service.as_str()
125    }
126
127    /// Return the canonical service name selected by this payload.
128    pub fn service_name(&self) -> &OnionServiceName {
129        &self.service
130    }
131
132    /// Return whether this payload belongs to the already canonical `service`.
133    pub fn is_service(&self, service: &OnionServiceName) -> bool {
134        &self.service == service
135    }
136
137    /// Return whether this payload belongs to `service` after service-name canonicalization.
138    pub fn matches_service(&self, service: &str) -> bool {
139        self.service.matches(service)
140    }
141}
142
143/// Client-decrypted backward payload plus the exit session proof that authenticated it.
144#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
145pub struct OnionAuthenticatedPayload {
146    /// Client/exit-only return id encrypted in the exit layer.
147    pub return_id: OnionReturnId,
148    /// Random transcript nonce signed by the exit for ciphertext and signature freshness.
149    pub nonce: OnionBackwardNonce,
150    /// Monotonic sequence in the exit-to-client direction for this circuit.
151    pub sequence: OnionBackwardSequence,
152    /// Exit session signature over the backward payload transcript.
153    pub authentication: MessageVerification,
154    /// Application payload signed by the exit and encrypted to the client.
155    pub payload: OnionCircuitPayload,
156}
157
158/// Client/exit-only id used to authenticate backward payloads.
159///
160/// This id is encrypted inside the exit layer and never appears as a relay edge header. Relays may
161/// rewrite [`OnionCircuitId`] while forwarding backward frames; the client adapter accepts a
162/// backward payload only when this signed return id matches its pending request or stream.
163#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
164pub struct OnionReturnId([u8; 16]);
165
166impl OnionReturnId {
167    /// Build a return id from random bytes.
168    pub const fn new(bytes: [u8; 16]) -> Self {
169        Self(bytes)
170    }
171
172    /// Generate a random return id.
173    pub fn random() -> Self {
174        Self(rand::random())
175    }
176}
177
178/// Random nonce for one backward payload on a circuit.
179#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
180pub struct OnionBackwardNonce([u8; 16]);
181
182impl OnionBackwardNonce {
183    /// Build a nonce from random bytes.
184    pub const fn new(bytes: [u8; 16]) -> Self {
185        Self(bytes)
186    }
187
188    /// Generate a random backward-payload nonce.
189    pub fn random() -> Self {
190        Self(rand::random())
191    }
192}
193
194/// Monotonic exit-to-client sequence number within one circuit.
195#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
196pub struct OnionBackwardSequence(u64);
197
198impl OnionBackwardSequence {
199    /// First sequence in a circuit direction.
200    pub const FIRST: Self = Self(0);
201
202    /// Build a sequence from its wire value.
203    pub const fn new(value: u64) -> Self {
204        Self(value)
205    }
206
207    /// Return the wire-order value.
208    pub const fn value(self) -> u64 {
209        self.0
210    }
211}
212
213/// Random nonce for one forward exit payload on a circuit.
214#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
215pub struct OnionForwardNonce([u8; 16]);
216
217impl OnionForwardNonce {
218    /// Build a nonce from random bytes.
219    pub const fn new(bytes: [u8; 16]) -> Self {
220        Self(bytes)
221    }
222
223    /// Generate a random forward-payload nonce.
224    pub fn random() -> Self {
225        Self(rand::random())
226    }
227}
228
229/// Monotonic client-to-exit sequence number within one circuit.
230#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
231pub struct OnionForwardSequence(u64);
232
233impl OnionForwardSequence {
234    /// First sequence in a circuit direction.
235    pub const FIRST: Self = Self(0);
236
237    /// Build a sequence from its wire value.
238    pub const fn new(value: u64) -> Self {
239        Self(value)
240    }
241
242    /// Return the wire-order value.
243    pub const fn value(self) -> u64 {
244        self.0
245    }
246}
247
248/// Backward payload that has passed exit identity, signature, and freshness checks.
249#[derive(Clone, Debug, Eq, PartialEq)]
250pub struct OnionVerifiedPayload {
251    /// Verified client/exit return id.
252    pub return_id: OnionReturnId,
253    /// Authenticated transcript nonce; replay admission is carried by `sequence`.
254    pub nonce: OnionBackwardNonce,
255    /// Verified monotonic backward sequence.
256    pub sequence: OnionBackwardSequence,
257    /// Verified application payload.
258    pub payload: OnionCircuitPayload,
259}
260
261/// Client return key encrypted into the exit layer.
262#[derive(Clone, Copy, Debug, Deserialize, Serialize, Eq, PartialEq)]
263pub struct OnionClientReturn {
264    /// Client session public key used for backward AEAD payloads.
265    pub session_public_key: PublicKey<33>,
266    /// Client/exit-only id used to authenticate backward payloads.
267    pub return_id: OnionReturnId,
268}
269
270impl OnionClientReturn {
271    /// Build a client return descriptor with a fresh return id.
272    pub fn new(session_public_key: PublicKey<33>) -> Self {
273        Self {
274            session_public_key,
275            return_id: OnionReturnId::random(),
276        }
277    }
278}
279
280/// Edge-local circuit id.
281///
282/// Invariant: an [`OnionCircuitId`] identifies exactly one directed edge of one route. Relay layers
283/// carry the next edge id under AEAD; backward forwarding rewrites the header back to the previous
284/// edge id.
285#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
286pub struct OnionCircuitId([u8; 16]);
287
288impl OnionCircuitId {
289    /// Build a circuit id from random bytes.
290    pub const fn new(bytes: [u8; 16]) -> Self {
291        Self(bytes)
292    }
293
294    /// Generate a random circuit id.
295    pub fn random() -> Self {
296        Self(rand::random())
297    }
298}
299
300/// Forward direction: client -> relays -> exit.
301#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
302pub struct OnionForwardFrame {
303    /// Edge-local circuit id for the receiving hop.
304    pub circuit_id: OnionCircuitId,
305    /// AEAD-encrypted layer for the receiving hop.
306    pub layer: AeadCiphertext,
307}
308
309/// Backward direction: exit -> relays -> client.
310#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
311pub struct OnionBackwardFrame {
312    /// Edge-local circuit id for the receiving relay or client.
313    pub circuit_id: OnionCircuitId,
314    /// AEAD payload encrypted to the client session public key.
315    pub payload: AeadCiphertext,
316}
317
318/// Authenticated immediate path used to originate one backward cell at an exit.
319#[derive(Clone, Copy, Debug, Eq, PartialEq)]
320pub struct OnionBackwardPath {
321    /// Edge-local circuit id expected by the immediate return peer.
322    pub circuit_id: OnionCircuitId,
323    /// Immediate overlay return peer.
324    pub return_peer: Did,
325    /// Session key that encrypts the hop-to-hop return cell.
326    pub return_session_public_key: PublicKey<33>,
327    /// Client-only key and return id for the inner signed payload.
328    pub client: OnionClientReturn,
329}
330
331impl OnionBackwardPath {
332    /// Build a return path from values authenticated in the decrypted exit layer.
333    pub const fn new(
334        circuit_id: OnionCircuitId,
335        return_peer: Did,
336        return_session_public_key: PublicKey<33>,
337        client: OnionClientReturn,
338    ) -> Self {
339        Self {
340            circuit_id,
341            return_peer,
342            return_session_public_key,
343            client,
344        }
345    }
346}
347
348#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
349pub(super) enum OnionForwardLayer {
350    Relay {
351        next_hop: Did,
352        next_circuit_id: OnionCircuitId,
353        next_session_public_key: PublicKey<33>,
354        return_session_public_key: PublicKey<33>,
355        inner: AeadCiphertext,
356    },
357    Exit {
358        client: OnionClientReturn,
359        return_session_public_key: PublicKey<33>,
360        expires_at_ms: u128,
361        forward_nonce: OnionForwardNonce,
362        forward_sequence: OnionForwardSequence,
363        payload: OnionCircuitPayload,
364    },
365}