use std::path::Path;
use std::sync::Arc;
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
use clawft_types::placement::vocabulary::MAX_VOCAB_BYTES;
use clawft_types::placement::vocabulary_pin::{GENESIS_EVENT, MAX_PIN_BYTES};
use clawft_types::placement::{
VOCABULARY_CONFIG_KEY, VOCABULARY_CONFIG_NAMESPACE, Vocabulary, VocabularyChange, VocabularyPin,
};
use crate::chain::{ChainManager, EVENT_KIND_CONFIG_SET};
use crate::error::KernelError;
use crate::gate::{GateBackend, GateDecision};
use crate::governance::{GateEffectKind, with_effect_context};
use crate::workload_pkg::codec::{hex_decode_exact, hex_encode, is_lower_hex};
use crate::workload_pkg::{TrustAnchors, key_id_for};
pub const BASELINE_VOCABULARY_DIGEST: &str =
"927ed3db1359a4d0193277bf53efb424280e49f18e204c295bb47b8a885ff4c5";
pub const VOCABULARY_GATE_ACTION: &str = "config.set";
pub const VOCABULARY_FILE: &str = "capabilities.toml";
pub const VOCABULARY_PIN_FILE: &str = "capabilities.pin.toml";
pub const VOCABULARY_FLOOR_FILE: &str = "capabilities.floor";
fn config_err(e: impl std::fmt::Display) -> KernelError {
KernelError::Config(format!("capability vocabulary: {e}"))
}
pub fn verify_pin(pin: &VocabularyPin, anchors: &TrustAnchors) -> Result<(), KernelError> {
let Some(sig_hex) = &pin.signature else {
let baseline = pin.version == 1
&& pin.event_hash.as_deref() == Some(GENESIS_EVENT)
&& pin.event_seq.is_none()
&& pin.key_id.is_none()
&& pin.digest == BASELINE_VOCABULARY_DIGEST;
return if baseline {
Ok(())
} else {
Err(config_err(
"pin is unsigned and is not the compiled-in baseline",
))
};
};
let event = pin.event_hash.as_deref().unwrap_or("");
if !is_lower_hex(event, 64) || pin.event_seq.is_none() {
return Err(config_err("signed pin is not bound to a chain event"));
}
let key_id = pin.key_id.as_deref().unwrap_or("");
let signer = anchors
.signers
.iter()
.find(|k| k.key_id == key_id)
.ok_or_else(|| config_err(format!("pin signer {key_id:?} is not a trusted key")))?;
let key = VerifyingKey::from_bytes(&signer.public_key).map_err(config_err)?;
let sig = hex_decode_exact::<64>(sig_hex)
.map(|b| Signature::from_bytes(&b))
.ok_or_else(|| config_err("pin signature is malformed"))?;
key.verify(&pin.signed_statement(), &sig)
.map_err(|_| config_err("pin signature does not verify"))
}
pub fn verify_pin_event(pin: &VocabularyPin, chain: &ChainManager) -> Result<(), KernelError> {
let (Some(seq), Some(hash)) = (pin.event_seq, pin.event_hash.as_deref()) else {
return Err(config_err("pin is not bound to a chain event"));
};
let ev = chain
.tail_from(seq.saturating_sub(1))
.into_iter()
.find(|e| e.sequence == seq)
.ok_or_else(|| config_err(format!("chain has no event {seq}")))?;
let digest_ok = ev
.payload
.as_ref()
.and_then(|p| p.pointer("/pin/digest"))
.and_then(|d| d.as_str())
== Some(pin.digest.as_str());
let version_ok = ev
.payload
.as_ref()
.and_then(|p| p.pointer("/pin/version"))
.and_then(|v| v.as_u64())
== Some(u64::from(pin.version));
if hex_encode(&ev.hash) != hash
|| ev.source != "placement"
|| ev.kind != EVENT_KIND_CONFIG_SET
|| !digest_ok
|| !version_ok
{
return Err(config_err(format!(
"chain event {seq} does not match the pin"
)));
}
Ok(())
}
pub fn load_governed(
vocab_text: &str,
pin_text: &str,
anchors: &TrustAnchors,
floor: u32,
) -> Result<Vocabulary, KernelError> {
let pin = VocabularyPin::from_toml_str(pin_text).map_err(config_err)?;
verify_pin(&pin, anchors)?;
if pin.version < floor {
return Err(config_err(format!(
"pin version {} is below the accepted floor {floor}; refusing a rollback",
pin.version
)));
}
Vocabulary::from_toml_pinned(vocab_text, &pin).map_err(config_err)
}
pub fn chain_version_floor(chain: &ChainManager) -> u32 {
chain
.tail(chain.len())
.iter()
.filter(|e| e.source == "placement" && e.kind == EVENT_KIND_CONFIG_SET)
.filter_map(|e| {
let p = e.payload.as_ref()?;
(p.get("key")?.as_str()? == VOCABULARY_CONFIG_KEY)
.then(|| p.pointer("/pin/version")?.as_u64())
.flatten()
})
.max()
.map_or(0, |v| u32::try_from(v).unwrap_or(u32::MAX))
}
fn read_floor(dir: &Path) -> Result<u32, KernelError> {
match std::fs::read_to_string(dir.join(VOCABULARY_FLOOR_FILE)) {
Ok(t) => t
.trim()
.parse()
.map_err(|_| config_err("capabilities.floor is not a version number")),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(0),
Err(e) => Err(config_err(format!("capabilities.floor: {e}"))),
}
}
fn record_floor(dir: &Path, version: u32) -> Result<(), KernelError> {
let tmp = dir.join(format!(".{VOCABULARY_FLOOR_FILE}.tmp"));
std::fs::write(&tmp, format!("{version}\n"))
.and_then(|_| std::fs::rename(&tmp, dir.join(VOCABULARY_FLOOR_FILE)))
.map_err(|e| config_err(format!("capabilities.floor: {e}")))
}
fn read_bounded(path: &Path, max: usize) -> Result<String, KernelError> {
let len = std::fs::metadata(path)
.map_err(|e| config_err(format!("{}: {e}", path.display())))?
.len();
if len > max as u64 {
return Err(config_err(format!(
"{} is larger than {max} bytes",
path.display()
)));
}
std::fs::read_to_string(path).map_err(|e| config_err(format!("{}: {e}", path.display())))
}
pub fn load_governed_dir(config_dir: &Path) -> Result<Vocabulary, KernelError> {
let anchors = TrustAnchors::weftos_default().map_err(config_err)?;
load_governed_dir_with(config_dir, &anchors)
}
pub fn load_governed_dir_with(
config_dir: &Path,
anchors: &TrustAnchors,
) -> Result<Vocabulary, KernelError> {
load_dir(config_dir, anchors, None)
}
pub fn load_governed_dir_on_chain(
config_dir: &Path,
anchors: &TrustAnchors,
chain: &ChainManager,
) -> Result<Vocabulary, KernelError> {
load_dir(config_dir, anchors, Some(chain))
}
fn load_dir(
config_dir: &Path,
anchors: &TrustAnchors,
chain: Option<&ChainManager>,
) -> Result<Vocabulary, KernelError> {
let vocab = read_bounded(&config_dir.join(VOCABULARY_FILE), MAX_VOCAB_BYTES)?;
let pin_text = read_bounded(&config_dir.join(VOCABULARY_PIN_FILE), MAX_PIN_BYTES)?;
let file_floor = read_floor(config_dir)?;
let floor = file_floor.max(chain.map_or(0, chain_version_floor));
let v = load_governed(&vocab, &pin_text, anchors, floor)?;
let pin = VocabularyPin::from_toml_str(&pin_text).map_err(config_err)?;
if pin.signature.is_some() {
if let Some(chain) = chain {
verify_pin_event(&pin, chain)?;
}
if pin.version > file_floor {
record_floor(config_dir, pin.version)?;
}
}
Ok(v)
}
#[derive(Debug, Clone)]
pub struct ApprovedVocabularyChange {
pub vocabulary: Vocabulary,
pub pin: VocabularyPin,
pub change: VocabularyChange,
}
pub struct VocabularyGovernor {
gate: Arc<dyn GateBackend>,
chain: Option<Arc<ChainManager>>,
signer: Option<(SigningKey, String)>,
}
impl VocabularyGovernor {
pub fn new(gate: Arc<dyn GateBackend>) -> Self {
Self {
gate,
chain: None,
signer: None,
}
}
pub fn with_signer(mut self, key: SigningKey) -> Self {
let id = key_id_for(key.verifying_key().as_bytes());
self.signer = Some((key, id));
self
}
pub fn with_chain(mut self, chain: Arc<ChainManager>) -> Self {
self.chain = Some(chain);
self
}
pub fn propose(
&self,
requester: &str,
current: &Vocabulary,
proposed_text: &str,
) -> Result<ApprovedVocabularyChange, KernelError> {
let (Some(chain), Some((key, key_id))) = (&self.chain, &self.signer) else {
return Err(config_err(
"governor has no chain or signing key; refusing to issue a pin",
));
};
let proposed = Vocabulary::from_toml_str(proposed_text).map_err(config_err)?;
let change = current.change_to(&proposed).map_err(config_err)?;
let ctx = with_effect_context(
serde_json::json!({
"namespace": VOCABULARY_CONFIG_NAMESPACE,
"key": VOCABULARY_CONFIG_KEY,
"requester": requester,
"from_digest": change.from_digest,
"to_digest": change.to_digest,
"from_version": change.from_version,
"to_version": change.to_version,
}),
&GateEffectKind::ConfigSet.effect_vector(),
);
match self.gate.check(requester, VOCABULARY_GATE_ACTION, &ctx) {
GateDecision::Permit { .. } => {}
GateDecision::Deny { reason, .. } => {
return Err(KernelError::GovernanceDenied(format!(
"vocabulary change denied: {reason}"
)));
}
GateDecision::Defer { reason } => {
return Err(KernelError::GovernanceDenied(format!(
"vocabulary change deferred for review: {reason}"
)));
}
#[allow(unreachable_patterns)]
_ => {
return Err(KernelError::GovernanceDenied(
"vocabulary change: unrecognised gate decision".into(),
));
}
}
let mut pin = VocabularyPin::of(&proposed);
let event = chain.append(
"placement",
EVENT_KIND_CONFIG_SET,
Some(serde_json::json!({
"namespace": VOCABULARY_CONFIG_NAMESPACE,
"key": VOCABULARY_CONFIG_KEY,
"requester": requester,
"change": change,
"pin": pin,
})),
);
pin.key_id = Some(key_id.clone());
pin.event_seq = Some(event.sequence);
pin.event_hash = Some(hex_encode(&event.hash));
pin.signature = Some(hex_encode(&key.sign(&pin.signed_statement()).to_bytes()));
Ok(ApprovedVocabularyChange {
vocabulary: proposed,
pin,
change,
})
}
}
#[cfg(test)]
#[path = "placement_vocabulary_tests.rs"]
mod tests;