use super::b64;
use ring::signature::{Ed25519KeyPair, KeyPair};
use std::path::Path;
pub struct AgentKey {
pair: Ed25519KeyPair,
seed: [u8; 32],
}
impl AgentKey {
pub fn generate() -> Result<AgentKey, String> {
let rng = ring::rand::SystemRandom::new();
let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng)
.map_err(|_| "aauth: key generation failed".to_string())?;
let doc = pkcs8.as_ref();
let seed: [u8; 32] = doc
.get(16..48)
.and_then(|s| s.try_into().ok())
.ok_or_else(|| "aauth: unexpected pkcs8 layout".to_string())?;
AgentKey::from_seed(&seed)
}
pub fn from_seed(seed: &[u8]) -> Result<AgentKey, String> {
let seed: [u8; 32] = seed
.try_into()
.map_err(|_| "aauth: seed must be 32 bytes".to_string())?;
let pair = Ed25519KeyPair::from_seed_unchecked(&seed)
.map_err(|_| "aauth: invalid Ed25519 seed".to_string())?;
Ok(AgentKey { pair, seed })
}
pub fn load_or_create(path: &Path) -> Result<AgentKey, String> {
if path.exists() {
let text = std::fs::read_to_string(path)
.map_err(|e| format!("aauth: read key {}: {e}", path.display()))?;
let seed = b64::url_decode(text.trim())
.map_err(|e| format!("aauth: key file {}: {e}", path.display()))?;
return AgentKey::from_seed(&seed);
}
let key = AgentKey::generate()?;
key.persist(path)?;
Ok(key)
}
pub fn persist(&self, path: &Path) -> Result<(), String> {
if path.exists() {
return Err(format!("aauth: key file {} already exists", path.display()));
}
std::fs::write(path, b64::url_nopad(&self.seed))
.map_err(|e| format!("aauth: write key {}: {e}", path.display()))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600));
}
Ok(())
}
pub fn public_bytes(&self) -> &[u8] {
self.pair.public_key().as_ref()
}
pub fn public_jwk(&self) -> serde_json::Value {
serde_json::json!({
"kty": "OKP",
"crv": "Ed25519",
"x": b64::url_nopad(self.public_bytes()),
})
}
pub fn thumbprint(&self) -> String {
let canon = format!(
r#"{{"crv":"Ed25519","kty":"OKP","x":"{}"}}"#,
b64::url_nopad(self.public_bytes())
);
let digest = ring::digest::digest(&ring::digest::SHA256, canon.as_bytes());
b64::url_nopad(digest.as_ref())
}
pub fn sign(&self, msg: &[u8]) -> Vec<u8> {
self.pair.sign(msg).as_ref().to_vec()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn generate_persist_load_round_trips_and_signs() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("agent.key");
let key = AgentKey::load_or_create(&path).expect("create");
assert!(path.exists());
let pub1 = key.public_bytes().to_vec();
let tp1 = key.thumbprint();
let key2 = AgentKey::load_or_create(&path).expect("reload");
assert_eq!(key2.public_bytes(), &pub1[..]);
assert_eq!(key2.thumbprint(), tp1);
let msg = b"the signature base";
let sig = key.sign(msg);
assert_eq!(sig.len(), 64);
let vk = ring::signature::UnparsedPublicKey::new(&ring::signature::ED25519, &pub1);
vk.verify(msg, &sig).expect("valid signature");
assert!(vk.verify(b"other", &sig).is_err());
}
#[test]
fn jwk_and_thumbprint_shapes() {
let seed = [7u8; 32];
let key = AgentKey::from_seed(&seed).unwrap();
let jwk = key.public_jwk();
assert_eq!(jwk["kty"], "OKP");
assert_eq!(jwk["crv"], "Ed25519");
assert!(jwk["x"].as_str().unwrap().len() >= 43); assert!(!key.thumbprint().is_empty());
}
}