use crate::utils::error::{Error, Result};
use base64::{engine::general_purpose, Engine as _};
use chrono::{DateTime, Utc};
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
const CONFIG_VERSION: &str = "1.0";
const TRUSTED_KEYS_FILENAME: &str = "trusted-keys.toml";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum KeyPurpose {
ReceiptVerification,
PackageSigning,
TemplateSigning,
General,
}
impl KeyPurpose {
pub fn from_str_lossy(s: &str) -> Self {
match s {
"receipt-verification" => Self::ReceiptVerification,
"package-signing" => Self::PackageSigning,
"template-signing" => Self::TemplateSigning,
_ => Self::General,
}
}
}
impl std::fmt::Display for KeyPurpose {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ReceiptVerification => write!(f, "receipt-verification"),
Self::PackageSigning => write!(f, "package-signing"),
Self::TemplateSigning => write!(f, "template-signing"),
Self::General => write!(f, "general"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrustedKeyEntry {
pub name: String,
pub public_key: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub fingerprint: Option<String>,
pub purpose: String,
pub added_at: DateTime<Utc>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expires_at: Option<DateTime<Utc>>,
}
impl TrustedKeyEntry {
pub fn new(name: String, public_key: String, purpose: String) -> Self {
let fingerprint = Self::compute_fingerprint(&public_key);
Self {
name,
public_key,
fingerprint: Some(fingerprint),
purpose,
added_at: Utc::now(),
expires_at: None,
}
}
pub fn with_expiry(
name: String, public_key: String, purpose: String, expires_at: DateTime<Utc>,
) -> Self {
let mut entry = Self::new(name, public_key, purpose);
entry.expires_at = Some(expires_at);
entry
}
pub fn compute_fingerprint(public_key_b64: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(public_key_b64.as_bytes());
let hash = hasher.finalize();
format!("sha256:{}", hex::encode(hash))
}
pub fn decode_public_key(&self) -> Result<Vec<u8>> {
general_purpose::STANDARD
.decode(&self.public_key)
.map_err(|e| Error::with_context("Failed to decode public key base64", &e.to_string()))
}
pub fn is_expired(&self) -> bool {
match self.expires_at {
Some(expiry) => Utc::now() > expiry,
None => false,
}
}
pub fn parsed_purpose(&self) -> KeyPurpose {
KeyPurpose::from_str_lossy(&self.purpose)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrustedKeysConfig {
pub version: String,
#[serde(default)]
pub keys: Vec<TrustedKeyEntry>,
}
impl Default for TrustedKeysConfig {
fn default() -> Self {
Self {
version: CONFIG_VERSION.to_string(),
keys: Vec::new(),
}
}
}
impl TrustedKeysConfig {
pub fn new() -> Self {
Self::default()
}
pub fn load_from_path(path: &Path) -> Result<Self> {
if !path.exists() {
return Ok(Self::new());
}
let content = std::fs::read_to_string(path)
.map_err(|e| Error::with_context("Failed to read trusted-keys.toml", &e.to_string()))?;
let config: TrustedKeysConfig = toml::from_str(&content).map_err(|e| {
Error::with_context("Failed to parse trusted-keys.toml", &e.to_string())
})?;
if config.version != CONFIG_VERSION {
return Err(Error::with_context(
"Unsupported trusted-keys.toml version",
&format!("expected {}, got {}", CONFIG_VERSION, config.version),
));
}
Ok(config)
}
pub fn load_from_project() -> Result<Self> {
let path = Self::project_config_path()?;
Self::load_from_path(&path)
}
pub fn load_from_user_dir() -> Result<Self> {
let path = Self::user_config_path()?;
Self::load_from_path(&path)
}
pub fn save_to_path(&self, path: &Path) -> Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(|e| {
Error::with_context("Failed to create config directory", &e.to_string())
})?;
}
let content = toml::to_string_pretty(self).map_err(|e| {
Error::with_context("Failed to serialize trusted-keys.toml", &e.to_string())
})?;
std::fs::write(path, content).map_err(|e| {
Error::with_context("Failed to write trusted-keys.toml", &e.to_string())
})?;
Ok(())
}
pub fn project_config_path() -> Result<PathBuf> {
Ok(PathBuf::from(".ggen").join(TRUSTED_KEYS_FILENAME))
}
pub fn user_config_path() -> Result<PathBuf> {
let base =
dirs::data_dir().ok_or_else(|| Error::new("Cannot determine user data directory"))?;
Ok(base.join("ggen").join(TRUSTED_KEYS_FILENAME))
}
}
pub struct PkiManager {
config: TrustedKeysConfig,
fingerprint_index: HashMap<String, usize>,
name_index: HashMap<String, usize>,
}
impl PkiManager {
pub fn new(config: TrustedKeysConfig) -> Self {
let mut fingerprint_index = HashMap::new();
let mut name_index = HashMap::new();
for (i, key) in config.keys.iter().enumerate() {
if let Some(fp) = &key.fingerprint {
fingerprint_index.insert(fp.clone(), i);
}
name_index.insert(key.name.clone(), i);
}
Self {
config,
fingerprint_index,
name_index,
}
}
pub fn from_project() -> Result<Self> {
let config = TrustedKeysConfig::load_from_project()?;
Ok(Self::new(config))
}
pub fn from_user_dir() -> Result<Self> {
let config = TrustedKeysConfig::load_from_user_dir()?;
Ok(Self::new(config))
}
pub fn add_key(&mut self, entry: TrustedKeyEntry) -> Result<()> {
if self.name_index.contains_key(&entry.name) {
return Err(Error::with_context(
"Trusted key already exists",
&format!("key named '{}' is already in the trust store", entry.name),
));
}
if let Some(fp) = &entry.fingerprint {
if self.fingerprint_index.contains_key(fp) {
return Err(Error::with_context(
"Duplicate public key detected",
&format!("a key with fingerprint '{}' already exists", fp),
));
}
}
let idx = self.config.keys.len();
if let Some(fp) = &entry.fingerprint {
self.fingerprint_index.insert(fp.clone(), idx);
}
self.name_index.insert(entry.name.clone(), idx);
self.config.keys.push(entry);
Ok(())
}
pub fn remove_key(&mut self, name: &str) -> Result<TrustedKeyEntry> {
let idx = self
.name_index
.remove(name)
.ok_or_else(|| Error::with_context("Trusted key not found", name))?;
let entry = self.config.keys.remove(idx);
self.rebuild_indexes();
Ok(entry)
}
pub fn get_key(&self, name: &str) -> Option<&TrustedKeyEntry> {
self.name_index
.get(name)
.and_then(|&idx| self.config.keys.get(idx))
}
pub fn get_key_by_fingerprint(&self, fingerprint: &str) -> Option<&TrustedKeyEntry> {
self.fingerprint_index
.get(fingerprint)
.and_then(|&idx| self.config.keys.get(idx))
}
pub fn all_keys(&self) -> &[TrustedKeyEntry] {
&self.config.keys
}
pub fn active_keys(&self) -> Vec<&TrustedKeyEntry> {
self.config
.keys
.iter()
.filter(|k| !k.is_expired())
.collect()
}
pub fn key_count(&self) -> usize {
self.config.keys.len()
}
pub fn verify_with_key(
&self, message: &[u8], signature_hex: &str, key_name: &str,
) -> Result<bool> {
let entry = self
.get_key(key_name)
.ok_or_else(|| Error::with_context("Trusted key not found", key_name))?;
if entry.is_expired() {
return Err(Error::with_context("Trusted key has expired", key_name));
}
verify_ed25519(message, signature_hex, &entry.public_key)
}
pub fn verify_against_trusted(
&self, message: &[u8], signature_hex: &str,
) -> Result<Option<String>> {
for entry in self.active_keys() {
if verify_ed25519(message, signature_hex, &entry.public_key)? {
return Ok(Some(entry.name.clone()));
}
}
Ok(None)
}
pub fn verify_with_purpose(
&self, message: &[u8], signature_hex: &str, purpose: &KeyPurpose,
) -> Result<Option<String>> {
for entry in self.active_keys() {
if entry.parsed_purpose() == *purpose {
if verify_ed25519(message, signature_hex, &entry.public_key)? {
return Ok(Some(entry.name.clone()));
}
}
}
Ok(None)
}
pub fn save_to_project(&self) -> Result<()> {
let path = TrustedKeysConfig::project_config_path()?;
self.config.save_to_path(&path)
}
pub fn save_to_user_dir(&self) -> Result<()> {
let path = TrustedKeysConfig::user_config_path()?;
self.config.save_to_path(&path)
}
pub fn into_config(self) -> TrustedKeysConfig {
self.config
}
fn rebuild_indexes(&mut self) {
self.fingerprint_index.clear();
self.name_index.clear();
for (i, key) in self.config.keys.iter().enumerate() {
if let Some(fp) = &key.fingerprint {
self.fingerprint_index.insert(fp.clone(), i);
}
self.name_index.insert(key.name.clone(), i);
}
}
}
pub fn verify_ed25519(message: &[u8], signature_hex: &str, public_key_b64: &str) -> Result<bool> {
let pk_bytes = general_purpose::STANDARD
.decode(public_key_b64)
.map_err(|e| Error::with_context("Failed to decode public key", &e.to_string()))?;
let sig_bytes = hex::decode(signature_hex)
.map_err(|e| Error::with_context("Failed to decode signature hex", &e.to_string()))?;
let pk_array: [u8; 32] = pk_bytes
.try_into()
.map_err(|_| Error::new("Ed25519 public key must be 32 bytes"))?;
let verifying_key = VerifyingKey::from_bytes(&pk_array)
.map_err(|e| Error::with_context("Invalid Ed25519 public key", &e.to_string()))?;
let signature = Signature::from_slice(&sig_bytes)
.map_err(|_| Error::new("Invalid Ed25519 signature format"))?;
match verifying_key.verify(message, &signature) {
Ok(()) => Ok(true),
Err(_) => Ok(false),
}
}
#[cfg(test)]
mod tests {
use super::*;
use ed25519_dalek::{Signer, SigningKey};
fn generate_test_key(name: &str, purpose: &str) -> (TrustedKeyEntry, SigningKey) {
let mut seed = [0u8; 32];
let name_bytes = name.as_bytes();
for (i, &b) in name_bytes.iter().enumerate().take(32) {
seed[i] = b;
}
if seed[0] == 0 {
seed[0] = 1;
}
let signing_key = SigningKey::from_bytes(&seed);
let verifying_key = signing_key.verifying_key();
let pk_bytes = verifying_key.to_bytes();
let pk_b64 = general_purpose::STANDARD.encode(pk_bytes);
let entry = TrustedKeyEntry::new(name.to_string(), pk_b64, purpose.to_string());
(entry, signing_key)
}
fn sign_message(signing_key: &SigningKey, message: &[u8]) -> String {
let signature = signing_key.sign(message);
hex::encode(signature.to_bytes())
}
#[test]
fn test_trusted_key_entry_creation() {
let entry = TrustedKeyEntry::new(
"test-key".to_string(),
"dGVzdA==".to_string(),
"receipt-verification".to_string(),
);
assert_eq!(entry.name, "test-key");
assert_eq!(entry.public_key, "dGVzdA==");
assert!(entry.fingerprint.is_some());
assert!(entry.fingerprint.as_ref().unwrap().starts_with("sha256:"));
assert!(!entry.is_expired());
assert_eq!(entry.parsed_purpose(), KeyPurpose::ReceiptVerification);
}
#[test]
fn test_trusted_key_entry_expiry() {
let past = Utc::now() - chrono::Duration::days(1);
let future = Utc::now() + chrono::Duration::days(30);
let expired = TrustedKeyEntry::with_expiry(
"expired-key".to_string(),
"dGVzdA==".to_string(),
"general".to_string(),
past,
);
assert!(expired.is_expired());
let valid = TrustedKeyEntry::with_expiry(
"valid-key".to_string(),
"dGVzdA==".to_string(),
"general".to_string(),
future,
);
assert!(!valid.is_expired());
}
#[test]
fn test_fingerprint_deterministic() {
let fp1 = TrustedKeyEntry::compute_fingerprint("abc123");
let fp2 = TrustedKeyEntry::compute_fingerprint("abc123");
assert_eq!(fp1, fp2);
let fp3 = TrustedKeyEntry::compute_fingerprint("xyz789");
assert_ne!(fp1, fp3);
}
#[test]
fn test_trusted_keys_config_default() {
let config = TrustedKeysConfig::new();
assert_eq!(config.version, "1.0");
assert!(config.keys.is_empty());
}
#[test]
fn test_trusted_keys_config_save_and_load() {
let dir = tempfile::tempdir().expect("Failed to create tempdir");
let path = dir.path().join("trusted-keys.toml");
let (entry, _) = generate_test_key("test-key", "receipt-verification");
let mut config = TrustedKeysConfig::new();
config.keys.push(entry);
config.save_to_path(&path).expect("Save failed");
assert!(path.exists());
let loaded = TrustedKeysConfig::load_from_path(&path).expect("Load failed");
assert_eq!(loaded.version, "1.0");
assert_eq!(loaded.keys.len(), 1);
assert_eq!(loaded.keys[0].name, "test-key");
}
#[test]
fn test_pki_manager_add_and_lookup() {
let (entry, _) = generate_test_key("my-key", "package-signing");
let config = TrustedKeysConfig::new();
let mut manager = PkiManager::new(config);
manager.add_key(entry).expect("Add failed");
assert_eq!(manager.key_count(), 1);
let found = manager.get_key("my-key");
assert!(found.is_some());
assert_eq!(found.unwrap().purpose, "package-signing");
let not_found = manager.get_key("nonexistent");
assert!(not_found.is_none());
}
#[test]
fn test_pki_manager_duplicate_name_rejected() {
let (entry1, _) = generate_test_key("same-name", "general");
let (entry2, _) = generate_test_key("same-name", "general");
let config = TrustedKeysConfig::new();
let mut manager = PkiManager::new(config);
manager.add_key(entry1).expect("First add should succeed");
let result = manager.add_key(entry2);
assert!(result.is_err());
}
#[test]
fn test_pki_manager_duplicate_public_key_rejected() {
let (entry1, _) = generate_test_key("key-a", "general");
let entry2 = TrustedKeyEntry::new(
"key-b".to_string(),
entry1.public_key.clone(),
"general".to_string(),
);
let config = TrustedKeysConfig::new();
let mut manager = PkiManager::new(config);
manager.add_key(entry1).expect("First add should succeed");
let result = manager.add_key(entry2);
assert!(result.is_err());
}
#[test]
fn test_pki_manager_remove_key() {
let (entry, _) = generate_test_key("remove-me", "general");
let config = TrustedKeysConfig::new();
let mut manager = PkiManager::new(config);
manager.add_key(entry).expect("Add failed");
assert_eq!(manager.key_count(), 1);
let removed = manager.remove_key("remove-me").expect("Remove failed");
assert_eq!(removed.name, "remove-me");
assert_eq!(manager.key_count(), 0);
}
#[test]
fn test_pki_manager_verify_signature() {
let (entry, signing_key) = generate_test_key("signing-key", "receipt-verification");
let config = TrustedKeysConfig::new();
let mut manager = PkiManager::new(config);
manager.add_key(entry).expect("Add failed");
let message = b"important data to sign";
let signature_hex = sign_message(&signing_key, message);
let result = manager.verify_with_key(message, &signature_hex, "signing-key");
assert!(result.is_ok());
assert!(result.unwrap());
let trusted_result = manager.verify_against_trusted(message, &signature_hex);
assert!(trusted_result.is_ok());
assert_eq!(trusted_result.unwrap(), Some("signing-key".to_string()));
}
#[test]
fn test_pki_manager_verify_wrong_signature() {
let (entry, signing_key) = generate_test_key("signing-key", "receipt-verification");
let config = TrustedKeysConfig::new();
let mut manager = PkiManager::new(config);
manager.add_key(entry).expect("Add failed");
let message = b"important data to sign";
let signature_hex = sign_message(&signing_key, message);
let result = manager.verify_with_key(b"wrong message", &signature_hex, "signing-key");
assert!(result.is_ok());
assert!(!result.unwrap());
}
#[test]
fn test_pki_manager_verify_with_purpose_filter() {
let (entry1, sk1) = generate_test_key("receipt-key", "receipt-verification");
let (entry2, _sk2) = generate_test_key("package-key", "package-signing");
let config = TrustedKeysConfig::new();
let mut manager = PkiManager::new(config);
manager.add_key(entry1).expect("Add failed");
manager.add_key(entry2).expect("Add failed");
let message = b"test data";
let sig_receipt = sign_message(&sk1, message);
let result =
manager.verify_with_purpose(message, &sig_receipt, &KeyPurpose::ReceiptVerification);
assert!(result.is_ok());
assert_eq!(result.unwrap(), Some("receipt-key".to_string()));
let result_pkg =
manager.verify_with_purpose(message, &sig_receipt, &KeyPurpose::PackageSigning);
assert!(result_pkg.is_ok());
assert!(result_pkg.unwrap().is_none());
}
#[test]
fn test_pki_manager_active_keys_excludes_expired() {
let past = Utc::now() - chrono::Duration::days(1);
let (entry, _) = generate_test_key("expired-key", "general");
let expired_entry =
TrustedKeyEntry::with_expiry(entry.name.clone(), entry.public_key, entry.purpose, past);
let config = TrustedKeysConfig::new();
let mut manager = PkiManager::new(config);
manager.add_key(expired_entry).expect("Add failed");
assert_eq!(manager.key_count(), 1);
assert_eq!(manager.active_keys().len(), 0);
}
#[test]
fn test_pki_manager_save_and_reload() {
let dir = tempfile::tempdir().expect("Failed to create tempdir");
let path = dir.path().join("trusted-keys.toml");
let (entry, _) = generate_test_key("persistent-key", "template-signing");
let config = TrustedKeysConfig::new();
let mut manager = PkiManager::new(config);
manager.add_key(entry).expect("Add failed");
manager.config.save_to_path(&path).expect("Save failed");
let loaded_config = TrustedKeysConfig::load_from_path(&path).expect("Load failed");
let manager2 = PkiManager::new(loaded_config);
assert_eq!(manager2.key_count(), 1);
assert!(manager2.get_key("persistent-key").is_some());
}
#[test]
fn test_verify_ed25519_standalone() {
let seed = [99u8; 32];
let signing_key = SigningKey::from_bytes(&seed);
let verifying_key = signing_key.verifying_key();
let pk_b64 = general_purpose::STANDARD.encode(verifying_key.to_bytes());
let message = b"standalone test";
let signature = signing_key.sign(message);
let sig_hex = hex::encode(signature.to_bytes());
let result = verify_ed25519(message, &sig_hex, &pk_b64);
assert!(result.is_ok());
assert!(result.unwrap());
}
#[test]
fn test_verify_ed25519_invalid_inputs() {
let result = verify_ed25519(b"msg", "deadbeef", "not-valid-base64!!!");
assert!(result.is_err());
let (_, signing_key) = generate_test_key("k", "g");
let verifying_key = signing_key.verifying_key();
let pk_b64 = general_purpose::STANDARD.encode(verifying_key.to_bytes());
let result = verify_ed25519(b"msg", "zzzz", &pk_b64);
assert!(result.is_err());
}
#[test]
fn test_key_purpose_display() {
assert_eq!(
KeyPurpose::ReceiptVerification.to_string(),
"receipt-verification"
);
assert_eq!(KeyPurpose::PackageSigning.to_string(), "package-signing");
assert_eq!(KeyPurpose::TemplateSigning.to_string(), "template-signing");
assert_eq!(KeyPurpose::General.to_string(), "general");
}
#[test]
fn test_key_purpose_from_str() {
assert_eq!(
KeyPurpose::from_str_lossy("receipt-verification"),
KeyPurpose::ReceiptVerification
);
assert_eq!(
KeyPurpose::from_str_lossy("package-signing"),
KeyPurpose::PackageSigning
);
assert_eq!(
KeyPurpose::from_str_lossy("template-signing"),
KeyPurpose::TemplateSigning
);
assert_eq!(KeyPurpose::from_str_lossy("unknown"), KeyPurpose::General);
assert_eq!(KeyPurpose::from_str_lossy(""), KeyPurpose::General);
}
}