Skip to main content

darkbio_wire/
side_host.rs

1// wire-rs: encrypted protocol between Ark and host
2// Copyright 2026 Dark Bio AG. All rights reserved.
3
4use crate::framing::Framing;
5use crate::handshake;
6use crate::protocol::{ArkToHost, HostToArk};
7use crate::session::Session;
8use crate::side_ark::Attestation;
9use crate::{
10    CRYPTO_DOMAIN_WIRE, CRYPTO_DOMAIN_WIRE_ARK_TO_HOST, CRYPTO_DOMAIN_WIRE_HOST_TO_ARK, Error,
11};
12use darkbio_cobs as cobs;
13use darkbio_crypto::{cbor, cose, xdsa, xhpke};
14use darkbio_trust as trust;
15use std::io::{Read, Write};
16use std::time::{SystemTime, UNIX_EPOCH};
17use tracing::{trace, warn};
18
19/// Maximum number of queued frames skipped while waiting for the ArkHello of a
20/// handshake, before giving up on the Ark. A well-behaved Ark only ever leaves
21/// a handful behind, as its writer blocks once the transport buffers fill up.
22const MAX_STALE_FRAMES: usize = 32;
23
24/// Trust policy for the device attestation an Ark presents in the handshake.
25/// It owns everything the wire deliberately does not (which roots to trust,
26/// self-signing rules, recovery overrides) and decides which Arks a session is
27/// opened with.
28pub trait Verifier {
29    /// Session info extracted from an accepted attestation.
30    type Info;
31
32    /// Verifies the device attestation, returning the Ark's identity key along
33    /// with any info extracted from the attestation. The handshake signature is
34    /// checked against the returned key, so this decision is what authenticates
35    /// the session. Rejecting the attestation aborts the handshake.
36    fn verify(&self, attestation: &Attestation) -> Result<(xdsa::PublicKey, Self::Info), String>;
37}
38
39/// A pinned identity, accepting any attestation and handing it back as
40/// presented. The handshake is authenticated against the pinned key instead.
41impl Verifier for xdsa::PublicKey {
42    type Info = Attestation;
43
44    fn verify(&self, attestation: &Attestation) -> Result<(xdsa::PublicKey, Self::Info), String> {
45        Ok((self.clone(), attestation.clone()))
46    }
47}
48
49/// Roots of trust, accepting the Arks attested under them: hardware Arks by
50/// the hardware roots and emulated Arks by the emulator roots, the attestation
51/// having to be valid at the current time. An Ark that was never onboarded is
52/// rejected, its self-signed attestation being an onboarding decision rather
53/// than one of trust.
54pub struct Roots<'a> {
55    pub hardware: &'a [xdsa::PublicKey], // Roots attesting hardware Arks
56    pub emulator: &'a [xdsa::PublicKey], // Roots attesting emulated Arks
57}
58
59impl Verifier for Roots<'_> {
60    type Info = trust::device::Device;
61
62    fn verify(&self, attestation: &Attestation) -> Result<(xdsa::PublicKey, Self::Info), String> {
63        let now = SystemTime::now()
64            .duration_since(UNIX_EPOCH)
65            .map_err(|err| err.to_string())?
66            .as_secs();
67
68        let device = trust::device::verify(
69            attestation.as_bytes(),
70            self.hardware,
71            self.emulator,
72            Some(now),
73        )
74        .map_err(|err| err.to_string())?;
75        Ok((device.signer.clone(), device))
76    }
77}
78
79/// Host side of the wire, an encrypted transport for issuing protobuf requests
80/// to a connected Ark. It initiates sessions by signaling a transport reset and
81/// driving the handshake, afterward encrypting outbound and decrypting inbound
82/// messages.
83///
84/// The device attestation presented in the handshake is not interpreted by the
85/// wire, it is handed to a `Verifier` deciding whether to trust the Ark.
86pub struct HostSide<R: Read, W: Write> {
87    framing: Framing<R, W>,   // COBS framed transport for ingress and egress data
88    session: Option<Session>, // Active encrypted session (if handshake completed)
89}
90
91impl<R: Read, W: Write> HostSide<R, W> {
92    /// Creates a new host side around a low level reader and writer. Reads block
93    /// per the transport's semantics, so a timeout for an unresponsive Ark must
94    /// be configured on the reader passed in.
95    pub fn new(reader: R, writer: W) -> Self {
96        Self {
97            framing: Framing::new(reader, writer),
98            session: None,
99        }
100    }
101
102    /// Sends a session reset and drives the encrypted handshake with the Ark:
103    ///
104    ///   1. Host -> Ark:  HostHello { host_signer, host_crypto }           (plain CBOR)
105    ///   2. Ark  -> Host: ArkHello  { ark_attest, ark_crypto, a2h_encap }  (cose::seal)
106    ///   3. Host -> Ark:  HostAck   { h2a_encap }                          (cose::seal)
107    ///
108    /// The verifier receives the raw device attestation from the Ark's hello and
109    /// its accepted info is returned once the session is established.
110    pub fn handshake<V: Verifier>(&mut self, verifier: &V) -> Result<V::Info, Error> {
111        self.session = None;
112
113        // Send two zero bytes: first terminates any interrupted message, second
114        // signals a fresh session.
115        self.framing.send_reset()?;
116
117        // Generate ephemeral host keys for this session
118        let host_xdsa_sk = xdsa::SecretKey::generate();
119        let host_xdsa_pk = host_xdsa_sk.public_key();
120        let host_xhpke_sk = xhpke::SecretKey::generate();
121        let host_xhpke_pk = host_xhpke_sk.public_key();
122
123        // Message 1: Send HostHello (plain CBOR, COBS-framed)
124        let hello = cbor::encode(&handshake::HostHello {
125            host_signer: host_xdsa_pk.clone(),
126            host_crypto: host_xhpke_pk.clone(),
127        })
128        .map_err(|err| Error::HandshakeFailed(format!("failed to encode host hello: {}", err)))?;
129
130        self.framing.send_packet(&hello)?;
131
132        // Message 2: Read ArkHello (COSE seal'd, COBS-framed). Frames the Ark
133        // emitted before processing the reset may still be queued, so skip
134        // everything not sealed to the fresh host key.
135        let host_xhpke_fp = host_xhpke_pk.fingerprint();
136
137        let mut stale = 0;
138        let size = loop {
139            // Empty frames are never sent by the Ark, they are stale junk too
140            let size = self.framing.next_packet()?.unwrap_or_default();
141            let recipient = cose::recipient(&self.framing.decobs_buffer[..size]);
142            if recipient.is_ok_and(|fp| fp == host_xhpke_fp) {
143                break size;
144            }
145            stale += 1;
146            if stale > MAX_STALE_FRAMES {
147                return Err(Error::HandshakeFailed(
148                    "too many stale frames before ark hello".into(),
149                ));
150            }
151            warn!("skipping stale frame during handshake");
152        };
153        let auth = handshake::ArkHelloAuth {
154            host_signer: host_xdsa_pk.clone(),
155            host_crypto: host_xhpke_pk.clone(),
156        };
157
158        // Step 2a: Decrypt the outer COSE_Encrypt0 layer
159        let sign1 = cose::decrypt(
160            &self.framing.decobs_buffer[..size],
161            &auth,
162            &host_xhpke_sk,
163            CRYPTO_DOMAIN_WIRE,
164        )
165        .map_err(|err| Error::HandshakeFailed(format!("failed to decrypt ark hello: {}", err)))?;
166
167        // Step 2b: Peek at the unverified payload to discover the Ark's identity
168        let unverified: handshake::ArkHello = cose::peek(&sign1)
169            .map_err(|err| Error::HandshakeFailed(format!("invalid ark hello payload: {}", err)))?;
170
171        // Step 2c: Hand the attestation to the verifier to obtain the Ark's
172        // identity key and the caller's session info
173        let attestation = Attestation::new(unverified.ark_attest)?;
174        let (ark_identity, info) = verifier
175            .verify(&attestation)
176            .map_err(Error::HandshakeFailed)?;
177
178        // Step 2d: Verify the COSE_Sign1 signature with the discovered identity
179        let ark_hello: handshake::ArkHello =
180            cose::verify(&sign1, &auth, &ark_identity, CRYPTO_DOMAIN_WIRE, None).map_err(
181                |err| Error::HandshakeFailed(format!("ark hello signature invalid: {}", err)),
182            )?;
183
184        // Set up the Ark->Host receiver context
185        let enc_a2h: [u8; xhpke::ENCAP_KEY_SIZE] = ark_hello
186            .a2h_encap
187            .try_into()
188            .map_err(|_| Error::HandshakeFailed("invalid a2h_encap size".into()))?;
189
190        let receiver = host_xhpke_sk
191            .new_receiver(&enc_a2h, CRYPTO_DOMAIN_WIRE_ARK_TO_HOST)
192            .map_err(|err| {
193                Error::HandshakeFailed(format!("host receiver setup failed: {}", err))
194            })?;
195
196        // Set up the Host->Ark sender context
197        let ark_xhpke_pk = ark_hello.ark_crypto;
198        let (sender, enc_h2a) = ark_xhpke_pk
199            .new_sender(CRYPTO_DOMAIN_WIRE_HOST_TO_ARK)
200            .map_err(|err| Error::HandshakeFailed(format!("host sender setup failed: {}", err)))?;
201
202        // Message 3: Send HostAck (COSE seal'd, COBS-framed)
203        let ack = cose::seal(
204            &handshake::HostAck {
205                h2a_encap: enc_h2a.to_vec(),
206            },
207            &handshake::HostAckAuth {
208                ark_signer: ark_identity,
209                ark_crypto: ark_xhpke_pk.clone(),
210            },
211            &host_xdsa_sk,
212            &ark_xhpke_pk,
213            CRYPTO_DOMAIN_WIRE,
214        )
215        .map_err(|err| Error::HandshakeFailed(format!("failed to seal host ack: {}", err)))?;
216
217        self.framing.send_packet(&ack)?;
218
219        // Session established
220        self.session = Some(Session { sender, receiver });
221        Ok(info)
222    }
223
224    /// Reads the next ark-to-host message, decrypting and protobuf decoding it.
225    /// A frame that cannot be decoded or a packet that cannot be decrypted
226    /// drops the session, as the Ark's HPKE sequence can no longer be followed;
227    /// only a fresh handshake recovers from that.
228    pub fn next_message(&mut self) -> Result<ArkToHost, Error> {
229        // Retrieve the next COBS encoded packet. A skipped frame may have
230        // carried a sealed message, so the session cannot continue past it.
231        // Empty frames are never sent by the Ark, so surface them as the
232        // decode failures they would have been.
233        let size = match self.framing.next_packet() {
234            Err(err) => {
235                self.session = None;
236                return Err(err);
237            }
238            Ok(None) => return Err(Error::FrameDecodingFailed(cobs::DecodeError::EmptyInput)),
239            Ok(Some(size)) => size,
240        };
241        // Decrypt the message and parse it with protobuf, dropping the session
242        // if the HPKE sequence cannot be followed anymore
243        let session = self
244            .session
245            .as_mut()
246            .ok_or_else(|| Error::EncryptionFailed("no active session".into()))?;
247
248        let res = match session.open(&self.framing.decobs_buffer[..size]) {
249            Err(err @ Error::EncryptionFailed(_)) => {
250                self.session = None;
251                return Err(err);
252            }
253            Err(err) => return Err(err),
254            Ok(res) => res,
255        };
256        trace!("read ark-to-host message ({} bytes encrypted)", size);
257        Ok(res)
258    }
259
260    /// Protobuf encodes a host-to-ark message, seals it with the session and
261    /// sends it. Fails without an active session, and a failure after sealing
262    /// drops the session, as the Ark's HPKE sequence can no longer be caught up
263    /// with.
264    pub fn send_message(&mut self, req: HostToArk) -> Result<(), Error> {
265        // Encode and seal the message, oversized messages are rejected before
266        // the HPKE sequence advances, only a failed seal breaks the session
267        let session = self
268            .session
269            .as_mut()
270            .ok_or_else(|| Error::EncryptionFailed("no active session".into()))?;
271
272        let blob = match session.seal(&req, &mut self.framing.encode_buffer) {
273            Err(err @ Error::EncryptionFailed(_)) => {
274                self.session = None;
275                return Err(err);
276            }
277            Err(err) => return Err(err),
278            Ok(blob) => blob,
279        };
280        // Send the sealed message, tearing down the session if the transport
281        // fails to deliver it
282        if let Err(err) = self.framing.send_packet(&blob) {
283            self.session = None;
284            return Err(err);
285        }
286        trace!("sent host-to-ark message ({} bytes)", blob.len());
287        Ok(())
288    }
289
290    /// Test and benchmark helper exposing the framer's `next_packet` with the
291    /// decoded packet as a slice. Not part of the API.
292    #[doc(hidden)]
293    #[inline]
294    #[cfg(any(test, feature = "bench", feature = "fuzz"))]
295    pub fn next_packet_blob(&mut self) -> Result<Option<&[u8]>, Error> {
296        self.framing.next_packet_blob()
297    }
298
299    /// Test and benchmark helper exposing the framer's `send_packet`. Not part
300    /// of the API.
301    #[doc(hidden)]
302    #[inline]
303    #[cfg(any(test, feature = "bench", feature = "fuzz"))]
304    pub fn send_packet_blob(&mut self, packet: &[u8]) -> Result<(), Error> {
305        self.framing.send_packet(packet)
306    }
307
308    /// Test and benchmark helper exposing the framer's `next_frame` with the raw
309    /// frame as a slice. Not part of the API.
310    #[doc(hidden)]
311    #[inline]
312    #[cfg(any(test, feature = "bench", feature = "fuzz"))]
313    pub fn next_frame_blob(&mut self) -> Result<&[u8], Error> {
314        self.framing.next_frame_blob()
315    }
316
317    /// Test and benchmark helper exposing the framer's `send_frame` with the raw
318    /// frame taken from a slice. Not part of the API.
319    #[doc(hidden)]
320    #[inline]
321    #[cfg(any(test, feature = "bench", feature = "fuzz"))]
322    pub fn send_frame_blob(&mut self, frame: &[u8]) -> Result<(), Error> {
323        self.framing.send_frame_blob(frame)
324    }
325}