use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
#[must_use]
pub fn current_agent_id() -> &'static str {
static CACHE: OnceLock<String> = OnceLock::new();
CACHE.get_or_init(|| {
std::env::var("LEAN_CTX_AGENT_ID")
.or_else(|_| std::env::var("LCTX_AGENT_ID"))
.unwrap_or_else(|_| "local".to_string())
})
}
static SESSION_ATTRIBUTION: OnceLock<String> = OnceLock::new();
static SESSION_ATTRIBUTION_CONFLICT: std::sync::atomic::AtomicBool =
std::sync::atomic::AtomicBool::new(false);
#[must_use]
pub(crate) fn ledger_agent_id() -> String {
let explicit = current_agent_id();
if explicit != "local" {
return explicit.to_string();
}
if !SESSION_ATTRIBUTION_CONFLICT.load(std::sync::atomic::Ordering::Relaxed)
&& let Some(session) = SESSION_ATTRIBUTION.get()
{
return session.clone();
}
host_from_env(|key| std::env::var(key).ok())
.unwrap_or("local")
.to_string()
}
pub fn set_session_attribution(client_name: &str, bus_agent_id: Option<&str>) {
let Some(value) = session_attribution_value(client_name, bus_agent_id) else {
return;
};
if let Err(value) = SESSION_ATTRIBUTION.set(value)
&& SESSION_ATTRIBUTION.get() != Some(&value)
{
SESSION_ATTRIBUTION_CONFLICT.store(true, std::sync::atomic::Ordering::Relaxed);
}
}
fn session_attribution_value(client_name: &str, bus_agent_id: Option<&str>) -> Option<String> {
let client = attribution_component(client_name, 48);
let agent = bus_agent_id.and_then(|id| attribution_component(id, 72));
match (client, agent) {
(Some(client), Some(agent)) => Some(format!("{client}:{agent}")),
(Some(single), None) | (None, Some(single)) => Some(single),
(None, None) => None,
}
}
fn attribution_component(raw: &str, max: usize) -> Option<String> {
let folded: String = raw
.trim()
.chars()
.map(|c| {
let c = c.to_ascii_lowercase();
if c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-') {
c
} else {
'-'
}
})
.take(max)
.collect();
let trimmed = folded.trim_matches('-');
(!trimmed.is_empty()).then(|| trimmed.to_string())
}
pub fn host_from_env(env: impl Fn(&str) -> Option<String>) -> Option<&'static str> {
let set = |key: &str| env(key).is_some_and(|v| !v.trim().is_empty());
if set("CLAUDECODE") {
Some("claude")
} else if set("CODEX_THREAD_ID") {
Some("codex")
} else if set("CURSOR_TASK_ID") {
Some("cursor")
} else {
None
}
}
#[must_use]
pub fn delivery_agent_id() -> &'static str {
static CACHE: OnceLock<String> = OnceLock::new();
CACHE.get_or_init(|| {
resolve_delivery_agent_id(|key| std::env::var(key).ok(), std::process::id())
})
}
fn resolve_delivery_agent_id(env: impl Fn(&str) -> Option<String>, pid: u32) -> String {
let non_empty = |key: &str| {
env(key)
.map(|v| v.trim().to_string())
.filter(|v| !v.is_empty())
};
if let Some(id) = non_empty("LEAN_CTX_AGENT_ID").or_else(|| non_empty("LCTX_AGENT_ID")) {
return id;
}
if let Some(task) = non_empty("CURSOR_TASK_ID") {
return task;
}
if non_empty("CLAUDECODE").is_some() {
return format!("claude-{pid}");
}
if non_empty("CODEX_THREAD_ID").is_some() {
return format!("codex-{pid}");
}
format!("local-{pid}")
}
pub fn get_or_create_keypair(agent_id: &str) -> Result<SigningKey, String> {
let path = key_path(agent_id)?;
if path.exists() {
load_key(&path)
} else {
generate_and_save(agent_id)
}
}
pub(crate) fn get_public_key(agent_id: &str) -> Result<VerifyingKey, String> {
let key = get_or_create_keypair(agent_id)?;
Ok(key.verifying_key())
}
pub(crate) fn get_stored_signing_key(agent_id: &str) -> Result<SigningKey, String> {
load_key(&key_path(agent_id)?)
}
pub fn get_stored_public_key(agent_id: &str) -> Result<VerifyingKey, String> {
let bytes = read_key_file(&pub_key_path(agent_id)?, "trusted public key")?;
let bytes: [u8; 32] = bytes
.try_into()
.map_err(|_| "invalid public key file (expected 32 bytes)".to_string())?;
VerifyingKey::from_bytes(&bytes).map_err(|error| format!("invalid public key: {error}"))
}
pub(crate) fn load_public_key(agent_id: &str) -> Result<VerifyingKey, String> {
get_stored_public_key(agent_id)
}
pub(crate) fn sign_bytes(agent_id: &str, data: &[u8]) -> Result<Vec<u8>, String> {
let key = get_or_create_keypair(agent_id)?;
let sig = key.sign(data);
Ok(sig.to_bytes().to_vec())
}
#[must_use]
pub fn sign_bytes_with(key: &SigningKey, data: &[u8]) -> Vec<u8> {
key.sign(data).to_bytes().to_vec()
}
pub fn verify_signature(public_key_bytes: &[u8], data: &[u8], signature_bytes: &[u8]) -> bool {
let pk_bytes: [u8; 32] = match public_key_bytes.try_into() {
Ok(b) => b,
Err(_) => return false,
};
let Ok(verifying_key) = VerifyingKey::from_bytes(&pk_bytes) else {
return false;
};
let sig_bytes: [u8; 64] = match signature_bytes.try_into() {
Ok(b) => b,
Err(_) => return false,
};
let signature = Signature::from_bytes(&sig_bytes);
verifying_key.verify(data, &signature).is_ok()
}
pub fn hex_encode(bytes: &[u8]) -> String {
use std::fmt::Write;
bytes.iter().fold(String::new(), |mut s, b| {
let _ = write!(s, "{b:02x}");
s
})
}
pub fn hex_decode(s: &str) -> Result<Vec<u8>, String> {
if !s.len().is_multiple_of(2) {
return Err("odd-length hex string".to_string());
}
if !s.is_ascii() {
return Err("non-ASCII hex string".to_string());
}
(0..s.len())
.step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).map_err(|e| e.to_string()))
.collect()
}
fn key_path(agent_id: &str) -> Result<PathBuf, String> {
validate_agent_id(agent_id)?;
let base = crate::core::data_dir::lean_ctx_data_dir()?;
Ok(base.join("keys").join(format!("{agent_id}.key")))
}
fn pub_key_path(agent_id: &str) -> Result<PathBuf, String> {
validate_agent_id(agent_id)?;
let base = crate::core::data_dir::lean_ctx_data_dir()?;
Ok(base.join("keys").join(format!("{agent_id}.pub")))
}
fn validate_agent_id(agent_id: &str) -> Result<(), String> {
if agent_id.is_empty()
|| agent_id.len() > 128
|| !agent_id.is_ascii()
|| !agent_id.as_bytes()[0].is_ascii_alphanumeric()
|| !agent_id
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || b"._-".contains(&byte))
|| agent_id.as_bytes().windows(2).any(|window| window == b"..")
{
return Err("invalid agent identity id".to_string());
}
Ok(())
}
fn generate_and_save(agent_id: &str) -> Result<SigningKey, String> {
let mut seed = [0u8; 32];
getrandom::fill(&mut seed).map_err(|e| format!("CSPRNG unavailable: {e}"))?;
let signing_key = SigningKey::from_bytes(&seed);
let key_file = key_path(agent_id)?;
let pub_file = pub_key_path(agent_id)?;
if let Some(parent) = key_file.parent() {
std::fs::create_dir_all(parent).map_err(|e| format!("mkdir keys: {e}"))?;
}
let signing_key = match create_key_file(&key_file, &signing_key.to_bytes()) {
Ok(()) => signing_key,
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => load_settled_key(&key_file)?,
Err(e) => return Err(format!("write key: {e}")),
};
let tmp = pub_file.with_extension(format!("pub.{}.tmp", std::process::id()));
std::fs::write(&tmp, signing_key.verifying_key().to_bytes())
.and_then(|()| std::fs::rename(&tmp, &pub_file))
.map_err(|e| {
let _ = std::fs::remove_file(&tmp);
format!("write pub: {e}")
})?;
Ok(signing_key)
}
fn create_key_file(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
use std::io::Write;
let mut options = std::fs::OpenOptions::new();
options.write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.mode(0o600);
}
let mut file = options.open(path)?;
file.write_all(bytes)?;
file.sync_all()
}
fn load_settled_key(path: &Path) -> Result<SigningKey, String> {
let mut last = String::new();
for _ in 0..50 {
match load_key(path) {
Ok(key) => return Ok(key),
Err(e) => last = e,
}
std::thread::sleep(std::time::Duration::from_millis(5));
}
Err(last)
}
fn load_key(path: &Path) -> Result<SigningKey, String> {
let bytes = read_key_file(path, "key")?;
let arr: [u8; 32] = bytes
.try_into()
.map_err(|_| "invalid key file (expected 32 bytes)".to_string())?;
Ok(SigningKey::from_bytes(&arr))
}
fn read_key_file(path: &Path, label: &str) -> Result<Vec<u8>, String> {
let parent = path
.parent()
.ok_or_else(|| format!("{label} directory is unavailable"))?;
#[cfg(unix)]
{
use std::ffi::CString;
use std::io::Read;
use std::os::fd::{AsRawFd, FromRawFd};
use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::OpenOptionsExt;
let directory = std::fs::OpenOptions::new()
.read(true)
.custom_flags(libc::O_CLOEXEC | libc::O_NOFOLLOW | libc::O_NONBLOCK)
.open(parent)
.map_err(|error| format!("open {label} directory without symlinks: {error}"))?;
if !directory
.metadata()
.map_err(|error| format!("inspect {label} directory: {error}"))?
.is_dir()
{
return Err(format!("{label} directory must be a directory"));
}
let file_name = path
.file_name()
.ok_or_else(|| format!("{label} filename is unavailable"))?;
let file_name = CString::new(file_name.as_bytes())
.map_err(|_| format!("{label} filename contains NUL"))?;
let fd = unsafe {
libc::openat(
directory.as_raw_fd(),
file_name.as_ptr(),
libc::O_RDONLY | libc::O_CLOEXEC | libc::O_NOFOLLOW | libc::O_NONBLOCK,
)
};
if fd < 0 {
return Err(format!(
"open {label} without symlinks: {}",
std::io::Error::last_os_error()
));
}
let file = unsafe { std::fs::File::from_raw_fd(fd) };
if !file
.metadata()
.map_err(|error| format!("inspect {label}: {error}"))?
.is_file()
{
return Err(format!("{label} must be a regular file"));
}
let mut bytes = Vec::new();
file.take(33)
.read_to_end(&mut bytes)
.map_err(|error| format!("read {label}: {error}"))?;
Ok(bytes)
}
#[cfg(windows)]
{
use std::io::Read;
use std::os::windows::ffi::OsStrExt;
use std::os::windows::fs::MetadataExt;
use std::os::windows::io::FromRawHandle;
use windows_sys::Wdk::Storage::FileSystem::{
FILE_NON_DIRECTORY_FILE, FILE_OPEN, FILE_OPEN_REPARSE_POINT,
FILE_SYNCHRONOUS_IO_NONALERT,
};
use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;
use windows_sys::Win32::Storage::FileSystem::{
CreateFileW, FILE_ATTRIBUTE_REPARSE_POINT, FILE_FLAG_BACKUP_SEMANTICS,
FILE_FLAG_OPEN_REPARSE_POINT, FILE_LIST_DIRECTORY, FILE_READ_ATTRIBUTES,
FILE_READ_DATA, FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING,
SYNCHRONIZE,
};
const TRAVERSE_DIRECTORY_ACCESS: u32 =
FILE_LIST_DIRECTORY | FILE_READ_ATTRIBUTES | SYNCHRONIZE;
const READ_KEY_ACCESS: u32 = FILE_READ_DATA | FILE_READ_ATTRIBUTES | SYNCHRONIZE;
let wide_parent = parent
.as_os_str()
.encode_wide()
.chain(std::iter::once(0))
.collect::<Vec<_>>();
let directory_handle = unsafe {
CreateFileW(
wide_parent.as_ptr(),
TRAVERSE_DIRECTORY_ACCESS,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
std::ptr::null(),
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
std::ptr::null_mut(),
)
};
if directory_handle == INVALID_HANDLE_VALUE || directory_handle.is_null() {
let error = std::io::Error::last_os_error();
return Err(format!(
"open {label} directory without reparse points: {error}"
));
}
let directory = unsafe { std::fs::File::from_raw_handle(directory_handle) };
let directory_metadata = directory
.metadata()
.map_err(|error| format!("inspect {label} directory: {error}"))?;
if directory_metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
return Err(format!("{label} directory reparse point rejected"));
}
if !directory_metadata.is_dir() {
return Err(format!("{label} directory must be a directory"));
}
let file_name = path
.file_name()
.ok_or_else(|| format!("{label} filename is unavailable"))?
.encode_wide()
.collect::<Vec<_>>();
let file = leanctx_native_storage::windows_file::open_relative(
&directory,
&file_name,
READ_KEY_ACCESS,
FILE_OPEN,
FILE_NON_DIRECTORY_FILE | FILE_OPEN_REPARSE_POINT | FILE_SYNCHRONOUS_IO_NONALERT,
)
.map_err(|status| {
use leanctx_native_storage::windows_file::OpenError;
let reason = match status {
OpenError::Missing => "Missing (NotFound)",
OpenError::Reparse => "Reparse",
OpenError::Unsupported => "Unsupported",
OpenError::Failure | OpenError::Collision => "Failure",
};
format!("open {label} without reparse traversal: {reason}")
})?;
let metadata = file
.metadata()
.map_err(|error| format!("inspect {label}: {error}"))?;
if metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
return Err(format!("{label} reparse point rejected"));
}
if !metadata.is_file() {
return Err(format!("{label} must be a regular file"));
}
let mut bytes = Vec::new();
file.take(33)
.read_to_end(&mut bytes)
.map_err(|error| format!("read {label}: {error}"))?;
Ok(bytes)
}
#[cfg(not(any(unix, windows)))]
{
let _ = (parent, path);
Err(format!(
"secure {label} reads are unavailable on this platform; refusing a race-prone key lookup"
))
}
}
#[cfg(all(test, any(unix, windows)))]
mod tests {
use super::*;
fn env_of(pairs: &[(&str, &str)]) -> impl Fn(&str) -> Option<String> {
let owned: Vec<(String, String)> = pairs
.iter()
.map(|(k, v)| ((*k).to_string(), (*v).to_string()))
.collect();
move |key| owned.iter().find(|(k, _)| k == key).map(|(_, v)| v.clone())
}
#[test]
fn session_attribution_combines_client_and_bus_id() {
assert_eq!(
session_attribution_value("claude-code", Some("mcp-1274-fbba0c02")).as_deref(),
Some("claude-code:mcp-1274-fbba0c02")
);
assert_eq!(
session_attribution_value("Codex MCP Client", None).as_deref(),
Some("codex-mcp-client")
);
assert_eq!(
session_attribution_value("", Some("mcp-9-x")).as_deref(),
Some("mcp-9-x")
);
assert_eq!(session_attribution_value(" ", None), None);
}
#[test]
fn session_attribution_is_bounded_and_ledger_safe() {
let long = "x".repeat(500);
let value = session_attribution_value(&format!("{long}\n:/"), Some(&long)).unwrap();
assert!(value.len() <= 48 + 1 + 72, "{}", value.len());
assert!(
value
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-' | ':'))
);
}
#[test]
fn host_from_env_detects_agent_hosts() {
assert_eq!(
host_from_env(env_of(&[("CLAUDECODE", "1")])),
Some("claude")
);
assert_eq!(
host_from_env(env_of(&[("CODEX_THREAD_ID", "t-1")])),
Some("codex")
);
assert_eq!(
host_from_env(env_of(&[("CURSOR_TASK_ID", "task")])),
Some("cursor")
);
assert_eq!(host_from_env(env_of(&[("CLAUDECODE", " ")])), None);
assert_eq!(host_from_env(env_of(&[])), None);
}
#[test]
fn delivery_id_two_claude_processes_are_distinct() {
let env = env_of(&[("CLAUDECODE", "1")]);
let a = resolve_delivery_agent_id(&env, 100);
let b = resolve_delivery_agent_id(&env, 200);
assert_eq!(a, "claude-100");
assert_ne!(a, b, "two Claude Code clients must not share an identity");
assert_ne!(a, "1", "the constant CLAUDECODE value is not an identity");
}
#[test]
fn delivery_id_priority_matrix() {
let explicit = env_of(&[
("LEAN_CTX_AGENT_ID", "ops-agent"),
("CURSOR_TASK_ID", "t1"),
("CLAUDECODE", "1"),
]);
assert_eq!(resolve_delivery_agent_id(&explicit, 7), "ops-agent");
let legacy = env_of(&[("LCTX_AGENT_ID", "legacy"), ("CLAUDECODE", "1")]);
assert_eq!(resolve_delivery_agent_id(&legacy, 7), "legacy");
let cursor = env_of(&[("CURSOR_TASK_ID", "t1"), ("CLAUDECODE", "1")]);
assert_eq!(resolve_delivery_agent_id(&cursor, 7), "t1");
let codex = env_of(&[("CODEX_THREAD_ID", "th")]);
assert_eq!(resolve_delivery_agent_id(&codex, 7), "codex-7");
assert_eq!(resolve_delivery_agent_id(env_of(&[]), 7), "local-7");
}
#[test]
fn delivery_id_ignores_blank_values() {
let env = env_of(&[("LEAN_CTX_AGENT_ID", " "), ("CURSOR_TASK_ID", "")]);
assert_eq!(resolve_delivery_agent_id(env, 9), "local-9");
}
#[test]
fn delivery_id_is_stable_within_process() {
assert_eq!(delivery_agent_id(), delivery_agent_id());
}
#[test]
fn sign_and_verify_roundtrip() {
let mut seed = [0u8; 32];
getrandom::fill(&mut seed).unwrap();
let key = SigningKey::from_bytes(&seed);
let data = b"test payload";
let sig = key.sign(data);
let pub_bytes = key.verifying_key().to_bytes();
assert!(verify_signature(&pub_bytes, data, &sig.to_bytes()));
}
#[test]
fn verify_rejects_tampered_data() {
let mut seed = [0u8; 32];
getrandom::fill(&mut seed).unwrap();
let key = SigningKey::from_bytes(&seed);
let sig = key.sign(b"original");
let pub_bytes = key.verifying_key().to_bytes();
assert!(!verify_signature(&pub_bytes, b"tampered", &sig.to_bytes()));
}
#[test]
fn hex_roundtrip() {
let data = vec![0xde, 0xad, 0xbe, 0xef];
let encoded = hex_encode(&data);
assert_eq!(encoded, "deadbeef");
let decoded = hex_decode(&encoded).unwrap();
assert_eq!(decoded, data);
}
#[test]
fn hex_decode_accepts_all_bytes_and_rejects_malformed_text() {
let bytes: Vec<u8> = (0..=255).collect();
let encoded = hex_encode(&bytes);
assert_eq!(hex_decode(&encoded).unwrap(), bytes);
assert_eq!(hex_decode(&encoded.to_uppercase()).unwrap(), bytes);
assert_eq!(hex_decode("").unwrap(), Vec::<u8>::new());
for malformed in ["0", "gg", " 0", "\u{1f512}", "0\u{20ac}", "\u{e9}\u{e9}"] {
assert!(hex_decode(malformed).is_err(), "{malformed:?}");
}
assert_eq!(hex_decode("0").unwrap_err(), "odd-length hex string");
assert_eq!(hex_decode("0\u{20ac}").unwrap_err(), "non-ASCII hex string");
}
#[test]
fn stored_public_key_is_read_only_and_ids_are_path_safe() {
let _isolated = crate::core::data_dir::isolated_data_dir();
assert!(get_stored_public_key("missing").is_err());
assert!(get_stored_public_key("../outside").is_err());
assert!(get_stored_public_key("/tmp/outside").is_err());
assert!(get_or_create_keypair("agent/child").is_err());
let expected = get_or_create_keypair("root-agent").unwrap().verifying_key();
assert_eq!(get_stored_public_key("root-agent").unwrap(), expected);
}
#[cfg(any(unix, windows))]
#[test]
fn stored_public_key_rejects_symlinks() {
let _isolated = crate::core::data_dir::isolated_data_dir();
let keys_dir = crate::core::data_dir::lean_ctx_data_dir()
.unwrap()
.join("keys");
std::fs::create_dir_all(&keys_dir).unwrap();
let outside = tempfile::tempdir().unwrap();
let outside_key = outside.path().join("outside.pub");
std::fs::write(&outside_key, [7_u8; 32]).unwrap();
create_file_symlink(&outside_key, &keys_dir.join("linked.pub"));
assert!(get_stored_public_key("linked").is_err());
}
#[cfg(any(unix, windows))]
#[test]
fn stored_public_key_rejects_traversal_absolute_and_symlink_paths() {
let _isolated = crate::core::data_dir::isolated_data_dir();
assert!(get_stored_public_key("../outside").is_err());
assert!(get_stored_public_key("/tmp/outside").is_err());
assert!(get_or_create_keypair("../outside").is_err());
let data_dir = crate::core::data_dir::lean_ctx_data_dir().unwrap();
let keys_dir = data_dir.join("keys");
std::fs::create_dir_all(&keys_dir).unwrap();
let outside = tempfile::tempdir().unwrap();
let outside_key = outside.path().join("outside.pub");
std::fs::write(
&outside_key,
SigningKey::from_bytes(&[19_u8; 32])
.verifying_key()
.to_bytes(),
)
.unwrap();
create_file_symlink(&outside_key, &keys_dir.join("linked.pub"));
assert!(get_stored_public_key("linked").is_err());
create_file_symlink(&outside_key, &keys_dir.join("linked.key"));
assert!(get_or_create_keypair("linked").is_err());
std::fs::remove_file(keys_dir.join("linked.pub")).unwrap();
std::fs::remove_file(keys_dir.join("linked.key")).unwrap();
std::fs::create_dir(keys_dir.join("directory.pub")).unwrap();
assert!(get_stored_public_key("directory").is_err());
std::fs::remove_dir(keys_dir.join("directory.pub")).unwrap();
std::fs::create_dir(keys_dir.join("directory.key")).unwrap();
assert!(get_or_create_keypair("directory").is_err());
std::fs::remove_dir(keys_dir.join("directory.key")).unwrap();
std::fs::remove_dir(&keys_dir).unwrap();
create_directory_symlink(outside.path(), &keys_dir);
assert!(get_stored_public_key("outside").is_err());
}
#[test]
fn agent_id_is_one_bounded_canonical_filename_component() {
assert!(validate_agent_id("agent-01.alpha").is_ok());
assert!(validate_agent_id("../outside").is_err());
assert!(validate_agent_id("agent/child").is_err());
assert!(validate_agent_id("agent\\child").is_err());
assert!(validate_agent_id("agent:key").is_err());
assert!(validate_agent_id("agent..child").is_err());
assert!(validate_agent_id(&"a".repeat(129)).is_err());
}
#[test]
fn stored_key_reads_are_bounded_and_reject_directories() {
let root = tempfile::tempdir().unwrap();
let path = root.path().join("key");
std::fs::write(&path, [7_u8; 32]).unwrap();
assert!(load_key(&path).is_ok());
std::fs::write(&path, vec![7_u8; 1024 * 1024]).unwrap();
assert_eq!(read_key_file(&path, "key").unwrap().len(), 33);
assert!(load_key(&path).is_err());
let directory = root.path().join("directory.key");
std::fs::create_dir(&directory).unwrap();
assert!(load_key(&directory).is_err());
}
#[cfg(unix)]
#[test]
fn stored_key_reads_reject_fifos_without_a_writer() {
use std::os::unix::ffi::OsStrExt;
let root = tempfile::tempdir().unwrap();
let fifo = root.path().join("fifo");
let raw = std::ffi::CString::new(fifo.as_os_str().as_bytes()).unwrap();
assert_eq!(unsafe { libc::mkfifo(raw.as_ptr(), 0o600) }, 0);
assert!(load_key(&fifo).is_err());
assert!(load_key(&fifo.join("child")).is_err());
}
#[cfg(unix)]
fn create_file_symlink(target: &Path, link: &Path) {
std::os::unix::fs::symlink(target, link).unwrap();
}
#[cfg(windows)]
fn create_file_symlink(target: &Path, link: &Path) {
std::os::windows::fs::symlink_file(target, link).unwrap();
}
#[cfg(unix)]
fn create_directory_symlink(target: &Path, link: &Path) {
std::os::unix::fs::symlink(target, link).unwrap();
}
#[cfg(windows)]
fn create_directory_symlink(target: &Path, link: &Path) {
std::os::windows::fs::symlink_dir(target, link).unwrap();
}
}