use std::env;
use std::fs;
use ed25519_dalek::VerifyingKey;
pub const TRUSTED_KEYS_DIR_ENV: &str = "KELEUSMA_TRUSTED_KEYS_DIR";
pub const REQUIRE_SIGNED_ENV: &str = "KELEUSMA_REQUIRE_SIGNED";
pub const DECRYPTION_KEYS_DIR_ENV: &str = "KELEUSMA_DECRYPTION_KEYS_DIR";
pub const REQUIRE_ENCRYPTED_ENV: &str = "KELEUSMA_REQUIRE_ENCRYPTED";
pub const X25519_PRIVATE_KEY_LEN: usize = 32;
#[derive(Debug, Clone, Default)]
pub struct PolicyContext {
pub enrolled_keys: Vec<VerifyingKey>,
pub strict_signing: bool,
pub decryption_keys: Vec<[u8; X25519_PRIVATE_KEY_LEN]>,
pub strict_encryption: bool,
}
pub fn build_policy_context() -> Result<PolicyContext, String> {
let enrolled_keys = discover_trusted_keys()?;
let strict_signing = !enrolled_keys.is_empty() || is_strict_signing_forced();
let decryption_keys = discover_decryption_keys()?;
let strict_encryption = !decryption_keys.is_empty() || is_strict_encryption_forced();
Ok(PolicyContext {
enrolled_keys,
strict_signing,
decryption_keys,
strict_encryption,
})
}
pub fn discover_trusted_keys() -> Result<Vec<VerifyingKey>, String> {
let (dir, explicit) = match env::var(TRUSTED_KEYS_DIR_ENV) {
Ok(d) => (d, true),
Err(_) => match default_trusted_keys_dir() {
Some(d) => (d, false),
None => return Ok(Vec::new()),
},
};
let entries = match fs::read_dir(&dir) {
Ok(e) => e,
Err(_) if !explicit => {
return Ok(Vec::new());
}
Err(e) => {
return Err(format!(
"strict mode: cannot read trusted-keys directory {}: {}",
dir, e
));
}
};
let mut keys = Vec::new();
for entry in entries {
let entry = entry
.map_err(|e| format!("strict mode: directory iteration error in {}: {}", dir, e))?;
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) != Some("pub") {
continue;
}
if !path.is_file() {
continue;
}
let bytes = fs::read(&path).map_err(|e| {
format!(
"strict mode: reading enrolled key {}: {}",
path.display(),
e
)
})?;
if bytes.len() != 32 {
return Err(format!(
"strict mode: trust store contains a malformed key file at {} ({} bytes; expected 32); refusing to start",
path.display(),
bytes.len()
));
}
let mut key_bytes = [0u8; 32];
key_bytes.copy_from_slice(&bytes);
let key = VerifyingKey::from_bytes(&key_bytes).map_err(|e| {
format!(
"strict mode: invalid Ed25519 public key in {}: {}",
path.display(),
e
)
})?;
keys.push(key);
}
Ok(keys)
}
fn default_trusted_keys_dir() -> Option<String> {
#[cfg(unix)]
{
Some(String::from("/etc/keleusma/trusted_keys"))
}
#[cfg(windows)]
{
env::var("PROGRAMDATA")
.ok()
.map(|p| format!("{}\\keleusma\\trusted_keys", p))
}
#[cfg(not(any(unix, windows)))]
{
None
}
}
fn is_strict_signing_forced() -> bool {
matches!(env::var(REQUIRE_SIGNED_ENV).as_deref(), Ok("1"))
}
fn is_strict_encryption_forced() -> bool {
matches!(env::var(REQUIRE_ENCRYPTED_ENV).as_deref(), Ok("1"))
}
pub fn discover_decryption_keys() -> Result<Vec<[u8; X25519_PRIVATE_KEY_LEN]>, String> {
let (dir, explicit) = match env::var(DECRYPTION_KEYS_DIR_ENV) {
Ok(d) => (d, true),
Err(_) => match default_decryption_keys_dir() {
Some(d) => (d, false),
None => return Ok(Vec::new()),
},
};
let entries = match fs::read_dir(&dir) {
Ok(e) => e,
Err(_) if !explicit => return Ok(Vec::new()),
Err(e) => {
return Err(format!(
"strict mode: cannot read decryption-keys directory {}: {}",
dir, e
));
}
};
let mut keys = Vec::new();
for entry in entries {
let entry = entry
.map_err(|e| format!("strict mode: directory iteration error in {}: {}", dir, e))?;
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) != Some("seed") {
continue;
}
if !path.is_file() {
continue;
}
let bytes = fs::read(&path).map_err(|e| {
format!(
"strict mode: reading decryption key {}: {}",
path.display(),
e
)
})?;
if bytes.len() != X25519_PRIVATE_KEY_LEN {
return Err(format!(
"strict mode: decryption key store contains a malformed key file at {} ({} bytes; expected {}); refusing to start",
path.display(),
bytes.len(),
X25519_PRIVATE_KEY_LEN,
));
}
let mut key_bytes = [0u8; X25519_PRIVATE_KEY_LEN];
key_bytes.copy_from_slice(&bytes);
keys.push(key_bytes);
}
Ok(keys)
}
fn default_decryption_keys_dir() -> Option<String> {
#[cfg(unix)]
{
Some(String::from("/etc/keleusma/decryption_keys"))
}
#[cfg(windows)]
{
env::var("PROGRAMDATA")
.ok()
.map(|p| format!("{}\\keleusma\\decryption_keys", p))
}
#[cfg(not(any(unix, windows)))]
{
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
static ENV_GUARD: Mutex<()> = Mutex::new(());
fn with_env<F: FnOnce()>(vars: &[(&str, Option<&str>)], f: F) {
let _guard = ENV_GUARD.lock().unwrap_or_else(|e| e.into_inner());
let mut originals: Vec<(String, Option<String>)> = Vec::new();
for (k, v) in vars {
originals.push(((*k).to_string(), env::var(k).ok()));
unsafe {
match v {
Some(value) => env::set_var(k, value),
None => env::remove_var(k),
}
}
}
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
for (k, v) in originals {
unsafe {
match v {
Some(val) => env::set_var(&k, &val),
None => env::remove_var(&k),
}
}
}
if let Err(e) = result {
std::panic::resume_unwind(e);
}
}
#[test]
fn empty_trust_store_yields_permissive_mode() {
with_env(
&[
(
TRUSTED_KEYS_DIR_ENV,
Some("/nonexistent/keleusma/test/empty"),
),
(REQUIRE_SIGNED_ENV, None),
],
|| {
let err = discover_trusted_keys();
assert!(err.is_err(), "expected error for missing explicit dir");
},
);
}
#[test]
fn unset_env_yields_permissive_when_default_dir_missing() {
with_env(
&[(TRUSTED_KEYS_DIR_ENV, None), (REQUIRE_SIGNED_ENV, None)],
|| match discover_trusted_keys() {
Ok(keys) => assert!(keys.is_empty(), "expected no keys, got {}", keys.len()),
Err(e) => panic!("expected empty trust list, got error: {}", e),
},
);
}
#[test]
fn force_strict_signing_env_var_recognised() {
with_env(&[(REQUIRE_SIGNED_ENV, Some("1"))], || {
assert!(
is_strict_signing_forced(),
"expected force-strict to be active"
);
});
with_env(&[(REQUIRE_SIGNED_ENV, Some("0"))], || {
assert!(
!is_strict_signing_forced(),
"expected force-strict to be inactive when not 1"
);
});
with_env(&[(REQUIRE_SIGNED_ENV, None)], || {
assert!(
!is_strict_signing_forced(),
"expected force-strict to be inactive when unset"
);
});
}
#[test]
fn force_strict_encryption_env_var_recognised() {
with_env(&[(REQUIRE_ENCRYPTED_ENV, Some("1"))], || {
assert!(is_strict_encryption_forced());
});
with_env(&[(REQUIRE_ENCRYPTED_ENV, None)], || {
assert!(!is_strict_encryption_forced());
});
}
#[test]
fn discovery_loads_well_formed_decryption_keys() {
let dir =
std::env::temp_dir().join(format!("keleusma_test_decrypt_{}", std::process::id()));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).expect("create temp dir");
let key1 = [0xa1u8; 32];
let key2 = [0xb2u8; 32];
fs::write(dir.join("workstation_42.seed"), key1).expect("write key1");
fs::write(dir.join("workstation_99.seed"), key2).expect("write key2");
fs::write(dir.join("readme.txt"), b"ignore me").expect("write readme");
with_env(
&[(DECRYPTION_KEYS_DIR_ENV, Some(dir.to_str().unwrap()))],
|| {
let keys = discover_decryption_keys().expect("discovery succeeds");
assert_eq!(keys.len(), 2);
},
);
fs::remove_dir_all(&dir).expect("cleanup");
}
#[test]
fn decryption_key_discovery_rejects_malformed() {
let dir = std::env::temp_dir().join(format!(
"keleusma_test_decrypt_malformed_{}",
std::process::id()
));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).expect("create temp dir");
fs::write(dir.join("bad.seed"), b"too short").expect("write bad");
with_env(
&[(DECRYPTION_KEYS_DIR_ENV, Some(dir.to_str().unwrap()))],
|| {
let result = discover_decryption_keys();
assert!(result.is_err(), "expected fail-closed rejection");
let msg = result.unwrap_err();
assert!(
msg.contains("malformed"),
"expected 'malformed' in diagnostic; got: {}",
msg
);
},
);
fs::remove_dir_all(&dir).expect("cleanup");
}
#[test]
fn discovery_loads_well_formed_keys() {
let dir = std::env::temp_dir().join(format!("keleusma_test_trust_{}", std::process::id()));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).expect("create temp dir");
let key1 = ed25519_dalek::SigningKey::generate(&mut rand_core::OsRng).verifying_key();
let key2 = ed25519_dalek::SigningKey::generate(&mut rand_core::OsRng).verifying_key();
fs::write(dir.join("alice.pub"), key1.to_bytes()).expect("write alice");
fs::write(dir.join("bob.pub"), key2.to_bytes()).expect("write bob");
fs::write(dir.join("readme.txt"), b"ignore me").expect("write readme");
with_env(
&[(TRUSTED_KEYS_DIR_ENV, Some(dir.to_str().unwrap()))],
|| {
let keys = discover_trusted_keys().expect("discovery succeeds");
assert_eq!(keys.len(), 2, "expected 2 keys, got {}", keys.len());
},
);
fs::remove_dir_all(&dir).expect("cleanup");
}
#[test]
fn discovery_rejects_malformed_key_files() {
let dir =
std::env::temp_dir().join(format!("keleusma_test_malformed_{}", std::process::id()));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).expect("create temp dir");
fs::write(dir.join("bad.pub"), b"too short").expect("write bad key");
with_env(
&[(TRUSTED_KEYS_DIR_ENV, Some(dir.to_str().unwrap()))],
|| {
let result = discover_trusted_keys();
assert!(result.is_err(), "expected fail-closed rejection");
let msg = result.unwrap_err();
assert!(
msg.contains("malformed"),
"expected 'malformed' in diagnostic; got: {}",
msg
);
},
);
fs::remove_dir_all(&dir).expect("cleanup");
}
#[test]
fn policy_context_reflects_force_strict() {
with_env(
&[
(TRUSTED_KEYS_DIR_ENV, None),
(REQUIRE_SIGNED_ENV, Some("1")),
],
|| {
let ctx = build_policy_context().expect("policy context builds");
assert!(ctx.enrolled_keys.is_empty());
assert!(
ctx.strict_signing,
"force-strict should activate strict mode"
);
},
);
}
}