Skip to main content

clawft_kernel/
overlay_trust.rs

1//! Trust root, rollback pin and revocation checks of a project kernel
2//! (ADR-103 D8, package E). Split from `overlay_runtime`.
3
4use 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
13/// File (under `<root>/.weftos/state/`) holding the newest accepted parent
14/// policy version.
15pub const VERSION_PIN_FILE: &str = "parent-policy.version";
16/// File in the user daemon's run dir (`<run>/<id>/user.pub`, outside the
17/// project tree) pinning the trusted user key as 64 lowercase hex. Written by
18/// the supervisor with [`write_user_pin`]. When present the child trusts only
19/// this key and refuses a certificate issued by another. It assumes whatever
20/// runs inside the project (the Phase 4 sandbox) cannot write the run dir;
21/// when absent (Phase 2 development) the child falls back to the key in its
22/// own certificate and logs a warning.
23pub const USER_PIN_FILE: &str = "user.pub";
24/// Marker the user daemon drops in the run dir when it revokes the project;
25/// its presence stops boot, reload and update. Defined once in
26/// `clawft_types::runtime_paths` (`revoked_marker`), shared with the writer
27/// and the supervisor.
28pub use clawft_types::runtime_paths::REVOKED_FILE;
29
30/// What the project chain says about earlier governance applications.
31#[derive(Debug, Default)]
32pub(crate) struct History {
33    /// Overlay hash of the newest `governance.overlay.applied` event.
34    pub last_overlay_hash: Option<String>,
35    /// Highest parent policy version any such event records.
36    pub max_parent_version: Option<u64>,
37    /// Some such event says a `user.pub` pin was in use.
38    pub user_pin_used: bool,
39    /// At least one such event exists.
40    pub any_applied: bool,
41}
42
43/// Source of the kernel's `governance.overlay.applied` events.
44pub(crate) const OVERLAY_APPLIED_SOURCE: &str = "governance";
45
46pub(crate) fn chain_history(chain: &ChainManager) -> History {
47    let mut h = History::default();
48    // Only the kernel's own records count: the source is reserved against
49    // callers (`chain::KERNEL_SOURCES`), so a forged event under another
50    // source (or this kind under any source a caller can use) is ignored
51    // rather than raising the rollback floor (review M3).
52    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
75/// Write the user-key pin the child reads (supervisor side, package G).
76pub 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
83/// The user key this child trusts, after re-checking the certificate (shape,
84/// issuer, expiry, project), the project key beside it, the out-of-tree pin
85/// and the revocation marker. Runs at boot and again on every reload and
86/// update, so an expired or revoked project stops taking policy.
87/// Returns the key and whether it came from the `user.pub` pin.
88pub(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    // The key beside the certificate must be the certified one.
131    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
145/// The pinned version: `None` when the file is absent; an unparsable file is
146/// an error (never read as "no pin").
147pub(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}