#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))]
use std::path::Path;
use exo_core::crypto::KeyPair;
use exo_pdp::{PdpSnapshot, PolicyDecisionPoint};
use zeroize::{Zeroize, Zeroizing};
use crate::private_file::{
PrivateFileReadError, read_private_file, write_private_create_new, write_private_replace,
};
const KEY_FILE: &str = "pdp.key";
const STATE_FILE: &str = "pdp-state.cbor";
const MAX_PDP_SNAPSHOT_BYTES: u64 = 67_108_864;
fn read_key(path: &Path) -> anyhow::Result<KeyPair> {
let mut secret_bytes = read_private_file(path, 32, "PDP key custody rejected")?;
if secret_bytes.len() != 32 {
anyhow::bail!(
"corrupt PDP key at {} — expected 32 bytes, got {}",
path.display(),
secret_bytes.len()
);
}
let mut buf = Zeroizing::new([0u8; 32]);
buf.copy_from_slice(&secret_bytes);
secret_bytes.zeroize();
Ok(KeyPair::from_secret_bytes(std::mem::take(&mut *buf))?)
}
fn load_or_create_key(path: &Path) -> anyhow::Result<KeyPair> {
match read_key(path) {
Ok(key) => Ok(key),
Err(read_error)
if matches!(
read_error.downcast_ref(),
Some(PrivateFileReadError::NotFound { .. })
) =>
{
let keypair = KeyPair::generate();
match write_private_create_new(path, keypair.secret_key().as_bytes()) {
Ok(()) => Ok(keypair),
Err(error)
if error
.downcast_ref::<std::io::Error>()
.is_some_and(|io_error| {
io_error.kind() == std::io::ErrorKind::AlreadyExists
}) =>
{
read_key(path)
}
Err(error) => Err(error),
}
}
Err(error) => Err(error),
}
}
pub fn load_or_create(data_dir: &Path) -> anyhow::Result<PolicyDecisionPoint> {
std::fs::create_dir_all(data_dir)?;
let key_path = data_dir.join(KEY_FILE);
let state_path = data_dir.join(STATE_FILE);
let keypair = load_or_create_key(&key_path)?;
let mut pdp = PolicyDecisionPoint::new(keypair);
match read_snapshot(&state_path) {
Ok(bytes) => {
let snapshot = PdpSnapshot::from_cbor(&bytes)?;
pdp.import_snapshot(snapshot)?;
tracing::info!(path = %state_path.display(), "loaded signed PDP runtime state");
}
Err(PrivateFileReadError::NotFound { .. }) => {}
Err(error) => return Err(error.into()),
}
Ok(pdp)
}
fn read_snapshot(path: &Path) -> Result<Zeroizing<Vec<u8>>, PrivateFileReadError> {
read_private_file(
path,
MAX_PDP_SNAPSHOT_BYTES,
"PDP snapshot custody rejected",
)
}
pub fn save(data_dir: &Path, pdp: &PolicyDecisionPoint) -> anyhow::Result<()> {
let bytes = Zeroizing::new(pdp.export_snapshot()?.to_cbor()?);
let dest = data_dir.join(STATE_FILE);
write_private_replace(&dest, &bytes)?;
Ok(())
}
#[cfg(test)]
mod tests {
use std::fs::OpenOptions;
use exo_core::{Did, Hash256, Timestamp, crypto::KeyPair};
use super::*;
fn create_sized_private_file(path: &Path, len: u64) {
write_private_create_new(path, b"x").expect("create private fixture");
OpenOptions::new()
.write(true)
.open(path)
.expect("open private fixture")
.set_len(len)
.expect("size private fixture");
}
#[test]
fn absent_pdp_snapshot_first_start_remains_accepted() {
let directory = tempfile::tempdir().expect("fixture");
let pdp = load_or_create(directory.path()).expect("first start");
assert_ne!(pdp.service_public_key().as_bytes(), &[0_u8; 32]);
assert!(!directory.path().join(STATE_FILE).exists());
}
#[test]
fn pdp_snapshot_bounds_are_enforced_before_cbor_parse() {
let directory = tempfile::tempdir().expect("fixture");
load_or_create(directory.path()).expect("create PDP key");
let path = directory.path().join(STATE_FILE);
create_sized_private_file(&path, 67_108_864);
assert_eq!(
read_snapshot(&path).expect("exact snapshot limit").len(),
67_108_864
);
OpenOptions::new()
.write(true)
.open(&path)
.expect("open private fixture")
.set_len(67_108_865)
.expect("size oversized fixture");
let error = match load_or_create(directory.path()) {
Ok(_) => panic!("oversized snapshot must fail before CBOR parse"),
Err(error) => error,
};
assert!(error.to_string().contains("PDP snapshot custody rejected"));
}
#[test]
fn restart_preserves_key_revocation_and_consumed_state() {
let dir = tempfile::tempdir().unwrap();
let mut pdp = load_or_create(dir.path()).unwrap();
let principal = Did::new("did:exo:principal").unwrap();
let principal_key = KeyPair::generate();
let mandate_hash = Hash256::digest(b"durable-mandate");
pdp.register_key(principal.clone(), *principal_key.public_key());
pdp.reserve(mandate_hash, Timestamp::new(1, 0)).unwrap();
pdp.commit(&mandate_hash).unwrap();
pdp.revoke_mandate(mandate_hash, Timestamp::new(2, 0), "revoked".into());
let service_public_key = pdp.service_public_key();
save(dir.path(), &pdp).unwrap();
let restored = load_or_create(dir.path()).unwrap();
assert_eq!(restored.service_public_key(), service_public_key);
assert_eq!(
restored.resolve_public(&principal),
Some(*principal_key.public_key())
);
assert!(restored.is_consumed(&mandate_hash));
assert!(restored.is_mandate_revoked(&mandate_hash));
}
#[cfg(unix)]
#[test]
fn new_key_and_state_files_are_owner_only() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let pdp = load_or_create(dir.path()).unwrap();
save(dir.path(), &pdp).unwrap();
let key_mode = std::fs::metadata(dir.path().join(KEY_FILE))
.unwrap()
.permissions()
.mode()
& 0o777;
let state_mode = std::fs::metadata(dir.path().join(STATE_FILE))
.unwrap()
.permissions()
.mode()
& 0o777;
assert_eq!(key_mode, 0o600);
assert_eq!(state_mode, 0o600);
}
#[test]
fn existing_key_is_never_overwritten() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join(KEY_FILE);
let original = [7u8; 32];
let expected_public_key = *KeyPair::from_secret_bytes(original).unwrap().public_key();
write_private_create_new(&path, &original).unwrap();
let loaded = load_or_create(dir.path()).unwrap();
assert_eq!(loaded.service_public_key(), expected_public_key);
assert_eq!(std::fs::read(path).unwrap(), original);
}
#[test]
fn stale_pdp_temp_owned_regular_file_recovers_without_weakening_create_new() {
let directory = tempfile::tempdir().unwrap();
let pdp = load_or_create(directory.path()).unwrap();
let stale = directory.path().join("pdp-state.cbor.tmp");
crate::private_file::write_private_create_new(&stale, b"stale owned temp").unwrap();
save(directory.path(), &pdp).expect("exact owned stale temp must recover");
assert!(!stale.exists());
assert!(directory.path().join(STATE_FILE).exists());
}
#[cfg(unix)]
#[test]
fn private_file_pdp_rejects_permissive_snapshot_and_symlink_key_before_decode() {
use std::os::unix::fs::{PermissionsExt, symlink};
let snapshot_dir = tempfile::tempdir().unwrap();
let pdp = load_or_create(snapshot_dir.path()).unwrap();
save(snapshot_dir.path(), &pdp).unwrap();
let snapshot = snapshot_dir.path().join(STATE_FILE);
std::fs::set_permissions(&snapshot, std::fs::Permissions::from_mode(0o644)).unwrap();
let error = match load_or_create(snapshot_dir.path()) {
Ok(_) => panic!("permissive PDP snapshot must fail before CBOR decode"),
Err(error) => error,
};
assert!(error.to_string().contains(STATE_FILE));
let source = tempfile::tempdir().unwrap();
let source_pdp = load_or_create(source.path()).unwrap();
let linked = tempfile::tempdir().unwrap();
symlink(source.path().join(KEY_FILE), linked.path().join(KEY_FILE)).unwrap();
let error = match load_or_create(linked.path()) {
Ok(_) => panic!("symlink PDP key must fail before key decode"),
Err(error) => error,
};
assert!(error.to_string().contains(KEY_FILE));
drop(source_pdp);
}
}