use std::fs;
use std::path::{Path, PathBuf};
use crate::error::PqfileError;
use crate::keygen::fingerprint_pem;
#[must_use]
pub fn revoked_path_for(pubkey_path: &Path) -> PathBuf {
let mut s = pubkey_path.as_os_str().to_owned();
s.push(".revoked");
PathBuf::from(s)
}
#[must_use = "revocation result must be checked"]
pub fn revoke_key(pubkey_path: &Path, reason: &str) -> Result<String, PqfileError> {
let pubkey_pem = fs::read_to_string(pubkey_path)?;
let fingerprint = fingerprint_pem(&pubkey_pem);
let revoked_path = revoked_path_for(pubkey_path);
let content = format!(
"{{\"fingerprint\":\"{fingerprint}\",\"reason\":{}}}",
json_str(reason),
);
fs::write(&revoked_path, content.as_bytes())?;
Ok(fingerprint)
}
#[must_use = "revocation check must be inspected; ignoring it means encrypting to revoked keys"]
pub fn check_not_revoked(pubkey_path: &Path, pubkey_pem: &str) -> Result<(), PqfileError> {
let revoked_path = revoked_path_for(pubkey_path);
if !revoked_path.exists() {
return Ok(());
}
let json = fs::read_to_string(&revoked_path)?;
let fp_in_file = extract_json_str(&json, "fingerprint").unwrap_or_default();
let reason = extract_json_str(&json, "reason").unwrap_or_default();
let fp_of_key = fingerprint_pem(pubkey_pem);
if fp_in_file.is_empty() || fp_of_key == fp_in_file {
return Err(PqfileError::KeyRevoked {
fingerprint: fp_of_key,
reason,
});
}
Ok(())
}
fn json_str(s: &str) -> String {
let escaped: String = s
.chars()
.flat_map(|c| match c {
'"' => vec!['\\', '"'],
'\\' => vec!['\\', '\\'],
'\n' => vec!['\\', 'n'],
'\r' => vec!['\\', 'r'],
'\t' => vec!['\\', 't'],
c => vec![c],
})
.collect();
format!("\"{escaped}\"")
}
fn extract_json_str(json: &str, key: &str) -> Option<String> {
let needle = format!("\"{}\":", key);
let pos = json.find(&needle)?;
let after_colon = json[pos + needle.len()..].trim_start();
if !after_colon.starts_with('"') {
return None;
}
let inner = &after_colon[1..];
let mut result = String::new();
let mut chars = inner.chars();
loop {
match chars.next()? {
'"' => break,
'\\' => match chars.next()? {
'"' => result.push('"'),
'\\' => result.push('\\'),
'n' => result.push('\n'),
'r' => result.push('\r'),
't' => result.push('\t'),
c => {
result.push('\\');
result.push(c);
}
},
c => result.push(c),
}
}
Some(result)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::keygen::keygen_bytes;
use tempfile::tempdir;
#[test]
fn revoke_creates_sidecar_file() {
let tmp = tempdir().unwrap();
let pub_path = tmp.path().join("pubkey.pem");
let (pub_pem, _) = keygen_bytes(768, None).unwrap();
std::fs::write(&pub_path, &pub_pem).unwrap();
revoke_key(&pub_path, "test reason").unwrap();
assert!(revoked_path_for(&pub_path).exists());
}
#[test]
fn check_not_revoked_passes_without_sidecar() {
let tmp = tempdir().unwrap();
let pub_path = tmp.path().join("pubkey.pem");
let (pub_pem, _) = keygen_bytes(768, None).unwrap();
std::fs::write(&pub_path, &pub_pem).unwrap();
check_not_revoked(&pub_path, &pub_pem).unwrap();
}
#[test]
fn check_not_revoked_fails_after_revoke() {
let tmp = tempdir().unwrap();
let pub_path = tmp.path().join("pubkey.pem");
let (pub_pem, _) = keygen_bytes(768, None).unwrap();
std::fs::write(&pub_path, &pub_pem).unwrap();
revoke_key(&pub_path, "compromised").unwrap();
let err = check_not_revoked(&pub_path, &pub_pem).unwrap_err();
assert!(matches!(err, PqfileError::KeyRevoked { .. }));
}
#[test]
fn revoke_returns_fingerprint() {
let tmp = tempdir().unwrap();
let pub_path = tmp.path().join("pubkey.pem");
let (pub_pem, _) = keygen_bytes(768, None).unwrap();
std::fs::write(&pub_path, &pub_pem).unwrap();
let fp = revoke_key(&pub_path, "").unwrap();
let parts: Vec<&str> = fp.split(':').collect();
assert_eq!(parts.len(), 16);
}
#[test]
fn revoke_stores_reason_in_sidecar() {
let tmp = tempdir().unwrap();
let pub_path = tmp.path().join("pubkey.pem");
let (pub_pem, _) = keygen_bytes(768, None).unwrap();
std::fs::write(&pub_path, &pub_pem).unwrap();
revoke_key(&pub_path, "key was leaked").unwrap();
let sidecar = std::fs::read_to_string(revoked_path_for(&pub_path)).unwrap();
assert!(sidecar.contains("key was leaked"));
}
#[test]
fn revoke_error_message_includes_reason() {
let tmp = tempdir().unwrap();
let pub_path = tmp.path().join("pubkey.pem");
let (pub_pem, _) = keygen_bytes(768, None).unwrap();
std::fs::write(&pub_path, &pub_pem).unwrap();
revoke_key(&pub_path, "lost my laptop").unwrap();
let err = check_not_revoked(&pub_path, &pub_pem).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("lost my laptop"));
}
}