use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use cellos_core::{
cloud_event_v1_keyset_verification_failed, cloud_event_v1_keyset_verified,
load_trust_verify_keys_file, ports::EventSink, verify_signed_trust_keyset_chain,
verify_signed_trust_keyset_envelope, SignedTrustKeysetEnvelope,
};
use ed25519_dalek::VerifyingKey;
use serde_json::Value;
#[derive(Debug, Clone)]
pub enum KeysetLoadOutcome {
NotConfigured,
Verified(KeysetVerifiedDetails),
Failed(KeysetVerificationFailedDetails),
}
#[derive(Debug, Clone)]
pub struct KeysetVerifiedDetails {
pub keyset_id: String,
pub payload_digest: String,
pub verified_signer_kid: String,
}
#[derive(Debug, Clone)]
pub struct KeysetVerificationFailedDetails {
pub attempted_keyset_basename: String,
pub reason: String,
}
pub fn load_trust_verify_keys_from_env(
require_trust_verify_keys: bool,
) -> Result<Arc<HashMap<String, VerifyingKey>>, anyhow::Error> {
match std::env::var_os("CELLOS_TRUST_VERIFY_KEYS_PATH") {
None => {
if require_trust_verify_keys {
return Err(anyhow::anyhow!(
"CELLOS_REQUIRE_TRUST_VERIFY_KEYS is set but CELLOS_TRUST_VERIFY_KEYS_PATH is unset"
));
}
tracing::debug!(
target: "cellos.supervisor.trust",
"CELLOS_TRUST_VERIFY_KEYS_PATH unset; supervisor runs with empty trust keyring"
);
Ok(Arc::new(HashMap::new()))
}
Some(path_os) => {
let path = PathBuf::from(&path_os);
let keys = load_trust_verify_keys_file(&path).map_err(|e| {
anyhow::anyhow!(
"CELLOS_TRUST_VERIFY_KEYS_PATH: cannot load '{}': {e}",
path.display()
)
})?;
tracing::info!(
target: "cellos.supervisor.trust",
path = %path.display(),
kid_count = keys.len(),
"trust verifying keys loaded"
);
Ok(Arc::new(keys))
}
}
}
pub fn load_and_verify_trust_keyset_from_env(
keys: &HashMap<String, VerifyingKey>,
require_trust_verify_keys: bool,
now: std::time::SystemTime,
) -> Result<KeysetLoadOutcome, anyhow::Error> {
if let Some(chain_path_os) = std::env::var_os("CELLOS_TRUST_KEYSET_CHAIN_PATH") {
return load_and_verify_chain_from_env(chain_path_os, keys, require_trust_verify_keys, now);
}
let Some(path_os) = std::env::var_os("CELLOS_TRUST_KEYSET_PATH") else {
return Ok(KeysetLoadOutcome::NotConfigured);
};
let path = PathBuf::from(&path_os);
let basename = path
.file_name()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| "(unknown)".to_string());
match attempt_load_and_verify(&path, keys, now) {
Ok(details) => {
tracing::info!(
target: "cellos.supervisor.trust",
keyset_id = %details.keyset_id,
payload_digest = %details.payload_digest,
verified_signer_kid = %details.verified_signer_kid,
"signed trust keyset envelope verified at supervisor startup"
);
Ok(KeysetLoadOutcome::Verified(details))
}
Err(reason_string) => {
if require_trust_verify_keys {
return Err(anyhow::anyhow!(
"CELLOS_TRUST_KEYSET_PATH: verification failed under CELLOS_REQUIRE_TRUST_VERIFY_KEYS=1: {reason_string}"
));
}
tracing::warn!(
target: "cellos.supervisor.trust",
attempted_keyset_basename = %basename,
reason = %reason_string,
"signed trust keyset envelope verification failed; continuing in degraded mode"
);
Ok(KeysetLoadOutcome::Failed(KeysetVerificationFailedDetails {
attempted_keyset_basename: basename,
reason: reason_string,
}))
}
}
}
fn load_and_verify_chain_from_env(
chain_path_os: std::ffi::OsString,
keys: &HashMap<String, VerifyingKey>,
require_trust_verify_keys: bool,
now: std::time::SystemTime,
) -> Result<KeysetLoadOutcome, anyhow::Error> {
let raw_str = chain_path_os.to_string_lossy().into_owned();
let paths: Vec<PathBuf> = raw_str
.split([',', '\n'])
.map(str::trim)
.filter(|s| !s.is_empty())
.map(PathBuf::from)
.collect();
if paths.is_empty() {
let reason = "CELLOS_TRUST_KEYSET_CHAIN_PATH set but parsed no envelope paths".to_string();
if require_trust_verify_keys {
return Err(anyhow::anyhow!(
"CELLOS_TRUST_KEYSET_CHAIN_PATH: verification failed under CELLOS_REQUIRE_TRUST_VERIFY_KEYS=1: {reason}"
));
}
tracing::warn!(
target: "cellos.supervisor.trust",
reason = %reason,
"signed trust keyset chain verification failed; continuing in degraded mode"
);
return Ok(KeysetLoadOutcome::Failed(KeysetVerificationFailedDetails {
attempted_keyset_basename: "(empty chain)".into(),
reason,
}));
}
let head_basename = paths
.last()
.and_then(|p| p.file_name())
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| "(unknown)".to_string());
match attempt_load_and_verify_chain(&paths, keys, now) {
Ok(details) => {
tracing::info!(
target: "cellos.supervisor.trust",
envelope_count = paths.len(),
keyset_id = %details.keyset_id,
payload_digest = %details.payload_digest,
verified_signer_kid = %details.verified_signer_kid,
"verified {}-envelope chain (head digest: {})",
paths.len(),
details.payload_digest
);
Ok(KeysetLoadOutcome::Verified(details))
}
Err(reason_string) => {
if require_trust_verify_keys {
return Err(anyhow::anyhow!(
"CELLOS_TRUST_KEYSET_CHAIN_PATH: verification failed under CELLOS_REQUIRE_TRUST_VERIFY_KEYS=1: {reason_string}"
));
}
tracing::warn!(
target: "cellos.supervisor.trust",
attempted_keyset_basename = %head_basename,
envelope_count = paths.len(),
reason = %reason_string,
"signed trust keyset chain verification failed; continuing in degraded mode"
);
Ok(KeysetLoadOutcome::Failed(KeysetVerificationFailedDetails {
attempted_keyset_basename: head_basename,
reason: reason_string,
}))
}
}
}
fn attempt_load_and_verify_chain(
paths: &[PathBuf],
keys: &HashMap<String, VerifyingKey>,
now: std::time::SystemTime,
) -> Result<KeysetVerifiedDetails, String> {
let mut chain: Vec<SignedTrustKeysetEnvelope> = Vec::with_capacity(paths.len());
for path in paths {
let raw =
read_envelope_file(path).map_err(|e| format!("cannot read {}: {e}", path.display()))?;
let envelope: SignedTrustKeysetEnvelope = serde_json::from_str(&raw)
.map_err(|e| format!("JSON parse error in {}: {e}", path.display()))?;
chain.push(envelope);
}
let head_payload_bytes =
verify_signed_trust_keyset_chain(&chain, keys, now).map_err(|e| format!("{e}"))?;
let head_envelope = chain.last().expect("chain non-empty");
let verified_signer_kid =
pick_verified_signer_kid(head_envelope, &head_payload_bytes, keys, now)
.unwrap_or_else(|| "(unknown)".to_string());
let keyset_id =
decode_inner_keyset_id(&head_payload_bytes).unwrap_or_else(|| "(unknown)".into());
Ok(KeysetVerifiedDetails {
keyset_id,
payload_digest: head_envelope.payload_digest.clone(),
verified_signer_kid,
})
}
fn attempt_load_and_verify(
path: &Path,
keys: &HashMap<String, VerifyingKey>,
now: std::time::SystemTime,
) -> Result<KeysetVerifiedDetails, String> {
let raw =
read_envelope_file(path).map_err(|e| format!("cannot read {}: {e}", path.display()))?;
let envelope: SignedTrustKeysetEnvelope = serde_json::from_str(&raw)
.map_err(|e| format!("JSON parse error in {}: {e}", path.display()))?;
let payload_bytes =
verify_signed_trust_keyset_envelope(&envelope, keys, now).map_err(|e| format!("{e}"))?;
let verified_signer_kid = pick_verified_signer_kid(&envelope, &payload_bytes, keys, now)
.unwrap_or_else(|| "(unknown)".to_string());
let keyset_id = decode_inner_keyset_id(&payload_bytes).unwrap_or_else(|| "(unknown)".into());
Ok(KeysetVerifiedDetails {
keyset_id,
payload_digest: envelope.payload_digest.clone(),
verified_signer_kid,
})
}
fn pick_verified_signer_kid(
envelope: &SignedTrustKeysetEnvelope,
payload_bytes: &[u8],
keys: &HashMap<String, VerifyingKey>,
now: std::time::SystemTime,
) -> Option<String> {
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine as _;
use chrono::DateTime;
use ed25519_dalek::Signature;
for sig in &envelope.signatures {
if sig.algorithm != "ed25519" {
continue;
}
let Some(verifying_key) = keys.get(&sig.signer_kid) else {
continue;
};
if !window_contains(now, sig.not_before.as_deref(), sig.not_after.as_deref()) {
continue;
}
let sig_b64 = sig.signature.trim_end_matches('=');
let Ok(sig_bytes) = URL_SAFE_NO_PAD.decode(sig_b64) else {
continue;
};
let Ok(sig_array) = <[u8; 64]>::try_from(sig_bytes.as_slice()) else {
continue;
};
let signature = Signature::from_bytes(&sig_array);
if verifying_key
.verify_strict(payload_bytes, &signature)
.is_ok()
{
return Some(sig.signer_kid.clone());
}
}
let _ = DateTime::<chrono::Utc>::from_timestamp(0, 0);
None
}
fn window_contains(
now: std::time::SystemTime,
not_before: Option<&str>,
not_after: Option<&str>,
) -> bool {
use chrono::DateTime;
let now_chrono: DateTime<chrono::Utc> = now.into();
if let Some(nb) = not_before {
match DateTime::parse_from_rfc3339(nb) {
Ok(t) => {
if now_chrono < t.with_timezone(&chrono::Utc) {
return false;
}
}
Err(_) => return false,
}
}
if let Some(na) = not_after {
match DateTime::parse_from_rfc3339(na) {
Ok(t) => {
if now_chrono > t.with_timezone(&chrono::Utc) {
return false;
}
}
Err(_) => return false,
}
}
true
}
fn decode_inner_keyset_id(payload_bytes: &[u8]) -> Option<String> {
let v: Value = serde_json::from_slice(payload_bytes).ok()?;
v.as_object()?.get("keysetId")?.as_str().map(String::from)
}
fn read_envelope_file(path: &Path) -> Result<String, std::io::Error> {
#[cfg(unix)]
{
use std::io::Read;
use std::os::unix::fs::OpenOptionsExt;
let mut opts = std::fs::OpenOptions::new();
opts.read(true);
opts.custom_flags(libc::O_RDONLY | libc::O_NOFOLLOW);
let mut file = opts.open(path)?;
let mut buf = String::new();
file.read_to_string(&mut buf)?;
Ok(buf)
}
#[cfg(not(unix))]
{
std::fs::read_to_string(path)
}
}
pub async fn emit_keyset_outcome(
outcome: &KeysetLoadOutcome,
event_sink: &Arc<dyn EventSink>,
jsonl_sink: Option<&Arc<dyn EventSink>>,
now: chrono::DateTime<chrono::Utc>,
) -> Result<(), anyhow::Error> {
let timestamp = now.to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
match outcome {
KeysetLoadOutcome::NotConfigured => Ok(()),
KeysetLoadOutcome::Verified(details) => {
let envelope = cloud_event_v1_keyset_verified(
"cellos-supervisor",
×tamp,
&details.keyset_id,
&details.payload_digest,
&details.verified_signer_kid,
×tamp,
None,
)?;
event_sink
.emit(&envelope)
.await
.map_err(|e| anyhow::anyhow!("emit keyset_verified: {e}"))?;
if let Some(secondary) = jsonl_sink {
secondary
.emit(&envelope)
.await
.map_err(|e| anyhow::anyhow!("emit keyset_verified to jsonl sink: {e}"))?;
}
Ok(())
}
KeysetLoadOutcome::Failed(details) => {
let envelope = cloud_event_v1_keyset_verification_failed(
"cellos-supervisor",
×tamp,
&details.attempted_keyset_basename,
&details.reason,
×tamp,
None,
)?;
event_sink
.emit(&envelope)
.await
.map_err(|e| anyhow::anyhow!("emit keyset_verification_failed: {e}"))?;
if let Some(secondary) = jsonl_sink {
secondary.emit(&envelope).await.map_err(|e| {
anyhow::anyhow!("emit keyset_verification_failed to jsonl sink: {e}")
})?;
}
Ok(())
}
}
}
#[cfg(test)]
mod tests {
use super::{
load_and_verify_trust_keyset_from_env, load_trust_verify_keys_from_env, KeysetLoadOutcome,
};
use std::sync::{Mutex, MutexGuard};
static ENV_MUTEX: Mutex<()> = Mutex::new(());
fn lock_env() -> MutexGuard<'static, ()> {
ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner())
}
#[test]
fn unset_path_with_require_unset_yields_empty_map() {
let _guard = lock_env();
std::env::remove_var("CELLOS_TRUST_VERIFY_KEYS_PATH");
let keys = load_trust_verify_keys_from_env(false).expect("legacy posture");
assert!(keys.is_empty());
}
#[test]
fn unset_path_with_require_set_errors() {
let _guard = lock_env();
std::env::remove_var("CELLOS_TRUST_VERIFY_KEYS_PATH");
let err = load_trust_verify_keys_from_env(true).expect_err("require set + unset path");
assert!(format!("{err}").contains("CELLOS_TRUST_VERIFY_KEYS_PATH"));
}
#[test]
fn keyset_path_unset_returns_not_configured() {
let _guard = lock_env();
std::env::remove_var("CELLOS_TRUST_KEYSET_PATH");
let outcome = load_and_verify_trust_keyset_from_env(
&Default::default(),
false,
std::time::SystemTime::now(),
)
.expect("unset path + fail-open");
assert!(matches!(outcome, KeysetLoadOutcome::NotConfigured));
}
}