darkbio_wire/transport/mod.rs
1// wire-rs: encrypted protocol between Ark and host
2// Copyright 2026 Dark Bio AG. All rights reserved.
3//
4// Use of this source code is governed by a BSD-style
5// license that can be found in the LICENSE file.
6
7//! Encrypted sessions over a duplex byte stream.
8//!
9//! COBS framing separates packets, the handshake establishes encryption
10//! contexts, and sealing protects messages. Each [`Client`] or [`Server`] owns
11//! its receive context and shares its send context with active sends.
12//! [`Sender`] handles allow other threads to send into that session.
13//!
14//! Adapters implement standard byte I/O plus the deadline setters in [`Read`]
15//! and [`Write`]. Each outgoing frame has one configurable budget covering
16//! partial writes and flush. A timeout ends that send or handshake without
17//! closing the byte stream. Established-session reads wait for data or adapter
18//! shutdown, with no session timeout. Handshakes use one configurable deadline
19//! on each side, defaulting to 5 s.
20//!
21//! The client writes reset and hello before draining stale replies, so adapters
22//! need enough available buffering to accept that output without concurrent
23//! client reads. Backpressure may fail an attempt; the caller can retry with a
24//! fresh reset.
25
26use std::time::Duration;
27
28mod client;
29mod framing;
30mod handshake;
31mod io;
32mod outbound;
33mod sealing;
34mod sender;
35mod server;
36mod stream;
37
38#[cfg(test)]
39#[cfg_attr(coverage_nightly, coverage(off))]
40mod clock_tests;
41
42#[cfg(any(test, feature = "fuzz"))]
43#[doc(hidden)]
44#[cfg_attr(coverage_nightly, coverage(off))]
45pub mod mock;
46
47#[cfg(any(test, feature = "bench", feature = "fuzz"))]
48#[doc(hidden)]
49#[cfg_attr(coverage_nightly, coverage(off))]
50pub mod testing;
51
52pub use client::{Client, Roots, Verifier};
53pub use io::{Read, Write};
54pub use sender::Sender;
55pub use server::{Attestation, Attester, Event, Server};
56pub use stream::{Closer, Stream};
57
58/// Default budget of 5 s for a handshake's output and peer replies.
59///
60/// Configure it with [`Client::set_handshake_timeout`] or
61/// [`Server::set_handshake_timeout`]. Progress, stale frames and resets within
62/// the attempt do not refresh it. Waiting for the writer lock and for caller
63/// callbacks may extend the call, but cannot extend its I/O deadline.
64pub const DEFAULT_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(5);
65
66/// Default budget of 5 s for encoding, writing and flushing one complete
67/// transport frame.
68///
69/// Configure it with [`Stream::set_write_timeout`]. Progress does not refresh
70/// it. The budget starts after acquiring the writer, excluding lock waits and
71/// encryption. Handshake frames are also limited by the handshake deadline.
72pub const DEFAULT_WRITE_TIMEOUT: Duration = Duration::from_secs(5);
73
74/// Maximum encoded frame size of 2 MiB, excluding its trailing delimiter.
75///
76/// An oversized incoming frame is a framing error that ends any active session.
77/// Its remainder is discarded through its delimiter so the stream can carry a
78/// fresh handshake.
79pub const MAX_FRAME_SIZE: usize = 2 * 1024 * 1024;
80
81/// Conservative soft limit for outgoing message sizes, guaranteed to fit a
82/// frame after sealing and worst-case COBS overhead.
83///
84/// The wire's hard limit is [`MAX_FRAME_SIZE`]; received messages may exceed
85/// this value if their encoded frames fit. Sending uses this conservative bound
86/// to reject oversized messages before sealing, without advancing the
87/// encryption sequence.
88pub const MAX_MESSAGE_SIZE: usize = {
89 let mut size = MAX_FRAME_SIZE;
90 while darkbio_cobs::encode_buffer(size + sealing::OVERHEAD) > MAX_FRAME_SIZE {
91 size -= 1;
92 }
93 size
94};
95
96/// Domain separator for the handshake's COSE envelopes,
97/// [`ArkHello`](handshake::ArkHello) and [`HostAck`](handshake::HostAck).
98///
99/// [`HostHello`](handshake::HostHello) is plain CBOR. The separator binds
100/// signatures and encryption to this protocol, preventing their reuse in
101/// another protocol with the same key.
102pub(crate) const CRYPTO_DOMAIN_WIRE: &[u8] = b"wire-v1";
103
104/// HPKE info string for the ark-to-host encryption context of an established
105/// session (message traffic after the handshake, not the handshake itself).
106pub(crate) const CRYPTO_DOMAIN_WIRE_ARK_TO_HOST: &[u8] = b"wire-v1:ark-to-host";
107
108/// HPKE info string for the host-to-ark encryption context of an established
109/// session (message traffic after the handshake, not the handshake itself).
110pub(crate) const CRYPTO_DOMAIN_WIRE_HOST_TO_ARK: &[u8] = b"wire-v1:host-to-ark";
111
112/// Things that can go wrong in the wire transport.
113#[derive(Debug, thiserror::Error)]
114#[cfg_attr(
115 all(any(test, feature = "fuzz"), not(docsrs)),
116 derive(strum::IntoStaticStr) // the mocks name the variants in their transcripts
117)]
118pub enum Error {
119 /// A message exceeds the sending bound [`MAX_MESSAGE_SIZE`].
120 ///
121 /// The size is the unencrypted message length. Refusal happens before
122 /// sealing, leaving the session and its encryption sequence unchanged.
123 #[error("wire packet too large: {0} bytes, max {MAX_MESSAGE_SIZE} bytes")]
124 PacketTooLarge(usize),
125
126 /// An encoded frame exceeds [`MAX_FRAME_SIZE`].
127 ///
128 /// On receive, the size counts bytes observed so far; the full frame may be
129 /// larger. On send, the size is the required worst-case COBS encoding
130 /// buffer. Receiving an oversized frame ends any active session; its
131 /// remainder is discarded through the delimiter before another frame can be
132 /// read.
133 #[error("wire frame too large: {0} bytes, max {MAX_FRAME_SIZE} bytes")]
134 FrameTooLarge(usize),
135
136 /// A delimited frame is not valid COBS.
137 ///
138 /// It may have carried an encrypted message, so skipping it ends any active
139 /// session. A reconnecting client discards malformed stale frames while
140 /// waiting for its fresh reply.
141 #[error("wire frame decode failed: {0}")]
142 FrameDecodingFailed(darkbio_cobs::DecodeError),
143
144 /// Writing a frame failed, possibly while setting its deadline or flushing.
145 ///
146 /// [`std::io::ErrorKind::TimedOut`] reports an expired output budget or a
147 /// timeout the adapter returned. The adapter may already have accepted part
148 /// or all of the frame. The affected
149 /// session or handshake cannot continue. This error does not close the byte
150 /// stream.
151 #[error("wire send failed: {0}")]
152 SendFailed(std::io::Error),
153
154 /// An adapter read or read deadline configuration failed.
155 ///
156 /// Idle read timeouts and interrupted reads are retried internally.
157 /// Configuration failures are returned immediately, as is expiry of an
158 /// overall handshake deadline. The client ends its session; the server
159 /// leaves its binding in place so the caller can decide whether to retry or
160 /// disconnect. Neither side closes the stream because of this error.
161 #[error("wire receive failed: {0}")]
162 RecvFailed(std::io::Error),
163
164 /// Reading reached EOF or a sender's transport owner was already released.
165 ///
166 /// Local closure also produces EOF once buffered frames have been consumed.
167 /// This does not guarantee that a concurrent shutdown has finished.
168 #[error("wire terminated")]
169 Terminated,
170
171 /// The session ended through a reset.
172 ///
173 /// The client returns it after the server's empty frame notification,
174 /// without closing the stream, so the client can reconnect. The transport
175 /// server reports a session's end through [`Event::Disconnected`] instead.
176 /// The [`protocol`](crate::protocol) server closes its session with this
177 /// error on that event, and when a replacement closes its predecessor.
178 #[error("wire session reset by the peer")]
179 SessionReset,
180
181 /// The presented CWT could not be decoded as hardware or emulator claims.
182 ///
183 /// This checks the token's shape. The client's [`Verifier`] decides whether
184 /// to trust a well-formed attestation.
185 #[error("attestation is not for a hardware or emulator")]
186 InvalidAttestation,
187
188 /// A handshake message could not be constructed, decoded or authenticated,
189 /// or the client's verifier rejected the attestation.
190 ///
191 /// No new session is established by this attempt. The error does not close
192 /// the byte stream.
193 #[error("wire handshake failed: {0}")]
194 HandshakeFailed(String),
195
196 /// A received packet could not be decrypted, or a send or receive has no
197 /// current session.
198 ///
199 /// Invalid incoming packets end the session. Refusing an obsolete sender
200 /// leaves any replacement session unaffected.
201 #[error("wire encryption failed: {0}")]
202 EncryptionFailed(String),
203}