clawft_kernel/workload_runtime/seed_bind/
v2.rs1use serde_json::json;
16use sha2::{Digest, Sha256};
17
18use super::{
19 BIND_CHAIN_SOURCE, BindError, SeedBinder, write_state, MAX_BOUND_DEVICES,
20};
21use crate::chain::{EVENT_KIND_WORKLOAD_NODE_BIND, EVENT_KIND_WORKLOAD_NODE_UNBIND};
22use crate::licence::{
23 AdmissionPosture, BindState as LicState, BindingExtraCheck, BindingRecord, CheckoutGrantStore, LicenceError,
24 NoExtraChecks, Outcome, SignedBinding, StewardCheck, key_id,
25};
26use crate::workload_pkg::codec::hex_decode_exact;
27use crate::workload_runtime::seed::SeedApiRuntime;
28use crate::workload_runtime::types::WorkloadRuntime;
29
30pub(super) fn licence_code(e: &LicenceError) -> &'static str {
32 match e {
33 LicenceError::Malformed(_) | LicenceError::TooLarge => "malformed",
34 LicenceError::BadSignature => "bad_signature",
35 LicenceError::UntrustedKey => "untrusted_operator",
36 LicenceError::WrongMesh => "wrong_mesh",
37 LicenceError::NoLocalMesh => "no_local_mesh",
38 LicenceError::NoBinding => "no_binding",
39 LicenceError::Conflict(_) => "binding_conflict",
40 LicenceError::BindingRefused(_) => "binding_refused",
41 LicenceError::Poisoned(_) => "store_poisoned",
42 LicenceError::Persist(_) => "state_unwritable",
43 LicenceError::CheckFailed(c) => match c.as_str() {
44 "not_a_bind" => "not_a_bind",
45 "not_an_unbind" => "not_an_unbind",
46 "steward_mismatch" => "steward_mismatch",
47 "fingerprint_mismatch" => "fingerprint_mismatch",
48 "expired" => "expired",
49 "seq_not_greater" => "seq_not_greater",
50 "device_mismatch" => "device_mismatch",
51 "identity_mismatch_device_id" => "identity_mismatch",
52 "identity_mismatch_device_key" => "identity_mismatch",
53 _ => "check_failed",
54 },
55 _ => "licence_refused",
56 }
57}
58
59pub struct StewardBind<'a> {
61 pub signed: &'a SignedBinding,
63 pub rt: &'a SeedApiRuntime,
65 pub store: &'a CheckoutGrantStore,
67 pub posture: AdmissionPosture,
69 pub confirmed_fingerprint: &'a str,
71 pub steward_node_id: &'a str,
73 pub steward_pubkey: &'a str,
75 pub now: u64,
77}
78
79fn lic(e: LicenceError) -> BindError {
80 BindError::Licence(e)
81}
82
83impl SeedBinder {
84 pub async fn bind_v2(&self, b: &StewardBind<'_>) -> Result<BindingRecord, BindError> {
86 match self.verify_v2(b).await {
87 Ok(r) => Ok(r),
88 Err(e) => {
89 self.refuse_record(&b.signed.payload, &e);
90 Err(e)
91 }
92 }
93 }
94
95 fn steward_check(&self, b: &StewardBind<'_>, held: Option<u64>) -> StewardCheck {
96 StewardCheck {
97 now: b.now,
98 max_age_secs: self.max_age_secs,
99 skew_secs: super::BIND_CLOCK_SKEW_SECS,
100 confirmed_fingerprint: b.confirmed_fingerprint.to_owned(),
101 steward_node_id: b.steward_node_id.to_owned(),
102 steward_pubkey: b.steward_pubkey.to_owned(),
103 held_seq: held,
104 seed_identity: None,
105 }
106 }
107
108 async fn verify_v2(&self, b: &StewardBind<'_>) -> Result<BindingRecord, BindError> {
109 let poisoned = || BindError::Malformed("binder poisoned".into());
110 b.posture.check().map_err(lic)?;
111 b.rt.link_security()
114 .require_pinned(b.rt.node_id())
115 .map_err(BindError::UnpinnedTransport)?;
116 let rec = b.store.verify_member(b.signed).map_err(lic)?;
117 let held_rec = b.store.held_binding();
121 if let Some(h) = &held_rec
122 && h.device_id == rec.device_id
123 && h.state == LicState::Bound
124 && h.mesh_id != rec.mesh_id
125 {
126 return Err(BindError::SeedBoundElsewhere { device_id: rec.device_id });
127 }
128 let held = held_rec.map(|h| h.seq);
129 let mut check = self.steward_check(b, held);
130 check.check(&rec).map_err(lic)?;
131 let (device_id, device_key) = b
133 .rt
134 .identity()
135 .await
136 .map_err(|e| BindError::Seed(e.to_string()))?;
137 check.seed_identity = Some((device_id, device_key));
138 check.check(&rec).map_err(lic)?;
139
140 let digest: [u8; 32] = Sha256::digest(
141 [b.signed.payload.as_bytes(), b.signed.signature.as_bytes()].concat(),
142 )
143 .into();
144 let mut seen = self.seen.lock().map_err(|_| poisoned())?;
145 let mut bound = self.bound.lock().map_err(|_| poisoned())?;
146 if seen.contains(&digest) {
147 return Err(BindError::Replayed);
148 }
149 if bound.get(&rec.device_id).is_some_and(|(at, _)| rec.bound_at <= *at) {
150 return Err(BindError::Replayed);
151 }
152 if !bound.contains_key(&rec.device_id) && bound.len() >= MAX_BOUND_DEVICES {
153 return Err(BindError::State("too many bound devices".into()));
154 }
155 let node_id = b.rt.node_id().to_owned();
156 let mut next = bound.clone();
157 next.insert(rec.device_id.clone(), (rec.bound_at, node_id));
158 if let Some(path) = &self.state_file {
161 write_state(path, &next).map_err(BindError::State)?;
162 }
163 *bound = next;
164 seen.insert(digest);
165 drop((seen, bound));
166 match b.store.accept_binding(b.signed, b.posture, &check).map_err(lic)? {
167 Outcome::Applied | Outcome::AppliedUnsaved => {}
168 Outcome::Duplicate | Outcome::Ignored => return Err(BindError::Replayed),
169 }
170 self.chain.append(
171 BIND_CHAIN_SOURCE,
172 EVENT_KIND_WORKLOAD_NODE_BIND,
173 Some(json!({
174 "v": 2,
175 "device_id": rec.device_id, "device_pubkey": rec.device_pubkey,
176 "mesh_id": rec.mesh_id, "seq": rec.seq, "bound_at": rec.bound_at,
177 "grant_fingerprint": b.confirmed_fingerprint,
178 "steward_node_id": rec.steward_node_id,
179 "operator_key": b.signed.public_key,
180 "record_hash": crate::workload_pkg::codec::hex_encode(&Sha256::digest(
181 b.signed.payload.as_bytes())),
182 })),
183 );
184 Ok(rec)
185 }
186
187 pub fn unbind_v2(
193 &self,
194 signed: &SignedBinding,
195 store: &CheckoutGrantStore,
196 ) -> Result<UnbindOutcome, BindError> {
197 match self.unbind_inner(signed, store) {
198 Ok(r) => Ok(r),
199 Err(e) => {
200 self.refuse_record(&signed.payload, &e);
201 Err(e)
202 }
203 }
204 }
205
206 fn unbind_inner(
207 &self,
208 signed: &SignedBinding,
209 store: &CheckoutGrantStore,
210 ) -> Result<UnbindOutcome, BindError> {
211 let rec = store.verify_member(signed).map_err(lic)?;
212 if rec.state != LicState::Unbound {
213 return Err(lic(LicenceError::CheckFailed("not_an_unbind".into())));
214 }
215 let held = store.held_binding().ok_or(lic(LicenceError::NoBinding))?;
216 if held.device_id != rec.device_id {
217 return Err(lic(LicenceError::CheckFailed("device_mismatch".into())));
218 }
219 if rec.seq <= held.seq {
220 return Err(lic(LicenceError::CheckFailed("seq_not_greater".into())));
221 }
222 let off = AdmissionPosture {
224 enforce: true,
225 verdict_source_bound: true,
226 open_membership: false,
227 };
228 let save_pending = match store.accept_binding(signed, off, &NoExtraChecks) {
229 Ok(Outcome::AppliedUnsaved) => true,
230 Ok(_) => false,
231 Err(LicenceError::Persist(_)) => true,
233 Err(e) => return Err(lic(e)),
234 };
235 self.chain.append(
236 BIND_CHAIN_SOURCE,
237 EVENT_KIND_WORKLOAD_NODE_UNBIND,
238 Some(json!({
239 "v": 2, "device_id": rec.device_id, "mesh_id": rec.mesh_id, "seq": rec.seq,
240 "save_pending": save_pending,
241 "operator_key": signed.public_key,
242 "record_hash": crate::workload_pkg::codec::hex_encode(&Sha256::digest(
243 signed.payload.as_bytes())),
244 })),
245 );
246 Ok(UnbindOutcome { record: rec, save_pending })
247 }
248}
249
250#[derive(Debug, Clone, PartialEq, Eq)]
252pub struct UnbindOutcome {
253 pub record: BindingRecord,
255 pub save_pending: bool,
257}
258
259pub fn grant_fingerprint(grant_pubkey_hex: &str) -> Option<String> {
262 hex_decode_exact::<32>(grant_pubkey_hex).map(|k| key_id(&k))
263}