Skip to main content

clawft_kernel/licence/
mod.rs

1//! Seed licence proxy, member side (ADR-106 phase 1a).
2//!
3//! Types, stores and policy for the mesh checkout of Cognitum cogs, as pure
4//! in-process code (no network, no transport):
5//!
6//! - [`MeshId`] and [`LocalMeshId`]: the mesh a binding, grant or approval is for.
7//! - [`BindingRecord`]: the operator-signed Seed to mesh binding (v2), with the
8//!   *member* verification profile ([`verify_binding_member`]). The *steward*
9//!   profile (age window, live identity match, fingerprint) is phase 1d and
10//!   plugs in through [`BindingExtraCheck`].
11//! - [`CheckoutGrant`]: signed by the Seed's grant key; highest `seq` wins.
12//! - [`Approval`]: the operator's additive, content-addressed hash approval.
13//! - [`CheckoutGrantStore`] and [`ApprovalStore`]: persisted, atomic, 0600,
14//!   size-capped, fail-closed.
15//! - [`MeshCheckoutPolicy`]: the swarm redistribution policy. With no binding
16//!   it behaves exactly like [`crate::mesh_swarm_state::ManifestPolicy`].
17//! - [`may_run`]: the run gate (valid grant AND an approval covering the sha256).
18//!
19//! Every signature is Ed25519, checked with `verify_strict`, over
20//! `domain tag || "\n" || canonical JSON payload`. The signer keys are the
21//! existing [`crate::workload_pkg::TrustAnchors`] operator keys; the grant key
22//! is the one the binding names.
23
24mod approval;
25mod approval_store;
26mod binding;
27mod chain_sink;
28mod client;
29mod exchange;
30mod exchange_sync;
31mod exchange_retry;
32mod exchange_types;
33mod floor;
34mod floor_preview;
35mod gate;
36#[cfg(all(feature = "workload-runtime", unix))]
37mod http_transport;
38mod links;
39mod mesh_config;
40mod persist;
41mod policy;
42mod relay;
43mod renewal;
44mod request;
45mod run_gate;
46mod store;
47mod steward;
48mod steward_client;
49mod store_accept;
50mod store_load;
51mod store_status;
52mod store_sync;
53mod service_links;
54
55#[cfg(test)]
56mod tests_common;
57#[cfg(test)]
58mod tests_exchange;
59#[cfg(test)]
60mod tests_fixes;
61#[cfg(test)]
62mod tests_sync;
63#[cfg(test)]
64mod tests_service_links;
65#[cfg(test)]
66mod tests_policy;
67#[cfg(test)]
68mod tests_relay;
69#[cfg(test)]
70mod tests_relay_local;
71#[cfg(test)]
72mod tests_request;
73#[cfg(test)]
74mod tests_stub;
75#[cfg(test)]
76mod tests_review;
77#[cfg(test)]
78mod tests_seed_service;
79#[cfg(test)]
80mod tests_store;
81#[cfg(test)]
82mod tests_types;
83#[cfg(test)]
84mod tests_run_gate;
85#[cfg(all(test, feature = "workload-runtime", unix))]
86mod tests_http;
87#[cfg(all(test, feature = "workload-runtime", unix))]
88mod tests_http_e2e;
89#[cfg(all(test, feature = "workload-runtime", unix))]
90mod tests_renewal;
91#[cfg(test)]
92mod tests_renewal_cursor;
93
94use std::sync::{Arc, Mutex, RwLock};
95
96#[allow(unused_imports)]
97use ed25519_dalek::{Signature, Signer, SigningKey, VerifyingKey};
98#[allow(unused_imports)]
99use serde::{Deserialize, Serialize};
100
101#[allow(unused_imports)]
102use crate::workload_pkg::codec::{hex_decode_exact, hex_encode};
103
104pub use approval::{Approval, SignedApproval, sign_approval, verify_approval, verify_approval_signature};
105pub use approval_store::{APPROVALS_FILE, ApprovalStore};
106pub use binding::{
107    AdmissionPosture, BindState, BindingExtraCheck, BindingRecord, NoExtraChecks, SignedBinding,
108    sign_binding, verify_binding_member,
109};
110pub use chain_sink::{ChainLicenceSink, LICENCE_EVENT_PREFIX};
111pub use client::{
112    ARTIFACT_PATH, CHECKOUT_PATH, CheckoutWire, ClockMs, system_clock_ms, GRANTS_PATH, GrantsPage, RENEW_PATH, LicenceClient, LicenceClientError,
113    LicenceResponse, LicenceTransport, SignedLicenceClient,
114};
115pub use exchange::{
116    CtxAdmission, ExchangeError, LicenceExchange, LicenceExchangeConfig, LicenceExchangeParts,
117    PeerAdmission, PostureFn, Receipt, Spend, sign_unbind,
118};
119pub use exchange_retry::MAX_SYNC_RETRIES;
120pub use exchange_sync::{GrantCursor, SYNC_MAX_BYTES, SYNC_MAX_ENTRIES, SyncMsg};
121pub use floor::FloorState;
122pub use floor_preview::{FloorPreview, RevivedGrant};
123pub use gate::{RunDenied, RunPermit, RunRequest, may_run};
124#[cfg(all(feature = "workload-runtime", unix))]
125pub use http_transport::{
126    DEFAULT_MAX_ARTIFACT_BYTES, HttpLicenceTransport, LicenceLinkConfig, TransportLimits,
127};
128pub use renewal::{
129    EVENT_KIND_CHECKOUT_LAPSED, EVENT_KIND_CHECKOUT_RENEWED, RenewalConfig, RenewalReport, Renewer,
130};
131pub use run_gate::{CognitumRunGate, RunRefusal, RunVerdict, StoreRunGate, check_run};
132pub use steward_client::{SharedTransport, StewardLicenceClient};
133pub use store_status::{ApprovalRow, BOUND_MARKER, GrantRow};
134pub use links::LicenceLinks;
135pub use service_links::{
136    LICENCE_TOPICS, MAX_REPLY_SENDS, MAX_REPLY_SENDS_PER_PEER, PEER_REFRESH, PeerDirectory, PeerSnapshot, ServiceLicenceLinks, ServiceLinksCounters,
137    is_licence_topic,
138};
139pub use weft_licence_wire::{
140    APPROVAL_DOMAIN, BINDING_DOMAIN, CheckoutGrant, FAR_FUTURE_CLAMP_SECS, GRANT_DOMAIN,
141    GRANT_SKEW_SECS, GrantArtifact, LicenceError, LicenceRef, MAX_APPROVALS, MAX_GRANT_SLOTS,
142    MAX_ARTIFACT_BYTES, MAX_FLOORS, MAX_GRANT_TTL_SECS, MAX_PAYLOAD_BYTES, MAX_STORE_BYTES, MAX_UNIX_TIME, MESH_ID_DOMAIN, MeshId,
143    SignedEnvelope, SignedGrant, key_id, sha256_hex, sign_grant, verify_grant,
144};
145#[allow(unused_imports)]
146pub(crate) use weft_licence_wire::{
147    envelope_key, parse_canonical, sign_envelope, signed_bytes, valid_hex32, valid_token,
148    verify_envelope, verify_grant_signature,
149};
150pub use mesh_config::{MeshIdConfigError, mesh_id_from_config};
151pub use policy::MeshCheckoutPolicy;
152pub use relay::{
153    CheckoutCaller, CheckoutRefusal, CheckoutRelay, RelayLimits, EVENT_KIND_CHECKOUT_GRANTED,
154    EVENT_KIND_CHECKOUT_REFUSED, GATE_ACTION, GrantFlood, NoFlood, install_grant,
155};
156pub use request::{
157    CLOCK_FLOOR_SECS, LicenceRequest, REQUEST_DOMAIN, REQUEST_WINDOW_MS, ReplayGuard,
158    RequestAuth, RequestRefused, sign_request, signing_string, valid_nonce, verify_request,
159};
160pub use steward::StewardCheck;
161pub use store::{CheckoutGrantStore, GRANTS_FILE, VerifiedCheckoutGrant};
162
163/// What an accepted record did.
164#[derive(Debug, Clone, Copy, PartialEq, Eq)]
165pub enum Outcome {
166    /// New state was recorded.
167    Applied,
168    /// A record that only restricts (an unbind, a withdrawal) is in force
169    /// but could not be saved; `tick` retries. Propagate it like `Applied`.
170    AppliedUnsaved,
171    /// Already held; nothing changed.
172    Duplicate,
173    /// A lower `seq` than the one held; ignored.
174    Ignored,
175}
176
177/// Shared, live handle to this node's mesh id. A change (new nonce or pin)
178/// reaches every store holding a clone, which then report orphaned state.
179#[derive(Clone, Default)]
180pub struct LocalMeshId(Arc<RwLock<Option<MeshId>>>);
181
182impl LocalMeshId {
183    /// A handle holding `id`.
184    pub fn new(id: MeshId) -> Self {
185        Self(Arc::new(RwLock::new(Some(id))))
186    }
187
188    /// A handle with no id yet (no mesh nonce configured).
189    pub fn unset() -> Self {
190        Self::default()
191    }
192
193    /// The current id.
194    pub fn get(&self) -> Option<MeshId> {
195        *self.0.read().unwrap_or_else(|p| p.into_inner())
196    }
197
198    /// Replace the id.
199    pub fn set(&self, id: Option<MeshId>) {
200        *self.0.write().unwrap_or_else(|p| p.into_inner()) = id;
201    }
202}
203
204/// Source of unix seconds. Injected so tests control the clock.
205pub type Clock = Arc<dyn Fn() -> u64 + Send + Sync>;
206
207/// The wall clock.
208pub fn system_clock() -> Clock {
209    Arc::new(|| {
210        std::time::SystemTime::now()
211            .duration_since(std::time::UNIX_EPOCH)
212            .map(|d| d.as_secs())
213            .unwrap_or(0)
214    })
215}
216
217/// Something worth chaining. The daemon maps these to chain events; the
218/// stores only report them through a [`LicenceEventSink`].
219#[derive(Debug, Clone, PartialEq, Eq)]
220pub enum LicenceEvent {
221    /// A binding was refused (`open_membership`, `observe`, ...).
222    BindingRefused(String),
223    /// Two different bindings at one `seq`.
224    BindingConflict(u64),
225    /// The stored binding's mesh id no longer matches the local one.
226    BindingOrphaned {
227        /// Mesh id in the stored binding.
228        stored: String,
229        /// Mesh id this node computes now.
230        local: String,
231    },
232    /// Two different grants at one `seq` for one (cog, version).
233    GrantConflict {
234        /// Cog id.
235        cog_id: String,
236        /// Version.
237        version: String,
238        /// The contested `seq`.
239        seq: u64,
240    },
241    /// An operator reset the floor.
242    FloorReset(u64),
243    /// A sync response carried a bad signature; the peer is banned from sync.
244    SyncBadSignature {
245        /// The peer that sent it.
246        peer: String,
247    },
248}
249
250impl LicenceEvent {
251    /// The chain event name.
252    pub fn name(&self) -> &'static str {
253        match self {
254            Self::BindingRefused(_) => "binding_refused",
255            Self::BindingConflict(_) => "binding_conflict",
256            Self::BindingOrphaned { .. } => "binding_orphaned",
257            Self::GrantConflict { .. } => "grant_conflict",
258            Self::FloorReset(_) => "floor_reset",
259            Self::SyncBadSignature { .. } => "sync_bad_signature",
260        }
261    }
262}
263
264/// Receives [`LicenceEvent`]s. Called after the store's lock is released, but
265/// a sink must still not call back into the store.
266pub trait LicenceEventSink: Send + Sync {
267    /// Handle one event.
268    fn emit(&self, event: LicenceEvent);
269}
270
271/// Drops every event.
272#[derive(Debug, Default)]
273pub struct NoopSink;
274
275impl LicenceEventSink for NoopSink {
276    fn emit(&self, _: LicenceEvent) {}
277}
278
279/// Keeps every event (tests, and callers that drain into the chain).
280#[derive(Debug, Default)]
281pub struct RecordingSink(Mutex<Vec<LicenceEvent>>);
282
283impl RecordingSink {
284    /// The events so far.
285    pub fn events(&self) -> Vec<LicenceEvent> {
286        self.0.lock().unwrap_or_else(|p| p.into_inner()).clone()
287    }
288}
289
290impl LicenceEventSink for RecordingSink {
291    fn emit(&self, event: LicenceEvent) {
292        self.0.lock().unwrap_or_else(|p| p.into_inner()).push(event);
293    }
294}
295