clawft_kernel/licence/
steward.rs1use super::{BindState, BindingExtraCheck, BindingRecord, LicenceError, key_id};
12use crate::workload_pkg::codec::hex_decode_exact;
13
14#[derive(Debug, Clone)]
16pub struct StewardCheck {
17 pub now: u64,
19 pub max_age_secs: u64,
21 pub skew_secs: u64,
23 pub confirmed_fingerprint: String,
26 pub steward_node_id: String,
28 pub steward_pubkey: String,
30 pub held_seq: Option<u64>,
32 pub seed_identity: Option<(String, String)>,
35}
36
37fn fail(code: &str) -> LicenceError {
38 LicenceError::CheckFailed(code.into())
39}
40
41impl BindingExtraCheck for StewardCheck {
42 fn check(&self, r: &BindingRecord) -> Result<(), LicenceError> {
43 if r.state != BindState::Bound {
44 return Err(fail("not_a_bind"));
45 }
46 if r.steward_node_id != self.steward_node_id || r.steward_pubkey != self.steward_pubkey {
47 return Err(fail("steward_mismatch"));
48 }
49 let gk = hex_decode_exact::<32>(&r.grant_pubkey).ok_or_else(|| fail("grant_key"))?;
50 if key_id(&gk) != self.confirmed_fingerprint {
51 return Err(fail("fingerprint_mismatch"));
52 }
53 if r.bound_at > self.now.saturating_add(self.skew_secs)
54 || self.now.saturating_sub(r.bound_at) > self.max_age_secs
55 {
56 return Err(fail("expired"));
57 }
58 if self.held_seq.is_some_and(|h| r.seq <= h) {
59 return Err(fail("seq_not_greater"));
60 }
61 if let Some((id, key)) = &self.seed_identity {
62 if *id != r.device_id {
63 return Err(fail("identity_mismatch_device_id"));
64 }
65 if *key != r.device_pubkey {
66 return Err(fail("identity_mismatch_device_key"));
67 }
68 }
69 Ok(())
70 }
71}
72
73#[cfg(test)]
74mod tests {
75 use super::super::tests_common::*;
76 use super::*;
77
78 fn rec() -> BindingRecord {
79 binding_rec(2, BindState::Bound, &grant_key(), &mesh())
80 }
81
82 fn check() -> StewardCheck {
83 StewardCheck {
84 now: T0,
85 max_age_secs: 600,
86 skew_secs: 60,
87 confirmed_fingerprint: key_id(&grant_key().verifying_key().to_bytes()),
88 steward_node_id: "node-steward".into(),
89 steward_pubkey: pk_hex(&sk(21)),
90 held_seq: Some(1),
91 seed_identity: Some(("seed-test".into(), pk_hex(&sk(20)))),
92 }
93 }
94
95 fn refused(c: &StewardCheck, r: &BindingRecord) -> String {
96 match c.check(r) {
97 Err(LicenceError::CheckFailed(code)) => code,
98 other => panic!("expected a refusal, got {other:?}"),
99 }
100 }
101
102 #[test]
103 fn a_matching_record_passes() {
104 assert_eq!(check().check(&rec()), Ok(()));
105 }
106
107 #[test]
108 fn each_check_refuses_with_its_own_code() {
109 let mut c = check();
110 c.confirmed_fingerprint = key_id(&sk(99).verifying_key().to_bytes());
111 assert_eq!(refused(&c, &rec()), "fingerprint_mismatch");
112
113 let mut c = check();
114 c.steward_node_id = "other".into();
115 assert_eq!(refused(&c, &rec()), "steward_mismatch");
116
117 let mut c = check();
118 c.now = T0 + 601;
119 assert_eq!(refused(&c, &rec()), "expired");
120 c.now = T0 - 61;
121 assert_eq!(refused(&c, &rec()), "expired");
122 c.now = T0 + 600;
123 assert_eq!(c.check(&rec()), Ok(()), "600 s is inside the window");
124
125 let mut c = check();
126 c.held_seq = Some(2);
127 assert_eq!(refused(&c, &rec()), "seq_not_greater");
128
129 let mut c = check();
130 c.seed_identity = Some(("someone-else".into(), pk_hex(&sk(20))));
131 assert_eq!(refused(&c, &rec()), "identity_mismatch_device_id");
132 c.seed_identity = Some(("seed-test".into(), pk_hex(&sk(77))));
133 assert_eq!(refused(&c, &rec()), "identity_mismatch_device_key");
134
135 let unbind = binding_rec(2, BindState::Unbound, &grant_key(), &mesh());
136 assert_eq!(refused(&check(), &unbind), "not_a_bind");
137 }
138
139 #[test]
140 fn the_precheck_without_a_seed_skips_only_the_identity_match() {
141 let mut c = check();
142 c.seed_identity = None;
143 assert_eq!(c.check(&rec()), Ok(()));
144 c.confirmed_fingerprint = "ed25519:0000000000000000".into();
145 assert_eq!(refused(&c, &rec()), "fingerprint_mismatch");
146 }
147}