1use std::collections::BTreeMap;
7use std::path::{Path, PathBuf};
8use std::sync::{Arc, Mutex, RwLock};
9
10use super::floor::FloorState;
11use super::persist::write_atomic;
12use super::store_load::{SlotFile, StoreFile, load};
13use super::{
14 BindState, BindingRecord, CheckoutGrant, Clock, GRANT_SKEW_SECS, GrantArtifact, LicenceError,
15 LicenceEvent, LicenceEventSink, LocalMeshId, NoopSink, Outcome, SignedEnvelope,
16};
17use crate::revocation::{RevocationKind, RevocationList};
18use crate::workload_pkg::TrustAnchors;
19
20pub const GRANTS_FILE: &str = "checkout_grants.json";
22const HW_PERSIST_STEP: u64 = 60;
24
25#[derive(Clone)]
26pub(super) struct Held<T> {
27 pub(super) signed: SignedEnvelope,
28 pub(super) body: T,
29}
30
31#[derive(Default, Clone)]
33pub(super) struct Slot {
34 pub(super) current: Option<Held<CheckoutGrant>>,
36 pub(super) previous: Option<Held<CheckoutGrant>>,
38 pub(super) conflicted: Vec<u64>,
40}
41
42#[derive(Default, Clone)]
43pub(super) struct Inner {
44 pub(super) binding: Option<Held<BindingRecord>>,
45 pub(super) slots: BTreeMap<(String, String), Slot>,
46 pub(super) floors: BTreeMap<String, FloorState>,
47 pub(super) persisted_hw: u64,
48 pub(super) orphan_reported: bool,
49 pub(super) dirty: bool,
51 pub(super) poisoned: Option<String>,
52}
53
54#[derive(Debug, Clone)]
58pub struct VerifiedCheckoutGrant {
59 grant: CheckoutGrant,
60 grant_pubkey_hex: String,
61}
62
63impl VerifiedCheckoutGrant {
64 #[cfg(test)]
66 pub(super) fn for_test(grant: CheckoutGrant) -> Self {
67 let pk = super::tests_common::pk_hex(&super::tests_common::grant_key());
68 Self { grant, grant_pubkey_hex: pk }
69 }
70
71 pub fn grant(&self) -> &CheckoutGrant {
73 &self.grant
74 }
75
76 pub fn grant_pubkey_hex(&self) -> &str {
78 &self.grant_pubkey_hex
79 }
80}
81
82pub struct CheckoutGrantStore {
84 pub(super) path: PathBuf,
85 pub(super) anchors: Arc<TrustAnchors>,
86 pub(super) local: LocalMeshId,
87 pub(super) clock: Clock,
88 pub(super) sink: RwLock<Arc<dyn LicenceEventSink>>,
89 pub(super) revocations: RwLock<Option<Arc<RevocationList>>>,
90 pub(super) inner: Mutex<Inner>,
91}
92
93impl std::fmt::Debug for CheckoutGrantStore {
94 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95 f.debug_struct("CheckoutGrantStore").finish_non_exhaustive()
96 }
97}
98
99pub(super) fn grant_valid(g: &CheckoutGrant, eff_now: u64) -> bool {
100 !g.is_withdrawal()
101 && eff_now < g.expires_at
102 && eff_now < g.licence.expires
103 && g.issued_at <= eff_now.saturating_add(GRANT_SKEW_SECS)
104}
105
106impl CheckoutGrantStore {
107 pub fn open(
110 dir: &Path,
111 anchors: Arc<TrustAnchors>,
112 local: LocalMeshId,
113 clock: Clock,
114 ) -> Result<Self, LicenceError> {
115 let path = dir.join(GRANTS_FILE);
116 let inner = load(&path, &anchors)?;
117 Ok(Self::build(path, anchors, local, clock, inner))
118 }
119
120 pub fn open_or_poisoned(
124 dir: &Path,
125 anchors: Arc<TrustAnchors>,
126 local: LocalMeshId,
127 clock: Clock,
128 ) -> Self {
129 let path = dir.join(GRANTS_FILE);
130 let inner = load(&path, &anchors).unwrap_or_else(|e| Inner {
131 poisoned: Some(e.to_string()),
132 ..Inner::default()
133 });
134 Self::build(path, anchors, local, clock, inner)
135 }
136
137 fn build(
138 path: PathBuf,
139 anchors: Arc<TrustAnchors>,
140 local: LocalMeshId,
141 clock: Clock,
142 inner: Inner,
143 ) -> Self {
144 Self {
145 path,
146 anchors,
147 local,
148 clock,
149 sink: RwLock::new(Arc::new(NoopSink)),
150 revocations: RwLock::new(None),
151 inner: Mutex::new(inner),
152 }
153 }
154
155 pub fn set_sink(&self, sink: Arc<dyn LicenceEventSink>) {
157 *self.sink.write().unwrap_or_else(|p| p.into_inner()) = sink;
158 }
159
160 pub fn attach_revocations(&self, list: Arc<RevocationList>) {
162 *self.revocations.write().unwrap_or_else(|p| p.into_inner()) = Some(list);
163 }
164
165 pub fn local_mesh_id(&self) -> &LocalMeshId {
167 &self.local
168 }
169
170 pub fn poisoned(&self) -> Option<String> {
172 self.lock().poisoned.clone()
173 }
174
175 pub(super) fn lock(&self) -> std::sync::MutexGuard<'_, Inner> {
176 self.inner.lock().unwrap_or_else(|p| p.into_inner())
177 }
178
179 fn emit(&self, ev: LicenceEvent) {
180 self.sink.read().unwrap_or_else(|p| p.into_inner()).emit(ev);
181 }
182
183 pub(super) fn run<T>(&self, f: impl FnOnce(&mut Inner, &mut Vec<LicenceEvent>) -> T) -> T {
187 let mut events = Vec::new();
188 let out = {
189 let mut g = self.lock();
190 f(&mut g, &mut events)
191 };
192 for e in events {
193 self.emit(e);
194 }
195 out
196 }
197
198 pub(super) fn key_revoked(&self, pk_hex: &str) -> bool {
199 let g = self.revocations.read().unwrap_or_else(|p| p.into_inner());
200 g.as_ref()
201 .is_some_and(|l| l.is_subject_revoked(RevocationKind::SignerKey, pk_hex))
202 }
203
204 pub fn is_hash_revoked(&self, blake3_hex: &str) -> bool {
206 let g = self.revocations.read().unwrap_or_else(|p| p.into_inner());
207 g.as_ref()
208 .is_some_and(|l| l.is_subject_revoked(RevocationKind::ArtifactHash, blake3_hex))
209 }
210
211 fn save(&self, inner: &mut Inner) -> Result<(), LicenceError> {
212 let file = StoreFile {
213 v: 1,
214 binding: inner.binding.as_ref().map(|b| b.signed.clone()),
215 grants: inner
216 .slots
217 .iter()
218 .map(|((cog_id, version), s)| SlotFile {
219 cog_id: cog_id.clone(),
220 version: version.clone(),
221 current: s.current.as_ref().map(|h| h.signed.clone()),
222 previous: s.previous.as_ref().map(|h| h.signed.clone()),
223 conflicted: s.conflicted.clone(),
224 })
225 .collect(),
226 floors: inner.floors.clone(),
227 };
228 let bytes =
229 serde_json::to_vec(&file).map_err(|e| LicenceError::Persist(e.to_string()))?;
230 write_atomic(&self.path, &bytes)?;
231 inner.persisted_hw = inner.floors.values().map(|f| f.hw).max().unwrap_or(0);
232 inner.dirty = false;
233 Ok(())
234 }
235
236 pub(super) fn commit(&self, inner: &mut Inner, mut next: Inner) -> Result<(), LicenceError> {
239 self.save(&mut next)?;
240 *inner = next;
241 Ok(())
242 }
243
244 pub(super) fn commit_restrictive(&self, inner: &mut Inner, mut next: Inner) -> Outcome {
249 let saved = self.save(&mut next);
250 next.dirty = saved.is_err();
251 *inner = next;
252 match saved {
253 Ok(()) => Outcome::Applied,
254 Err(e) => {
255 tracing::warn!(error = %e, "restrictive licence record applied but not saved; tick retries");
256 Outcome::AppliedUnsaved
257 }
258 }
259 }
260
261 pub(super) fn binding_in_effect(
265 &self,
266 inner: &mut Inner,
267 ev: &mut Vec<LicenceEvent>,
268 ) -> Result<BindingRecord, LicenceError> {
269 if let Some(p) = &inner.poisoned {
270 return Err(LicenceError::Poisoned(p.clone()));
271 }
272 let local = self.local.get().ok_or(LicenceError::NoLocalMesh)?;
273 let b = inner.binding.as_ref().ok_or(LicenceError::NoBinding)?.body.clone();
274 if b.mesh_id != local.to_hex() {
275 if !inner.orphan_reported {
276 inner.orphan_reported = true;
277 ev.push(LicenceEvent::BindingOrphaned {
278 stored: b.mesh_id.clone(),
279 local: local.to_hex(),
280 });
281 }
282 return Err(LicenceError::Orphaned);
283 }
284 inner.orphan_reported = false;
285 if b.state != BindState::Bound {
286 return Err(LicenceError::Unbound);
287 }
288 Ok(b)
289 }
290
291 pub(super) fn eff_now(&self, inner: &mut Inner, key: &str) -> u64 {
294 let now = (self.clock)();
295 let fs = inner.floors.entry(key.to_owned()).or_default();
296 fs.observe(now);
297 fs.effective_now(now)
298 }
299
300 pub fn active_binding(&self) -> Option<BindingRecord> {
302 self.binding_status().ok()
303 }
304
305 pub fn binding_status(&self) -> Result<BindingRecord, LicenceError> {
307 self.run(|inner, ev| self.binding_in_effect(inner, ev))
308 }
309
310 pub fn held_binding(&self) -> Option<BindingRecord> {
313 self.lock().binding.as_ref().map(|h| h.body.clone())
314 }
315
316 pub fn verify_member(&self, signed: &super::SignedBinding) -> Result<BindingRecord, LicenceError> {
320 let local = self.local.get().ok_or(LicenceError::NoLocalMesh)?;
321 super::verify_binding_member(signed, &self.anchors, &local)
322 }
323
324 fn with_valid<T>(&self, mut f: impl FnMut(&CheckoutGrant, &str) -> Option<T>) -> Option<T> {
327 self.run(|inner, ev| {
328 let b = self.binding_in_effect(inner, ev).ok()?;
329 if self.key_revoked(&b.grant_pubkey) {
330 return None;
331 }
332 let eff = self.eff_now(inner, &b.grant_pubkey);
333 inner
334 .slots
335 .values()
336 .filter_map(|s| s.current.as_ref())
337 .filter(|h| h.body.mesh_id == b.mesh_id && grant_valid(&h.body, eff))
338 .find_map(|h| f(&h.body, &b.grant_pubkey))
339 })
340 }
341
342 pub fn valid_grant_covering(
344 &self,
345 blake3_hex: &str,
346 cog_id: &str,
347 version: &str,
348 ) -> Option<CheckoutGrant> {
349 self.with_valid(|g, _| {
350 (g.cog_id == cog_id && g.version == version && g.artifact_by_blake3(blake3_hex).is_some())
351 .then(|| g.clone())
352 })
353 }
354
355 pub fn valid_grant_for_artifact(
358 &self,
359 cog_id: &str,
360 version: &str,
361 sha256: &str,
362 blake3: &str,
363 ) -> Option<(CheckoutGrant, GrantArtifact)> {
364 self.with_valid(|g, _| {
365 if g.cog_id != cog_id || g.version != version {
366 return None;
367 }
368 g.artifacts
369 .iter()
370 .find(|a| a.sha256 == sha256 && a.blake3 == blake3)
371 .map(|a| (g.clone(), a.clone()))
372 })
373 }
374
375 pub fn verified_grants(&self) -> Vec<VerifiedCheckoutGrant> {
377 let mut out = Vec::new();
378 self.with_valid::<()>(|g, pk| {
379 out.push(VerifiedCheckoutGrant { grant: g.clone(), grant_pubkey_hex: pk.to_owned() });
380 None
381 });
382 out
383 }
384
385 pub fn floor(&self) -> Option<u64> {
387 self.run(|inner, ev| {
388 let b = self.binding_in_effect(inner, ev).ok()?;
389 self.eff_now(inner, &b.grant_pubkey);
390 inner.floors.get(&b.grant_pubkey).map(FloorState::floor)
391 })
392 }
393
394 pub fn reset_floor(&self) -> Result<(), LicenceError> {
398 self.run(|inner, ev| {
399 let b = self.binding_in_effect(inner, ev)?;
400 let now = (self.clock)();
401 let mut next = inner.clone();
402 next.floors.entry(b.grant_pubkey).or_default().reset(now);
403 ev.push(LicenceEvent::FloorReset(now));
404 self.commit(inner, next)
405 })
406 }
407
408 pub fn tick(&self) {
411 self.run(|inner, ev| {
412 if inner.dirty && inner.poisoned.is_none() {
415 let _ = self.save(inner);
416 }
417 if let Ok(b) = self.binding_in_effect(inner, ev) {
418 self.eff_now(inner, &b.grant_pubkey);
419 let hw = inner.floors.get(&b.grant_pubkey).map_or(0, |f| f.hw);
420 if hw >= inner.persisted_hw.saturating_add(HW_PERSIST_STEP) {
421 let _ = self.save(inner);
422 }
423 }
424 })
425 }
426}