Skip to main content

vta_support/
sealed_nonce_store.rs

1//! Persistent backing for the sealed-transfer `NonceStore` trait.
2//!
3//! Stores one row per sealed `bundle_id` under the `sealed-nonce:<hex>` key
4//! prefix in a dedicated keyspace, so repeated seals of the same request
5//! (typically: the operator re-running `vta bootstrap seal` after a network
6//! glitch, or a replayed Mode A / Mode B request) are rejected with
7//! [`SealedTransferError::NonceReplay`].
8//!
9//! Mode A token consumption and Mode B carve-out sentinels already prevent
10//! cross-restart replay at the policy layer — this store is belt-and-
11//! suspenders for anyone who calls `seal_payload` directly and a meaningful
12//! guard for the Mode C offline CLI where there is no other anti-replay state.
13
14use std::future::Future;
15use std::pin::Pin;
16
17use tokio::sync::Mutex;
18use vta_sdk::sealed_transfer::{NonceStore, SealedTransferError};
19
20use vti_common::store::KeyspaceHandle;
21
22const KEY_PREFIX: &str = "sealed-nonce:";
23
24/// Fjall-backed (or vsock-backed) persistent nonce store. Any
25/// [`KeyspaceHandle`] will do. The bundle_id lives in the (plaintext)
26/// key; encrypted deployments now also encrypt the row value, binding it
27/// to its `(keyspace, key)` location via AAD so a hostile store operator
28/// cannot relocate or substitute a nonce record (P0.1). Anti-rollback of
29/// the whole keyspace is a separate concern (P0.2).
30pub struct PersistentNonceStore {
31    ks: KeyspaceHandle,
32    /// Serialises the check-and-record critical section across threads so
33    /// two concurrent `seal_payload` calls with the same bundle_id cannot
34    /// both pass the absence check. Callers hit the store very rarely
35    /// (at most a few bootstrap flows per minute) so a mutex is fine.
36    lock: Mutex<()>,
37}
38
39impl PersistentNonceStore {
40    pub fn new(ks: KeyspaceHandle) -> Self {
41        Self {
42            ks,
43            lock: Mutex::new(()),
44        }
45    }
46
47    fn key(bundle_id: &[u8; 16]) -> String {
48        let mut s = String::with_capacity(KEY_PREFIX.len() + 32);
49        s.push_str(KEY_PREFIX);
50        const T: &[u8; 16] = b"0123456789abcdef";
51        for &b in bundle_id {
52            s.push(T[(b >> 4) as usize] as char);
53            s.push(T[(b & 0xf) as usize] as char);
54        }
55        s
56    }
57}
58
59impl NonceStore for PersistentNonceStore {
60    fn check_and_record<'a>(
61        &'a self,
62        bundle_id: &'a [u8; 16],
63    ) -> Pin<Box<dyn Future<Output = Result<(), SealedTransferError>> + Send + 'a>> {
64        let key = Self::key(bundle_id);
65        Box::pin(async move {
66            let _guard = self.lock.lock().await;
67            if self
68                .ks
69                .get_raw(key.clone())
70                .await
71                .map_err(|e| SealedTransferError::NonceStore(e.to_string()))?
72                .is_some()
73            {
74                return Err(SealedTransferError::NonceReplay);
75            }
76            self.ks
77                .insert_raw(key, b"1".to_vec())
78                .await
79                .map_err(|e| SealedTransferError::NonceStore(e.to_string()))?;
80            Ok(())
81        })
82    }
83}