Skip to main content

clawft_kernel/licence/
approval_store.rs

1//! [`ApprovalStore`]: the operator hash approvals this node holds.
2//!
3//! Additive and content-addressed (no `seq`): a duplicate is idempotent and a
4//! new approval never replaces an older one. Persisted atomically, 0600,
5//! size-capped, fail-closed. An approval whose `mesh_id` is not the local one
6//! (a `mesh_nonce` change) stays on disk but is inactive and listed by
7//! [`ApprovalStore::orphaned`].
8
9use std::collections::BTreeMap;
10use std::path::{Path, PathBuf};
11use std::sync::{Arc, Mutex};
12
13use serde::{Deserialize, Serialize};
14
15use super::approval::{Approval, SignedApproval, verify_approval, verify_approval_signature};
16use super::persist::{read_capped, write_atomic};
17use super::{LicenceError, LocalMeshId, MAX_APPROVALS, Outcome};
18use crate::workload_pkg::TrustAnchors;
19
20/// File name inside the store directory.
21pub const APPROVALS_FILE: &str = "checkout_approvals.json";
22
23#[derive(Serialize, Deserialize)]
24#[serde(deny_unknown_fields)]
25struct ApprovalsFile {
26    v: u32,
27    approvals: Vec<SignedApproval>,
28}
29
30/// Held approvals by content key, with the verified body.
31pub(super) type Held = BTreeMap<String, (SignedApproval, Approval)>;
32
33/// Persisted operator hash approvals of one node.
34pub struct ApprovalStore {
35    path: PathBuf,
36    anchors: Arc<TrustAnchors>,
37    pub(super) local: LocalMeshId,
38    inner: Mutex<(Held, Option<String>)>,
39}
40
41impl ApprovalStore {
42    /// Open the store in `dir`. A missing file is an empty store; a bad file
43    /// is an error and nothing is written.
44    pub fn open(
45        dir: &Path,
46        anchors: Arc<TrustAnchors>,
47        local: LocalMeshId,
48    ) -> Result<Self, LicenceError> {
49        let path = dir.join(APPROVALS_FILE);
50        let held = load(&path, &anchors)?;
51        Ok(Self { path, anchors, local, inner: Mutex::new((held, None)) })
52    }
53
54    /// [`Self::open`], but a bad file gives a poisoned store: it has no
55    /// active approvals, refuses writes and leaves the file alone.
56    pub fn open_or_poisoned(dir: &Path, anchors: Arc<TrustAnchors>, local: LocalMeshId) -> Self {
57        let path = dir.join(APPROVALS_FILE);
58        let (held, poison) = match load(&path, &anchors) {
59            Ok(h) => (h, None),
60            Err(e) => (Held::new(), Some(e.to_string())),
61        };
62        Self { path, anchors, local, inner: Mutex::new((held, poison)) }
63    }
64
65    pub(super) fn lock(&self) -> std::sync::MutexGuard<'_, (Held, Option<String>)> {
66        self.inner.lock().unwrap_or_else(|p| p.into_inner())
67    }
68
69    /// Why the store is poisoned, if it is.
70    pub fn poisoned(&self) -> Option<String> {
71        self.lock().1.clone()
72    }
73
74    /// Accept an approval: pinned operator signature, `approval.mesh_id`
75    /// equal to the local mesh id. A repeat is [`Outcome::Duplicate`].
76    pub fn accept(&self, signed: &SignedApproval) -> Result<Outcome, LicenceError> {
77        let mut g = self.lock();
78        if let Some(p) = &g.1 {
79            return Err(LicenceError::Poisoned(p.clone()));
80        }
81        let local = self.local.get().ok_or(LicenceError::NoLocalMesh)?;
82        let approval = verify_approval(signed, &self.anchors, &local)?;
83        let key = approval.content_key();
84        if g.0.contains_key(&key) {
85            return Ok(Outcome::Duplicate);
86        }
87        if g.0.len() >= MAX_APPROVALS {
88            return Err(LicenceError::Full);
89        }
90        g.0.insert(key, (signed.clone(), approval));
91        save(&self.path, &g.0)?;
92        Ok(Outcome::Applied)
93    }
94
95    /// The content key of an active approval for `cog_id` `version` that
96    /// lists `sha256`.
97    pub fn covering(&self, cog_id: &str, version: &str, sha256: &str) -> Option<String> {
98        let g = self.lock();
99        let local = self.local.get()?.to_hex();
100        if g.1.is_some() {
101            return None;
102        }
103        g.0.iter()
104            .find(|(_, (_, a))| {
105                a.mesh_id == local && a.cog_id == cog_id && a.version == version && a.covers(sha256)
106            })
107            .map(|(k, _)| k.clone())
108    }
109
110    /// Content keys of approvals for another mesh id (`weaver doctor`).
111    pub fn orphaned(&self) -> Vec<String> {
112        let g = self.lock();
113        let local = self.local.get().map(|m| m.to_hex());
114        g.0.iter()
115            .filter(|(_, (_, a))| Some(&a.mesh_id) != local.as_ref())
116            .map(|(k, _)| k.clone())
117            .collect()
118    }
119
120    /// Number of approvals held, active or not.
121    pub fn len(&self) -> usize {
122        self.lock().0.len()
123    }
124
125    /// True when no approval is held.
126    pub fn is_empty(&self) -> bool {
127        self.len() == 0
128    }
129}
130
131fn save(path: &Path, held: &Held) -> Result<(), LicenceError> {
132    let file = ApprovalsFile { v: 1, approvals: held.values().map(|(s, _)| s.clone()).collect() };
133    let bytes = serde_json::to_vec(&file).map_err(|e| LicenceError::Persist(e.to_string()))?;
134    write_atomic(path, &bytes)
135}
136
137fn load(path: &Path, anchors: &TrustAnchors) -> Result<Held, LicenceError> {
138    let Some(bytes) = read_capped(path)? else {
139        return Ok(Held::new());
140    };
141    let file: ApprovalsFile =
142        serde_json::from_slice(&bytes).map_err(|e| LicenceError::Malformed(e.to_string()))?;
143    if file.v != 1 || file.approvals.len() > MAX_APPROVALS {
144        return Err(LicenceError::Malformed("approvals version or size".into()));
145    }
146    let mut held = Held::new();
147    for s in file.approvals {
148        let a = verify_approval_signature(&s, anchors)?;
149        held.insert(a.content_key(), (s, a));
150    }
151    Ok(held)
152}