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