1use super::{membership::EvidenceRef, peer::ActiveInputs, *};
3use crate::sync::seed_claim::membership::{
4 Evidence, MAX_CANDIDATES, MAX_RECORD_BYTES, MAX_TRANSITIONS, Membership, RotationMaterial,
5};
6use anyhow::{Context, Result, ensure};
7use serde::{Deserialize, Serialize};
8type Hash = [u8; 32];
9const INTENT_BYTES: usize = 1024;
10const PLAN_BYTES: usize = 1024;
11const RECORD_BYTES: usize = MAX_RECORD_BYTES + 128;
12const MATERIAL_BYTES: usize = 256;
13
14#[derive(Serialize, Deserialize)]
15#[serde(deny_unknown_fields)]
16pub struct Intent {
17 index: usize,
18 target: Option<Hash>,
19 #[serde(default, skip_serializing_if = "Option::is_none")]
22 withdraw: Option<Hash>,
23 original: EvidenceRef,
24}
25impl Intent {
26 fn name(&self, phase: &str) -> String {
27 format!("management-{}-{phase}", self.index)
28 }
29 pub fn target(&self) -> Option<Hash> {
30 self.target
31 }
32 fn revoke_targets(&self) -> Result<Vec<Hash>> {
34 match (self.target, self.withdraw) {
35 (Some(target), None) => Ok(vec![target]),
36 (None, Some(_)) => Ok(vec![]),
37 _ => anyhow::bail!("error management-target"),
38 }
39 }
40}
41#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
42pub enum Action {
43 Revoke,
44 Rotate,
45}
46#[derive(Serialize, Deserialize)]
47#[serde(deny_unknown_fields)]
48struct Plan {
49 before: EvidenceRef,
50 action: Action,
51 cutoff: u64,
52}
53pub struct Dispatch {
54 pub record: Vec<u8>,
55 pub evidence: Evidence,
56 pub membership: Membership,
57 pub action: Action,
58}
59
60impl ProtectedLocalKeyStore {
61 fn management_action(&self, inputs: &ActiveInputs, intent: &Intent) -> Result<Option<Action>> {
62 let (_, original) = self.load_evidence(&intent.original)?;
63 ensure!(
64 inputs.membership.extends(&original),
65 "error management-fork"
66 );
67 match (intent.target, intent.withdraw) {
68 (Some(target), None) => {
69 ensure!(original.has_device(target), "error management-target")
70 }
71 (None, None) => ensure!(original.rotation_pending(), "error management-intent"),
72 (None, Some(_)) => {}
73 (Some(_), Some(_)) => anyhow::bail!("error management-intent"),
74 }
75 let mut removed_generation = intent
76 .target
77 .is_none()
78 .then_some(original.current_generation().id);
79 let mut before = original.clone();
80 for t in inputs
81 .evidence
82 .transitions
83 .iter()
84 .skip(original.sequence() as usize - 1)
85 {
86 let next = before.append(&t.declaration, &t.request, &t.record)?;
87 if removed_generation.is_none()
88 && intent.target.is_some_and(|target| !next.has_device(target))
89 {
90 removed_generation = Some(next.current_generation().id);
91 }
92 if removed_generation
93 .is_some_and(|generation| generation != next.current_generation().id)
94 && !next.rotation_pending()
95 {
96 return Ok(None);
97 }
98 before = next;
99 }
100 let freeze = intent.withdraw.is_some() && !inputs.membership.rotation_pending();
102 Ok(Some(if removed_generation.is_none() || freeze {
103 Action::Revoke
104 } else {
105 Action::Rotate
106 }))
107 }
108
109 async fn management_plans(
113 &self,
114 db: &Database,
115 inputs: &ActiveInputs,
116 intent: &Intent,
117 prove: bool,
118 ) -> Result<Vec<(Plan, Option<Membership>, Option<Vec<u8>>)>> {
119 let mut plans = Vec::new();
120 let mut gap = false;
121 let mut previous_sequence = 0;
122 for index in 0..MAX_CANDIDATES {
123 let raw = self
124 .phase(db, &intent.name(&format!("plan-{index}")), PLAN_BYTES)
125 .await?;
126 let record = self
127 .phase(
128 db,
129 &intent.name(&format!("candidate-{index}")),
130 RECORD_BYTES,
131 )
132 .await?;
133 let material = self
134 .phase(
135 db,
136 &intent.name(&format!("material-{index}")),
137 MATERIAL_BYTES,
138 )
139 .await?;
140 let sent = self
141 .phase(db, &intent.name(&format!("sent-{index}")), 128)
142 .await?;
143 let Some(raw) = raw else {
144 ensure!(
145 record.is_none() && material.is_none() && sent.is_none(),
146 "error management-plan-missing"
147 );
148 gap = true;
149 continue;
150 };
151 ensure!(!gap, "error management-plan-missing");
152 let plan: Plan = serde_json::from_slice(&raw)?;
153 if plan.action == Action::Revoke {
154 ensure!(
155 material.is_none() && plan.cutoff == 0,
156 "error management-plan"
157 );
158 }
159 if let Some(material) = &material {
160 RotationMaterial::from_protected_storage(material)?;
161 }
162 ensure!(
163 record.is_some() || sent.is_none(),
164 "error management-candidate-missing"
165 );
166 ensure!(
167 record.is_none() || plan.action != Action::Rotate || material.is_some(),
168 "error management-material-missing"
169 );
170 if !prove {
171 self.load_evidence_bytes(&plan.before)?;
172 plans.push((plan, None, record.map(|r| r.to_vec())));
173 continue;
174 }
175 let (_, before) = self.load_evidence(&plan.before)?;
176 ensure!(inputs.membership.extends(&before), "error management-fork");
177 ensure!(
178 before.sequence() > previous_sequence,
179 "error management-plan-order"
180 );
181 previous_sequence = before.sequence();
182 if let Some(record) = &record {
183 let after = before.append(&[], &[], record)?;
184 if plan.action == Action::Revoke {
185 ensure!(
186 inputs
187 .authority()
188 .prepare_revoke(&before, &intent.revoke_targets()?)?
189 == record.as_slice(),
190 "error management-target-mismatch"
191 );
192 }
193 ensure!(
194 (after.generations().len() > before.generations().len())
195 == (plan.action == Action::Rotate),
196 "error management-action"
197 );
198 if plan.action == Action::Rotate {
199 ensure!(
200 after.current_generation().starts_after == plan.cutoff,
201 "error management-cutoff"
202 );
203 RotationMaterial::from_protected_storage(
204 material
205 .as_ref()
206 .context("error management-material-missing")?,
207 )?
208 .validate_generation(&after)?;
209 }
210 if inputs.membership.head_at(after.sequence()) == Some(after.head()) {
211 ensure!(sent.is_some(), "error management-sent-missing");
212 }
213 if let Some(sent) = &sent {
214 ensure!(
215 sent.as_slice() == after.head(),
216 "error management-sent-mismatch"
217 );
218 }
219 }
220 plans.push((plan, Some(before), record.map(|r| r.to_vec())));
221 }
222 Ok(plans)
223 }
224 pub async fn management_intent(
227 &self,
228 db: &Database,
229 inputs: &ActiveInputs,
230 target: Option<Hash>,
231 withdraw: Option<Hash>,
232 ) -> Result<Option<Intent>> {
233 let mut next = 0;
234 let mut gap = false;
235 let mut unfinished = None;
236 let mut completed_target = false;
237 for index in 0..MAX_TRANSITIONS {
238 let raw = self
239 .phase(db, &format!("management-{index}-intent"), INTENT_BYTES)
240 .await?;
241 let Some(raw) = raw else {
242 gap = true;
243 continue;
244 };
245 ensure!(!gap, "error management-intent-missing");
246 ensure!(unfinished.is_none(), "error management-intent-order");
247 next = index + 1;
248 let intent: Intent = serde_json::from_slice(&raw)?;
249 ensure!(intent.index == index, "error management-intent");
250 let done = if let Some(ready) = self.phase(db, &intent.name("ready"), 128).await? {
254 ensure!(
255 inputs
256 .membership
257 .contains_head(&ready.as_slice().try_into()?),
258 "error management-ready-mismatch"
259 );
260 self.load_evidence_bytes(&intent.original)?;
261 self.management_plans(db, inputs, &intent, false).await?;
262 true
263 } else {
264 self.management_plans(db, inputs, &intent, true).await?;
265 let done = self.management_action(inputs, &intent)?.is_none();
266 if done {
267 self.save_management_phase(
268 db,
269 inputs,
270 &intent.name("ready"),
271 128,
272 &inputs.membership.head(),
273 )
274 .await?;
275 }
276 done
277 };
278 if done {
279 if (target.is_some() && target == intent.target)
280 || (withdraw.is_some() && withdraw == intent.withdraw)
281 {
282 completed_target = true;
283 }
284 } else {
285 unfinished = Some(intent);
286 }
287 }
288 if completed_target {
289 return Ok(None);
290 }
291 if let Some(intent) = unfinished {
292 ensure!(
293 (target.is_none() && withdraw.is_none())
294 || (target == intent.target && withdraw == intent.withdraw),
295 "error management-unfinished"
296 );
297 return Ok(Some(intent));
298 }
299 if target.is_none() && withdraw.is_none() && !inputs.membership.rotation_pending() {
300 return Ok(None);
301 }
302 ensure!(next < MAX_TRANSITIONS, "error membership-change-limit");
303 if let Some(target) = target {
304 inputs
305 .authority()
306 .prepare_revoke(&inputs.membership, &[target])?;
307 }
308 let intent = Intent {
309 index: next,
310 target,
311 withdraw,
312 original: self.save_evidence(&inputs.evidence)?,
313 };
314 self.save_management_phase(
315 db,
316 inputs,
317 &intent.name("intent"),
318 INTENT_BYTES,
319 &serde_json::to_vec(&intent)?,
320 )
321 .await?;
322 Ok(Some(intent))
323 }
324 pub async fn management_dispatch(
325 &self,
326 db: &Database,
327 inputs: &ActiveInputs,
328 intent: &Intent,
329 high: u64,
330 ) -> Result<Option<Dispatch>> {
331 let plans = self.management_plans(db, inputs, intent, true).await?;
332 let Some(action) = self.management_action(inputs, intent)? else {
333 return Ok(None);
334 };
335 let mut index = plans.len();
336 let mut selected = None;
337 for (n, (plan, before, record)) in plans.into_iter().enumerate() {
338 let before = before.context("error management-plan")?;
339 if let Some(head) = inputs.membership.head_at(before.sequence() + 1) {
340 if record
341 .as_ref()
342 .is_some_and(|r| Sha256::digest(r).as_slice() == head)
343 {
344 ensure!(
345 self.phase(db, &intent.name(&format!("sent-{n}")), 128)
346 .await?
347 .is_some(),
348 "error management-sent-missing"
349 );
350 }
351 continue;
352 }
353 ensure!(
354 before.head() == inputs.membership.head() && plan.action == action,
355 "error management-unresolved"
356 );
357 index = n;
358 selected = Some((plan, record));
359 break;
360 }
361 ensure!(index < MAX_CANDIDATES, "error management-limit");
362 let (plan, saved) = match selected {
363 Some(value) => value,
364 None => {
365 let plan = Plan {
366 before: self.save_evidence(&inputs.evidence)?,
367 action,
368 cutoff: if action == Action::Rotate { high } else { 0 },
369 };
370 self.save_management_phase(
371 db,
372 inputs,
373 &intent.name(&format!("plan-{index}")),
374 PLAN_BYTES,
375 &serde_json::to_vec(&plan)?,
376 )
377 .await?;
378 (plan, None)
379 }
380 };
381 let record = if let Some(saved) = saved {
382 saved
383 } else {
384 let record = match action {
385 Action::Revoke => inputs
386 .authority()
387 .prepare_revoke(&inputs.membership, &intent.revoke_targets()?)?,
388 Action::Rotate => {
389 ensure!(plan.cutoff == high, "error management-cutoff");
390 let name = intent.name(&format!("material-{index}"));
391 let material = match self.phase(db, &name, MATERIAL_BYTES).await? {
392 Some(bytes) => RotationMaterial::from_protected_storage(&bytes)?,
393 None => {
394 let material = RotationMaterial::generate()?;
395 self.save_management_phase(
396 db,
397 inputs,
398 &name,
399 MATERIAL_BYTES,
400 &material.protected_storage_bytes(),
401 )
402 .await?;
403 material
404 }
405 };
406 inputs.authority().prepare_rotation_with(
407 &inputs.membership,
408 inputs.generation_keys(),
409 plan.cutoff,
410 &material,
411 )?
412 }
413 };
414 self.save_management_phase(
415 db,
416 inputs,
417 &intent.name(&format!("candidate-{index}")),
418 RECORD_BYTES,
419 &record,
420 )
421 .await?;
422 record
423 };
424 self.save_management_phase(
425 db,
426 inputs,
427 &intent.name(&format!("sent-{index}")),
428 128,
429 &Sha256::digest(&record),
430 )
431 .await?;
432 let (mut evidence, before) = self.load_evidence(&plan.before)?;
433 let membership = before.append(&[], &[], &record)?;
434 evidence
435 .transitions
436 .push(crate::sync::seed_claim::membership::EvidenceRecord {
437 declaration: vec![],
438 request: vec![],
439 record: record.clone(),
440 });
441 Ok(Some(Dispatch {
442 record,
443 evidence,
444 membership,
445 action,
446 }))
447 }
448}