use super::signing::ModuleSignatureData;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum TrustLevel {
Full,
Scoped(Vec<String>),
Pinned([u8; 32]),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrustedAuthor {
pub name: String,
pub public_key: [u8; 32],
pub trust_level: TrustLevel,
}
#[derive(Clone)]
pub struct Keychain {
trusted: HashMap<[u8; 32], TrustedAuthor>,
require_signatures: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VerifyResult {
Trusted,
Unsigned,
Rejected(String),
}
impl Keychain {
pub fn new(require_signatures: bool) -> Self {
Self {
trusted: HashMap::new(),
require_signatures,
}
}
pub fn add_trusted(&mut self, author: TrustedAuthor) {
self.trusted.insert(author.public_key, author);
}
pub fn remove_trusted(&mut self, public_key: &[u8; 32]) -> Option<TrustedAuthor> {
self.trusted.remove(public_key)
}
pub fn is_trusted(
&self,
public_key: &[u8; 32],
module_name: &str,
manifest_hash: &[u8; 32],
) -> bool {
let Some(author) = self.trusted.get(public_key) else {
return false;
};
match &author.trust_level {
TrustLevel::Full => true,
TrustLevel::Scoped(prefixes) => prefixes
.iter()
.any(|prefix| module_name.starts_with(prefix)),
TrustLevel::Pinned(pinned_hash) => pinned_hash == manifest_hash,
}
}
pub fn verify_module(
&self,
module_name: &str,
manifest_hash: &[u8; 32],
signature: Option<&ModuleSignatureData>,
) -> VerifyResult {
let Some(sig) = signature else {
return if self.require_signatures {
VerifyResult::Rejected("module is unsigned and signatures are required".into())
} else {
VerifyResult::Unsigned
};
};
if !sig.verify(manifest_hash) {
return VerifyResult::Rejected("invalid signature".into());
}
if !self.is_trusted(&sig.author_key, module_name, manifest_hash) {
return VerifyResult::Rejected(format!(
"author key {} is not trusted for module '{}'",
hex::encode(sig.author_key),
module_name,
));
}
VerifyResult::Trusted
}
pub fn requires_signatures(&self) -> bool {
self.require_signatures
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::crypto::signing::generate_keypair;
fn make_author(name: &str, key: [u8; 32], trust: TrustLevel) -> TrustedAuthor {
TrustedAuthor {
name: name.to_string(),
public_key: key,
trust_level: trust,
}
}
#[test]
fn test_unsigned_allowed_when_not_required() {
let kc = Keychain::new(false);
let result = kc.verify_module("my_mod", &[0u8; 32], None);
assert_eq!(result, VerifyResult::Unsigned);
}
#[test]
fn test_unsigned_rejected_when_required() {
let kc = Keychain::new(true);
let result = kc.verify_module("my_mod", &[0u8; 32], None);
assert!(matches!(result, VerifyResult::Rejected(_)));
}
#[test]
fn test_full_trust_verifies() {
let (signing_key, verifying_key) = generate_keypair();
let mut kc = Keychain::new(true);
kc.add_trusted(make_author(
"alice",
verifying_key.to_bytes(),
TrustLevel::Full,
));
let hash = [1u8; 32];
let sig = ModuleSignatureData::sign(&hash, &signing_key);
assert_eq!(
kc.verify_module("anything", &hash, Some(&sig)),
VerifyResult::Trusted
);
}
#[test]
fn test_scoped_trust_allows_matching_prefix() {
let (signing_key, verifying_key) = generate_keypair();
let mut kc = Keychain::new(true);
kc.add_trusted(make_author(
"bob",
verifying_key.to_bytes(),
TrustLevel::Scoped(vec!["std::".to_string()]),
));
let hash = [2u8; 32];
let sig = ModuleSignatureData::sign(&hash, &signing_key);
assert_eq!(
kc.verify_module("std::core::math", &hash, Some(&sig)),
VerifyResult::Trusted
);
}
#[test]
fn test_scoped_trust_rejects_non_matching() {
let (signing_key, verifying_key) = generate_keypair();
let mut kc = Keychain::new(true);
kc.add_trusted(make_author(
"bob",
verifying_key.to_bytes(),
TrustLevel::Scoped(vec!["std::".to_string()]),
));
let hash = [2u8; 32];
let sig = ModuleSignatureData::sign(&hash, &signing_key);
let result = kc.verify_module("vendor::malware", &hash, Some(&sig));
assert!(matches!(result, VerifyResult::Rejected(_)));
}
#[test]
fn test_pinned_trust_matching_hash() {
let (signing_key, verifying_key) = generate_keypair();
let pinned_hash = [5u8; 32];
let mut kc = Keychain::new(true);
kc.add_trusted(make_author(
"carol",
verifying_key.to_bytes(),
TrustLevel::Pinned(pinned_hash),
));
let sig = ModuleSignatureData::sign(&pinned_hash, &signing_key);
assert_eq!(
kc.verify_module("some_mod", &pinned_hash, Some(&sig)),
VerifyResult::Trusted
);
}
#[test]
fn test_pinned_trust_wrong_hash() {
let (signing_key, verifying_key) = generate_keypair();
let pinned_hash = [5u8; 32];
let mut kc = Keychain::new(true);
kc.add_trusted(make_author(
"carol",
verifying_key.to_bytes(),
TrustLevel::Pinned(pinned_hash),
));
let different_hash = [6u8; 32];
let sig = ModuleSignatureData::sign(&different_hash, &signing_key);
let result = kc.verify_module("some_mod", &different_hash, Some(&sig));
assert!(matches!(result, VerifyResult::Rejected(_)));
}
#[test]
fn test_untrusted_key_rejected() {
let (signing_key, _) = generate_keypair();
let kc = Keychain::new(true);
let hash = [3u8; 32];
let sig = ModuleSignatureData::sign(&hash, &signing_key);
let result = kc.verify_module("my_mod", &hash, Some(&sig));
assert!(matches!(result, VerifyResult::Rejected(_)));
}
#[test]
fn test_invalid_signature_rejected() {
let (signing_key, verifying_key) = generate_keypair();
let mut kc = Keychain::new(true);
kc.add_trusted(make_author(
"dave",
verifying_key.to_bytes(),
TrustLevel::Full,
));
let hash = [4u8; 32];
let mut sig = ModuleSignatureData::sign(&hash, &signing_key);
sig.signature[0] ^= 0xFF; let result = kc.verify_module("mod", &hash, Some(&sig));
assert!(matches!(result, VerifyResult::Rejected(_)));
}
#[test]
fn test_remove_trusted() {
let (_, verifying_key) = generate_keypair();
let mut kc = Keychain::new(false);
let key_bytes = verifying_key.to_bytes();
kc.add_trusted(make_author("eve", key_bytes, TrustLevel::Full));
assert!(kc.is_trusted(&key_bytes, "any", &[0u8; 32]));
let removed = kc.remove_trusted(&key_bytes);
assert!(removed.is_some());
assert!(!kc.is_trusted(&key_bytes, "any", &[0u8; 32]));
}
}