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