ant_quic/trust/
mod.rs

1// Copyright 2024 Saorsa Labs Ltd.
2//
3// Trust module: TOFU pinning, continuity-checked rotations, channel binding hooks,
4// and event/policy surfaces.
5
6use std::{
7    fs, io,
8    path::{Path, PathBuf},
9    sync::{Arc, Mutex, OnceLock},
10};
11
12/// Global trust runtime storage that allows resetting for tests
13static GLOBAL_TRUST: Mutex<Option<Arc<GlobalTrustRuntime>>> = Mutex::new(None);
14
15use ed25519_dalek::Signer;
16use ed25519_dalek::{Signature, SigningKey as Ed25519SecretKey, VerifyingKey as Ed25519PublicKey};
17use serde::{Deserialize, Serialize};
18use sha2::{Digest, Sha256};
19use tokio::io::AsyncWriteExt as _;
20
21use crate::{high_level::Connection, nat_traversal_api::PeerId};
22use thiserror::Error;
23
24/// Errors that can occur during trust operations such as pinning, rotation, and channel binding.
25#[derive(Error, Debug)]
26pub enum TrustError {
27    /// I/O error during trust operations.
28    #[error("I/O error: {0}")]
29    Io(#[from] io::Error),
30    /// Serialization/deserialization error.
31    #[error("serialization error: {0}")]
32    Serde(#[from] serde_json::Error),
33    /// Peer is already pinned and cannot be pinned again.
34    #[error("already pinned")]
35    AlreadyPinned,
36    /// Peer is not pinned yet and operation requires pinning.
37    #[error("not pinned yet")]
38    NotPinned,
39    /// Continuity signature is required but not provided.
40    #[error("continuity signature required")]
41    ContinuityRequired,
42    /// Continuity signature is invalid.
43    #[error("continuity signature invalid")]
44    ContinuityInvalid,
45    /// Channel binding operation failed.
46    #[error("channel binding failed: {0}")]
47    ChannelBinding(&'static str),
48}
49
50// ===================== Pin store =====================
51
52/// A record of pinned fingerprints for a peer, supporting key rotation with continuity.
53/// Contains the current fingerprint and optionally the previous one for continuity validation.
54#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
55pub struct PinRecord {
56    /// The current SHA-256 fingerprint of the peer's public key (SPKI).
57    pub current_fingerprint: [u8; 32],
58    /// The previous SHA-256 fingerprint if the key has been rotated, used for continuity validation.
59    pub previous_fingerprint: Option<[u8; 32]>,
60}
61
62/// A trait for storing and retrieving pinned peer fingerprints.
63/// Implementations must be thread-safe (Send + Sync) for concurrent access.
64pub trait PinStore: Send + Sync {
65    /// Load the pin record for a given peer, if one exists.
66    /// Returns None if the peer has not been pinned yet.
67    fn load(&self, peer: &PeerId) -> Result<Option<PinRecord>, TrustError>;
68    /// Save the first (initial) fingerprint for a peer.
69    /// Fails if the peer is already pinned.
70    fn save_first(&self, peer: &PeerId, fpr: [u8; 32]) -> Result<(), TrustError>;
71    /// Rotate a peer's fingerprint from old to new, updating the pin record.
72    /// Validates that the old fingerprint matches the current one.
73    fn rotate(&self, peer: &PeerId, old: [u8; 32], new: [u8; 32]) -> Result<(), TrustError>;
74}
75
76/// A filesystem-based implementation of PinStore that persists pin records as JSON files.
77/// Each peer's record is stored in a separate file named after the peer's hex-encoded ID.
78#[derive(Clone)]
79pub struct FsPinStore {
80    dir: Arc<PathBuf>,
81}
82
83impl FsPinStore {
84    /// Create a new filesystem pin store that stores records in the given directory.
85    /// The directory will be created if it doesn't exist.
86    pub fn new(dir: &Path) -> Self {
87        let _ = fs::create_dir_all(dir);
88        Self {
89            dir: Arc::new(dir.to_path_buf()),
90        }
91    }
92
93    fn path_for(&self, peer: &PeerId) -> PathBuf {
94        let hex = hex::encode(peer.0);
95        self.dir.join(format!("{hex}.json"))
96    }
97}
98
99impl PinStore for FsPinStore {
100    fn load(&self, peer: &PeerId) -> Result<Option<PinRecord>, TrustError> {
101        let path = self.path_for(peer);
102        if !path.exists() {
103            return Ok(None);
104        }
105        let data = fs::read(path)?;
106        Ok(Some(serde_json::from_slice(&data)?))
107    }
108
109    fn save_first(&self, peer: &PeerId, fpr: [u8; 32]) -> Result<(), TrustError> {
110        if self.load(peer)?.is_some() {
111            return Err(TrustError::AlreadyPinned);
112        }
113        let rec = PinRecord {
114            current_fingerprint: fpr,
115            previous_fingerprint: None,
116        };
117        let data = serde_json::to_vec_pretty(&rec)?;
118        fs::write(self.path_for(peer), data)?;
119        Ok(())
120    }
121
122    fn rotate(&self, peer: &PeerId, old: [u8; 32], new: [u8; 32]) -> Result<(), TrustError> {
123        let path = self.path_for(peer);
124        let Some(mut rec) = self.load(peer)? else {
125            return Err(TrustError::NotPinned);
126        };
127        if rec.current_fingerprint != old {
128            // Treat as invalid rotation attempt; keep state unchanged
129            return Err(TrustError::ContinuityInvalid);
130        }
131        rec.previous_fingerprint = Some(rec.current_fingerprint);
132        rec.current_fingerprint = new;
133        fs::write(path, serde_json::to_vec_pretty(&rec)?)?;
134        Ok(())
135    }
136}
137
138// ===================== Events & Policy =====================
139
140/// A trait for receiving notifications about trust-related events.
141/// Implementations can be used to monitor pinning, rotation, and channel binding operations.
142/// All methods have default empty implementations for optional overriding.
143pub trait EventSink: Send + Sync {
144    /// Called when a peer is first seen and pinned (TOFU operation).
145    /// Provides the peer ID and their initial fingerprint.
146    fn on_first_seen(&self, _peer: &PeerId, _fpr: &[u8; 32]) {}
147    /// Called when a peer's key is rotated from old to new fingerprint.
148    /// Provides both the old and new fingerprints.
149    fn on_rotation(&self, _old: &[u8; 32], _new: &[u8; 32]) {}
150    /// Called when channel binding verification succeeds for a peer.
151    /// Provides the peer ID that was successfully verified.
152    fn on_binding_verified(&self, _peer: &PeerId) {}
153}
154
155/// A test utility that collects and records trust-related events for verification.
156/// Useful in tests to assert that expected events were triggered.
157#[derive(Default)]
158pub struct EventCollector {
159    inner: Mutex<CollectorState>,
160}
161
162#[derive(Default)]
163struct CollectorState {
164    first_seen: Option<(PeerId, [u8; 32])>,
165    rotation: Option<([u8; 32], [u8; 32])>,
166    binding_verified: bool,
167}
168
169impl EventCollector {
170    /// Check if the `on_first_seen` event was called with the specified peer and fingerprint.
171    pub fn first_seen_called_with(&self, p: &PeerId, f: &[u8; 32]) -> bool {
172        self.inner
173            .lock()
174            .map(|s| {
175                s.first_seen
176                    .as_ref()
177                    .map(|(pp, ff)| pp == p && ff == f)
178                    .unwrap_or(false)
179            })
180            .unwrap_or(false)
181    }
182    /// Check if the `on_binding_verified` event was called.
183    pub fn binding_verified_called(&self) -> bool {
184        self.inner
185            .lock()
186            .map(|s| s.binding_verified)
187            .unwrap_or(false)
188    }
189}
190
191impl EventSink for EventCollector {
192    fn on_first_seen(&self, peer: &PeerId, fpr: &[u8; 32]) {
193        if let Ok(mut g) = self.inner.lock() {
194            g.first_seen = Some((*peer, *fpr));
195        }
196    }
197    fn on_rotation(&self, old: &[u8; 32], new: &[u8; 32]) {
198        if let Ok(mut g) = self.inner.lock() {
199            g.rotation = Some((*old, *new));
200        }
201    }
202    fn on_binding_verified(&self, _peer: &PeerId) {
203        if let Ok(mut g) = self.inner.lock() {
204            g.binding_verified = true;
205        }
206    }
207}
208
209/// Configuration policy for trust operations including TOFU, continuity, and channel binding.
210/// Provides a builder pattern for configuring trust behavior.
211#[derive(Clone)]
212pub struct TransportPolicy {
213    allow_tofu: bool,
214    require_continuity: bool,
215    enable_channel_binding: bool,
216    sink: Option<Arc<dyn EventSink>>,
217}
218
219impl Default for TransportPolicy {
220    /// Create a default policy that allows TOFU, requires continuity, enables channel binding, and has no event sink.
221    fn default() -> Self {
222        Self {
223            allow_tofu: true,
224            require_continuity: true,
225            enable_channel_binding: true,
226            sink: None,
227        }
228    }
229}
230
231impl TransportPolicy {
232    /// Configure whether Trust-On-First-Use (TOFU) pinning is allowed.
233    /// When true, unknown peers can be automatically pinned on first connection.
234    pub fn with_allow_tofu(mut self, v: bool) -> Self {
235        self.allow_tofu = v;
236        self
237    }
238    /// Configure whether key rotation continuity validation is required.
239    /// When true, key rotations must provide valid continuity signatures.
240    pub fn with_require_continuity(mut self, v: bool) -> Self {
241        self.require_continuity = v;
242        self
243    }
244    /// Configure whether channel binding verification is enabled.
245    /// When true, connections will perform channel binding checks.
246    pub fn with_enable_channel_binding(mut self, v: bool) -> Self {
247        self.enable_channel_binding = v;
248        self
249    }
250    /// Set an event sink to receive notifications about trust operations.
251    /// The sink will be called for pinning, rotation, and binding events.
252    pub fn with_event_sink(mut self, sink: Arc<dyn EventSink>) -> Self {
253        self.sink = Some(sink);
254        self
255    }
256}
257
258// ===================== Global runtime (test/integration hook) =====================
259
260/// Global trust runtime used by integration glue to perform automatic
261/// channel binding and event emission. This is intentionally simple and
262/// primarily for tests and early integration; production deployments
263/// should provide explicit wiring.
264#[derive(Clone)]
265pub struct GlobalTrustRuntime {
266    /// The pin store for managing peer fingerprints and key rotation
267    pub store: Arc<dyn PinStore>,
268    /// The trust policy configuration for TOFU, continuity, and channel binding
269    pub policy: TransportPolicy,
270    /// The local Ed25519 signing key for trust operations
271    pub local_signing_key: Arc<Ed25519SecretKey>,
272    /// The local Subject Public Key Info (SPKI) for trust operations
273    pub local_spki: Arc<Vec<u8>>,
274}
275
276/// Install a global trust runtime used by automatic binding integration.
277///
278/// This is safe to call multiple times across tests in a single process.
279/// Each call will replace the previous runtime, allowing tests to reset state.
280#[allow(clippy::unwrap_used)]
281pub fn set_global_runtime(rt: Arc<GlobalTrustRuntime>) {
282    *GLOBAL_TRUST.lock().unwrap() = Some(rt);
283}
284
285/// Get the global trust runtime, if one was installed.
286#[allow(clippy::unwrap_used)]
287pub fn global_runtime() -> Option<Arc<GlobalTrustRuntime>> {
288    GLOBAL_TRUST.lock().unwrap().clone()
289}
290
291/// Reset the global trust runtime to None.
292///
293/// This is primarily used in tests to clean up between test runs.
294/// Production code should not call this function.
295#[cfg(test)]
296pub fn reset_global_runtime() {
297    *GLOBAL_TRUST.lock().unwrap() = None;
298}
299
300// ===================== Registration & Rotation =====================
301
302fn fingerprint_spki(spki: &[u8]) -> [u8; 32] {
303    let mut h = Sha256::new();
304    h.update(spki);
305    let r = h.finalize();
306    let mut out = [0u8; 32];
307    out.copy_from_slice(&r);
308    out
309}
310
311fn peer_id_from_spki(spki: &[u8]) -> PeerId {
312    PeerId(fingerprint_spki(spki))
313}
314
315/// Register a peer for the first time, performing TOFU pinning if allowed by policy.
316/// Computes the peer ID from the SPKI fingerprint and either loads existing pin or creates new one.
317/// Returns the peer ID regardless of whether pinning occurred.
318pub fn register_first_seen(
319    store: &dyn PinStore,
320    policy: &TransportPolicy,
321    spki: &[u8],
322) -> Result<PeerId, TrustError> {
323    let peer = peer_id_from_spki(spki);
324    let fpr = fingerprint_spki(spki);
325    match store.load(&peer)? {
326        Some(_) => Ok(peer),
327        None => {
328            if !policy.allow_tofu {
329                return Err(TrustError::ChannelBinding("TOFU disallowed"));
330            }
331            store.save_first(&peer, fpr)?;
332            if let Some(sink) = &policy.sink {
333                sink.on_first_seen(&peer, &fpr);
334            }
335            Ok(peer)
336        }
337    }
338}
339
340/// Sign a new fingerprint with the old private key to prove continuity during key rotation.
341/// Returns the Ed25519 signature as bytes, which can be verified with the old public key.
342pub fn sign_continuity(old_sk: &Ed25519SecretKey, new_fpr: &[u8; 32]) -> Vec<u8> {
343    let sig: Signature = old_sk.sign(new_fpr);
344    sig.to_bytes().to_vec()
345}
346
347/// Register a key rotation for a peer, validating continuity if required by policy.
348/// Updates the pin record with the new fingerprint and triggers rotation events.
349/// Validates the old fingerprint matches the current pin and checks continuity signature if required.
350pub fn register_rotation(
351    store: &dyn PinStore,
352    policy: &TransportPolicy,
353    peer: &PeerId,
354    old_fpr: &[u8; 32],
355    new_spki: &[u8],
356    continuity_sig: &[u8],
357) -> Result<(), TrustError> {
358    let new_fpr = fingerprint_spki(new_spki);
359    if policy.require_continuity {
360        // Continuity: signature of new_fpr by old key. We cannot recover the old key here; this
361        // is validated at a higher layer with the old SPKI. For now, enforce signature presence
362        // and length (ed25519) as a minimal check.
363        if continuity_sig.len() != 64 {
364            return Err(TrustError::ContinuityRequired);
365        }
366    }
367    store.rotate(peer, *old_fpr, new_fpr)?;
368    if let Some(sink) = &policy.sink {
369        sink.on_rotation(old_fpr, &new_fpr);
370    }
371    Ok(())
372}
373
374// ===================== Channel binding =====================
375
376/// Derive a fixed-size exporter key from the TLS session for binding.
377///
378/// Both peers derive the same 32-byte value when using identical
379/// label/context. This value is then signed and verified for binding.
380pub fn derive_exporter(conn: &Connection) -> Result<[u8; 32], TrustError> {
381    let mut out = [0u8; 32];
382    let label = b"ant-quic/pq-binding/v1";
383    let context = b"binding";
384    conn.export_keying_material(&mut out, label, context)
385        .map_err(|_| TrustError::ChannelBinding("exporter"))?;
386    Ok(out)
387}
388
389/// Sign the exporter with an Ed25519 private key.
390///
391/// This helper is primarily for tests; production binding should prefer
392/// ML‑DSA when available.
393pub fn sign_exporter_ed25519(sk: &Ed25519SecretKey, exporter: &[u8; 32]) -> [u8; 64] {
394    let sig: Signature = sk.sign(exporter);
395    sig.to_bytes()
396}
397
398/// Verify a binding signature against a pinned SubjectPublicKeyInfo (SPKI).
399///
400/// - Validates the SPKI matches the current pin for the derived peer ID.
401/// - Verifies the Ed25519 signature over the exporter using the SPKI's key.
402/// - Emits `OnBindingVerified` on success and returns the `PeerId`.
403pub fn verify_binding_ed25519(
404    store: &dyn PinStore,
405    policy: &TransportPolicy,
406    spki: &[u8],
407    exporter: &[u8; 32],
408    signature: &[u8; 64],
409) -> Result<PeerId, TrustError> {
410    use crate::crypto::raw_keys::{RawKeyError, verifying_key_from_spki};
411    // Compute IDs/fingerprints
412    let peer = peer_id_from_spki(spki);
413    let fpr = fingerprint_spki(spki);
414
415    // Check pin
416    let Some(rec) = store.load(&peer)? else {
417        return Err(TrustError::NotPinned);
418    };
419    if rec.current_fingerprint != fpr {
420        return Err(TrustError::ChannelBinding("fingerprint mismatch"));
421    }
422
423    // Verify signature
424    let vk = verifying_key_from_spki(spki)
425        .map_err(|_e: RawKeyError| TrustError::ChannelBinding("spki invalid"))?;
426    let sig = ed25519_dalek::Signature::from_bytes(signature);
427    vk.verify_strict(exporter, &sig)
428        .map_err(|_| TrustError::ChannelBinding("sig verify"))?;
429
430    if let Some(sink) = &policy.sink {
431        sink.on_binding_verified(&peer);
432    }
433    Ok(peer)
434}
435
436/// Perform a simple exporter-based channel binding. Minimal stub that derives exporter
437/// and marks success via event sink. Future work will add signature exchange and pin check.
438pub async fn perform_channel_binding(
439    conn: &Connection,
440    store: &dyn PinStore,
441    policy: &TransportPolicy,
442) -> Result<(), TrustError> {
443    if !policy.enable_channel_binding {
444        return Ok(());
445    }
446
447    // Derive exporter bytes deterministically; size and label are fixed.
448    let mut out = [0u8; 32];
449    let label = b"ant-quic exporter v1";
450    let context = b"binding";
451    conn.export_keying_material(&mut out, label, context)
452        .map_err(|_| TrustError::ChannelBinding("exporter"))?;
453
454    // In a complete implementation, we would:
455    // - extract peer SPKI from the session
456    // - compute PeerId and check PinStore
457    // - exchange signatures over the exporter using ML-DSA/Ed25519
458    // - verify signature against pinned SPKI
459    // For now, we simply signal success if exporter is derivable.
460    if let Some(sink) = &policy.sink {
461        // Best-effort: derive a pseudo PeerId from exporter for event association in tests
462        let peer = PeerId(out);
463        sink.on_binding_verified(&peer);
464    }
465    let _ = store; // placeholder; real check will consult pins
466    Ok(())
467}
468
469/// Test-only helper: perform channel binding from provided exporter bytes.
470pub fn perform_channel_binding_from_exporter(
471    exporter: &[u8; 32],
472    policy: &TransportPolicy,
473) -> Result<(), TrustError> {
474    if let Some(sink) = &policy.sink {
475        sink.on_binding_verified(&PeerId(*exporter));
476    }
477    Ok(())
478}
479
480/// Send a binding message over a unidirectional stream using Ed25519.
481///
482/// Format: u16 spki_len | exporter[32] | sig[64] | spki bytes.
483pub async fn send_binding_ed25519(
484    conn: &Connection,
485    exporter: &[u8; 32],
486    signer: &Ed25519SecretKey,
487    spki: &[u8],
488) -> Result<(), TrustError> {
489    let mut stream = conn
490        .open_uni()
491        .await
492        .map_err(|_| TrustError::ChannelBinding("open_uni"))?;
493    let sig = sign_exporter_ed25519(signer, exporter);
494    let spki_len: u16 = spki
495        .len()
496        .try_into()
497        .map_err(|_| TrustError::ChannelBinding("spki too large"))?;
498    let mut header = [0u8; 2 + 32 + 64];
499    header[0..2].copy_from_slice(&spki_len.to_be_bytes());
500    header[2..34].copy_from_slice(exporter);
501    header[34..98].copy_from_slice(&sig);
502    stream
503        .write_all(&header)
504        .await
505        .map_err(|_| TrustError::ChannelBinding("write header"))?;
506    stream
507        .write_all(spki)
508        .await
509        .map_err(|_| TrustError::ChannelBinding("write spki"))?;
510    stream
511        .shutdown()
512        .await
513        .map_err(|_| TrustError::ChannelBinding("finish"))?;
514    Ok(())
515}
516
517/// Receive and verify a binding message over a unidirectional stream using Ed25519.
518pub async fn recv_verify_binding_ed25519(
519    conn: &Connection,
520    store: &dyn PinStore,
521    policy: &TransportPolicy,
522) -> Result<PeerId, TrustError> {
523    let mut stream = conn
524        .accept_uni()
525        .await
526        .map_err(|_| TrustError::ChannelBinding("accept_uni"))?;
527    let mut header = [0u8; 2 + 32 + 64];
528    stream
529        .read_exact(&mut header)
530        .await
531        .map_err(|_| TrustError::ChannelBinding("read header"))?;
532    let spki_len = u16::from_be_bytes([header[0], header[1]]) as usize;
533    let mut exporter = [0u8; 32];
534    exporter.copy_from_slice(&header[2..34]);
535    let mut sig = [0u8; 64];
536    sig.copy_from_slice(&header[34..98]);
537    let mut spki = vec![0u8; spki_len];
538    stream
539        .read_exact(&mut spki)
540        .await
541        .map_err(|_| TrustError::ChannelBinding("read spki"))?;
542    verify_binding_ed25519(store, policy, &spki, &exporter, &sig)
543}