use clawft_types::project::ProjectCert;
use clawft_types::project::canon::{hex_decode, hex_encode};
use chrono::Utc;
use clawft_types::runtime_paths::RuntimePaths;
use crate::chain::ChainManager;
use crate::governance_overlay::{OverlayError, read_capped};
use crate::parent_policy::{ParentPolicy, ParentPolicyError, write_atomic_0600};
pub const VERSION_PIN_FILE: &str = "parent-policy.version";
pub const USER_PIN_FILE: &str = "user.pub";
pub use clawft_types::runtime_paths::REVOKED_FILE;
#[derive(Debug, Default)]
pub(crate) struct History {
pub last_overlay_hash: Option<String>,
pub max_parent_version: Option<u64>,
pub user_pin_used: bool,
pub any_applied: bool,
}
pub(crate) const OVERLAY_APPLIED_SOURCE: &str = "governance";
pub(crate) fn chain_history(chain: &ChainManager) -> History {
let mut h = History::default();
for e in chain
.tail(0)
.iter()
.filter(|e| e.kind == "governance.overlay.applied" && e.source == OVERLAY_APPLIED_SOURCE)
{
h.any_applied = true;
let p = e.payload.as_ref();
h.last_overlay_hash = Some(
p.and_then(|p| p.get("overlay_hash"))
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_owned(),
);
if let Some(v) = p.and_then(|p| p.get("parent_version")).and_then(|v| v.as_u64()) {
h.max_parent_version = Some(h.max_parent_version.map_or(v, |m| m.max(v)));
}
if p.and_then(|p| p.get("user_pin")).and_then(|v| v.as_bool()) == Some(true) {
h.user_pin_used = true;
}
}
h
}
pub fn write_user_pin(run_dir: &std::path::Path, user_pubkey: &[u8; 32]) -> std::io::Result<()> {
write_atomic_0600(
&run_dir.join(USER_PIN_FILE),
format!("{}\n", hex_encode(user_pubkey)).as_bytes(),
)
}
pub(crate) fn load_user_pubkey(paths: &RuntimePaths) -> Result<([u8; 32], bool), OverlayError> {
let cert_err = |m: String| OverlayError::Cert(m);
let revoked = paths.revoked_marker();
if revoked.exists() {
return Err(OverlayError::Revoked(revoked.display().to_string()));
}
let path = paths
.project_cert()
.ok_or_else(|| cert_err("no certificate path for this root".into()))?;
let text = read_capped(&path)?
.ok_or_else(|| cert_err(format!("{} does not exist", path.display())))?;
let cert: ProjectCert =
serde_json::from_str(&text).map_err(|e| cert_err(format!("does not parse: {e}")))?;
let cert_pk: [u8; 32] = hex_decode(&cert.user_pubkey)
.ok_or_else(|| cert_err("`user_pubkey` is not 64 lowercase hex".into()))?;
let pin_path = paths.trust_root().join(USER_PIN_FILE);
let mut pinned = false;
let user_pk = match read_capped(&pin_path)? {
Some(t) => {
pinned = true;
let pin: [u8; 32] = hex_decode(t.trim())
.ok_or_else(|| cert_err(format!("{} is not 64 lowercase hex", pin_path.display())))?;
if pin != cert_pk {
return Err(cert_err(
"`user_pubkey` differs from the user.pub pin in the run dir".into(),
));
}
pin
}
None => {
tracing::warn!(
path = %pin_path.display(),
"no user.pub pin: trusting the user key in the project certificate"
);
cert_pk
}
};
cert.verify(&user_pk, Utc::now())
.map_err(|e| cert_err(e.to_string()))?;
if Some(cert.project_id.as_str()) != paths.child_id() {
return Err(cert_err("`project_id` is not this kernel's project".into()));
}
if let Some(kp) = paths.project_key()
&& let Ok(bytes) = std::fs::read(&kp)
&& let Ok(seed) = <[u8; 32]>::try_from(bytes.as_slice())
{
let pk = ed25519_dalek::SigningKey::from_bytes(&seed)
.verifying_key()
.to_bytes();
if hex_encode(&pk) != cert.project_pubkey {
return Err(cert_err("project.key is not the certified project key".into()));
}
}
Ok((user_pk, pinned))
}
pub(crate) fn read_pin(paths: &RuntimePaths) -> Result<Option<u64>, OverlayError> {
let Some(p) = paths.state_dir().map(|d| d.join(VERSION_PIN_FILE)) else {
return Ok(None);
};
let bad = |reason: String| OverlayError::PinCorrupt {
path: p.display().to_string(),
reason,
};
match std::fs::read_to_string(&p) {
Ok(t) => t
.trim()
.parse()
.map(Some)
.map_err(|_| bad("not a version number".into())),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(bad(e.to_string())),
}
}
pub(crate) fn write_pin(paths: &RuntimePaths, version: u64) -> Result<(), OverlayError> {
let p = paths
.state_dir()
.ok_or(OverlayError::NotAChild)?
.join(VERSION_PIN_FILE);
write_atomic_0600(&p, version.to_string().as_bytes()).map_err(|e| OverlayError::Io {
path: p.display().to_string(),
reason: e.to_string(),
})
}
pub(crate) fn check_version(parent: &ParentPolicy, pinned: Option<u64>) -> Result<(), OverlayError> {
match pinned {
Some(pinned) if parent.version < pinned => Err(ParentPolicyError::Rollback {
have: parent.version,
pinned,
}
.into()),
_ => Ok(()),
}
}