clawft_kernel/
overlay_trust.rs1use clawft_types::project::ProjectCert;
5use clawft_types::project::canon::{hex_decode, hex_encode};
6use chrono::Utc;
7use clawft_types::runtime_paths::RuntimePaths;
8
9use crate::chain::ChainManager;
10use crate::governance_overlay::{OverlayError, read_capped};
11use crate::parent_policy::{ParentPolicy, ParentPolicyError, write_atomic_0600};
12
13pub const VERSION_PIN_FILE: &str = "parent-policy.version";
16pub const USER_PIN_FILE: &str = "user.pub";
24pub use clawft_types::runtime_paths::REVOKED_FILE;
29
30#[derive(Debug, Default)]
32pub(crate) struct History {
33 pub last_overlay_hash: Option<String>,
35 pub max_parent_version: Option<u64>,
37 pub user_pin_used: bool,
39 pub any_applied: bool,
41}
42
43pub(crate) const OVERLAY_APPLIED_SOURCE: &str = "governance";
45
46pub(crate) fn chain_history(chain: &ChainManager) -> History {
47 let mut h = History::default();
48 for e in chain
53 .tail(0)
54 .iter()
55 .filter(|e| e.kind == "governance.overlay.applied" && e.source == OVERLAY_APPLIED_SOURCE)
56 {
57 h.any_applied = true;
58 let p = e.payload.as_ref();
59 h.last_overlay_hash = Some(
60 p.and_then(|p| p.get("overlay_hash"))
61 .and_then(|v| v.as_str())
62 .unwrap_or_default()
63 .to_owned(),
64 );
65 if let Some(v) = p.and_then(|p| p.get("parent_version")).and_then(|v| v.as_u64()) {
66 h.max_parent_version = Some(h.max_parent_version.map_or(v, |m| m.max(v)));
67 }
68 if p.and_then(|p| p.get("user_pin")).and_then(|v| v.as_bool()) == Some(true) {
69 h.user_pin_used = true;
70 }
71 }
72 h
73}
74
75pub fn write_user_pin(run_dir: &std::path::Path, user_pubkey: &[u8; 32]) -> std::io::Result<()> {
77 write_atomic_0600(
78 &run_dir.join(USER_PIN_FILE),
79 format!("{}\n", hex_encode(user_pubkey)).as_bytes(),
80 )
81}
82
83pub(crate) fn load_user_pubkey(paths: &RuntimePaths) -> Result<([u8; 32], bool), OverlayError> {
89 let cert_err = |m: String| OverlayError::Cert(m);
90 let revoked = paths.revoked_marker();
91 if revoked.exists() {
92 return Err(OverlayError::Revoked(revoked.display().to_string()));
93 }
94 let path = paths
95 .project_cert()
96 .ok_or_else(|| cert_err("no certificate path for this root".into()))?;
97 let text = read_capped(&path)?
98 .ok_or_else(|| cert_err(format!("{} does not exist", path.display())))?;
99 let cert: ProjectCert =
100 serde_json::from_str(&text).map_err(|e| cert_err(format!("does not parse: {e}")))?;
101 let cert_pk: [u8; 32] = hex_decode(&cert.user_pubkey)
102 .ok_or_else(|| cert_err("`user_pubkey` is not 64 lowercase hex".into()))?;
103 let pin_path = paths.trust_root().join(USER_PIN_FILE);
104 let mut pinned = false;
105 let user_pk = match read_capped(&pin_path)? {
106 Some(t) => {
107 pinned = true;
108 let pin: [u8; 32] = hex_decode(t.trim())
109 .ok_or_else(|| cert_err(format!("{} is not 64 lowercase hex", pin_path.display())))?;
110 if pin != cert_pk {
111 return Err(cert_err(
112 "`user_pubkey` differs from the user.pub pin in the run dir".into(),
113 ));
114 }
115 pin
116 }
117 None => {
118 tracing::warn!(
119 path = %pin_path.display(),
120 "no user.pub pin: trusting the user key in the project certificate"
121 );
122 cert_pk
123 }
124 };
125 cert.verify(&user_pk, Utc::now())
126 .map_err(|e| cert_err(e.to_string()))?;
127 if Some(cert.project_id.as_str()) != paths.child_id() {
128 return Err(cert_err("`project_id` is not this kernel's project".into()));
129 }
130 if let Some(kp) = paths.project_key()
132 && let Ok(bytes) = std::fs::read(&kp)
133 && let Ok(seed) = <[u8; 32]>::try_from(bytes.as_slice())
134 {
135 let pk = ed25519_dalek::SigningKey::from_bytes(&seed)
136 .verifying_key()
137 .to_bytes();
138 if hex_encode(&pk) != cert.project_pubkey {
139 return Err(cert_err("project.key is not the certified project key".into()));
140 }
141 }
142 Ok((user_pk, pinned))
143}
144
145pub(crate) fn read_pin(paths: &RuntimePaths) -> Result<Option<u64>, OverlayError> {
148 let Some(p) = paths.state_dir().map(|d| d.join(VERSION_PIN_FILE)) else {
149 return Ok(None);
150 };
151 let bad = |reason: String| OverlayError::PinCorrupt {
152 path: p.display().to_string(),
153 reason,
154 };
155 match std::fs::read_to_string(&p) {
156 Ok(t) => t
157 .trim()
158 .parse()
159 .map(Some)
160 .map_err(|_| bad("not a version number".into())),
161 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
162 Err(e) => Err(bad(e.to_string())),
163 }
164}
165
166pub(crate) fn write_pin(paths: &RuntimePaths, version: u64) -> Result<(), OverlayError> {
167 let p = paths
168 .state_dir()
169 .ok_or(OverlayError::NotAChild)?
170 .join(VERSION_PIN_FILE);
171 write_atomic_0600(&p, version.to_string().as_bytes()).map_err(|e| OverlayError::Io {
172 path: p.display().to_string(),
173 reason: e.to_string(),
174 })
175}
176
177pub(crate) fn check_version(parent: &ParentPolicy, pinned: Option<u64>) -> Result<(), OverlayError> {
178 match pinned {
179 Some(pinned) if parent.version < pinned => Err(ParentPolicyError::Rollback {
180 have: parent.version,
181 pinned,
182 }
183 .into()),
184 _ => Ok(()),
185 }
186}