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