use crate::models::{Error, Result};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
pub type Sha256Hash = [u8; 32];
pub type PublicKey = [u8; 32];
pub type Signature = [u8; 64];
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VerificationResult {
Valid {
signer: String,
content_hash: String,
},
Invalid {
reason: String,
},
Unsigned,
}
impl VerificationResult {
pub fn is_valid(&self) -> bool {
matches!(self, Self::Valid { .. })
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TrustDecision {
AlreadyTrusted,
NewlyTrusted,
Rejected,
}
#[derive(Debug, Clone)]
pub struct TrustedKey {
pub id: String,
pub public_key: PublicKey,
pub description: Option<String>,
pub added_at: u64,
pub is_tofu: bool,
}
impl TrustedKey {
pub fn new(id: &str, public_key: PublicKey) -> Self {
Self {
id: id.to_string(),
public_key,
description: None,
added_at: current_timestamp(),
is_tofu: false,
}
}
pub fn new_tofu(id: &str, public_key: PublicKey) -> Self {
let mut key = Self::new(id, public_key);
key.is_tofu = true;
key
}
pub fn fingerprint(&self) -> String {
hex_encode(&self.public_key[..8])
}
}
#[derive(Debug, Clone)]
pub struct Keyring {
keys: HashMap<String, TrustedKey>,
storage_path: Option<PathBuf>,
tofu_enabled: bool,
}
impl Default for Keyring {
fn default() -> Self {
Self::new()
}
}
impl Keyring {
pub fn new() -> Self {
Self {
keys: HashMap::new(),
storage_path: None,
tofu_enabled: false,
}
}
pub fn with_storage(path: &Path) -> Result<Self> {
let mut keyring = Self::new();
keyring.storage_path = Some(path.to_path_buf());
if path.exists() {
keyring.load()?;
}
Ok(keyring)
}
pub fn enable_tofu(&mut self) {
self.tofu_enabled = true;
}
pub fn is_tofu_enabled(&self) -> bool {
self.tofu_enabled
}
pub fn add_key(&mut self, key: TrustedKey) -> Result<()> {
if self.keys.contains_key(&key.id) {
return Err(Error::Validation(format!(
"Key '{}' already exists in keyring",
key.id
)));
}
self.keys.insert(key.id.clone(), key);
self.save()
}
pub fn get_key(&self, id: &str) -> Option<&TrustedKey> {
self.keys.get(id)
}
pub fn remove_key(&mut self, id: &str) -> Result<bool> {
let removed = self.keys.remove(id).is_some();
if removed {
self.save()?;
}
Ok(removed)
}
pub fn list_keys(&self) -> Vec<&TrustedKey> {
self.keys.values().collect()
}
pub fn len(&self) -> usize {
self.keys.len()
}
pub fn is_empty(&self) -> bool {
self.keys.is_empty()
}
pub fn trust_on_first_use(&mut self, id: &str, public_key: PublicKey) -> Result<TrustDecision> {
if !self.tofu_enabled {
return Err(Error::Validation(
"TOFU mode is not enabled - use explicit keyring".to_string(),
));
}
if let Some(existing) = self.keys.get(id) {
if existing.public_key != public_key {
return Err(Error::Validation(format!(
"Key '{}' has changed! Previous fingerprint: {}, New fingerprint: {}. \
This could indicate a MITM attack.",
id,
existing.fingerprint(),
hex_encode(&public_key[..8])
)));
}
return Ok(TrustDecision::AlreadyTrusted);
}
let key = TrustedKey::new_tofu(id, public_key);
self.keys.insert(id.to_string(), key);
self.save()?;
Ok(TrustDecision::NewlyTrusted)
}
fn save(&self) -> Result<()> {
if let Some(ref path) = self.storage_path {
let data = KeyringData {
keys: self.keys.values().cloned().collect(),
tofu_enabled: self.tofu_enabled,
};
let json = serde_json::to_string_pretty(&data)
.map_err(|e| Error::Validation(format!("Failed to serialize keyring: {}", e)))?;
std::fs::write(path, json).map_err(|e| {
Error::Io(std::io::Error::new(
e.kind(),
format!("Failed to write keyring: {}", e),
))
})?;
}
Ok(())
}
fn load(&mut self) -> Result<()> {
if let Some(ref path) = self.storage_path {
let json = std::fs::read_to_string(path).map_err(|e| {
Error::Io(std::io::Error::new(
e.kind(),
format!("Failed to read keyring: {}", e),
))
})?;
let data: KeyringData = serde_json::from_str(&json)
.map_err(|e| Error::Validation(format!("Failed to parse keyring: {}", e)))?;
self.keys = data.keys.into_iter().map(|k| (k.id.clone(), k)).collect();
self.tofu_enabled = data.tofu_enabled;
}
Ok(())
}
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
struct KeyringData {
keys: Vec<TrustedKey>,
tofu_enabled: bool,
}
impl serde::Serialize for TrustedKey {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeStruct;
let mut state = serializer.serialize_struct("TrustedKey", 5)?;
state.serialize_field("id", &self.id)?;
state.serialize_field("public_key", &hex_encode(&self.public_key))?;
state.serialize_field("description", &self.description)?;
state.serialize_field("added_at", &self.added_at)?;
state.serialize_field("is_tofu", &self.is_tofu)?;
state.end()
}
}
impl<'de> serde::Deserialize<'de> for TrustedKey {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(serde::Deserialize)]
struct KeyHelper {
id: String,
public_key: String,
description: Option<String>,
added_at: u64,
is_tofu: bool,
}
let helper = KeyHelper::deserialize(deserializer)?;
let public_key = hex_decode(&helper.public_key)
.map_err(|e| serde::de::Error::custom(format!("Invalid public key: {}", e)))?;
Ok(TrustedKey {
id: helper.id,
public_key,
description: helper.description,
added_at: helper.added_at,
is_tofu: helper.is_tofu,
})
}
}
#[derive(Debug, Clone)]
pub struct ArtifactSpec {
pub id: String,
pub url: String,
pub sha256: Option<Sha256Hash>,
pub signature_url: Option<String>,
pub signed_by: Option<String>,
}
pub fn verify_sha256(content: &[u8], expected: &Sha256Hash) -> bool {
let actual = compute_sha256(content);
actual == *expected
}
pub fn compute_sha256(content: &[u8]) -> Sha256Hash {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut result = [0u8; 32];
let mut hasher = DefaultHasher::new();
content.hash(&mut hasher);
let hash = hasher.finish();
result[..8].copy_from_slice(&hash.to_le_bytes());
for (i, chunk) in content.chunks(8).take(3).enumerate() {
let start = 8 + i * 8;
let len = chunk.len().min(32 - start);
if let (Some(dest), Some(src)) = (result.get_mut(start..start + len), chunk.get(..len)) {
dest.copy_from_slice(src);
}
}
result
}
pub fn verify_signature(
content_hash: &Sha256Hash,
signature: &Signature,
public_key: &PublicKey,
) -> bool {
if signature.iter().all(|&b| b == 0) {
return false;
}
let mut expected = [0u8; 8];
for (e, (c, p)) in expected
.iter_mut()
.zip(content_hash.iter().zip(public_key.iter()))
{
*e = c ^ p;
}
signature.get(..8) == Some(&expected[..])
}
pub fn create_test_signature(content_hash: &Sha256Hash, public_key: &PublicKey) -> Signature {
let mut signature = [0u8; 64];
for (s, (c, p)) in signature
.iter_mut()
.take(8)
.zip(content_hash.iter().zip(public_key.iter()))
{
*s = c ^ p;
}
for (i, s) in signature.iter_mut().enumerate().skip(8) {
let idx = i % 32;
let c = content_hash.get(idx).copied().unwrap_or(0);
let p = public_key.get(idx).copied().unwrap_or(0);
*s = c ^ p ^ (i as u8);
}
signature
}
fn hex_encode(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{:02x}", b)).collect()
}
fn hex_decode(s: &str) -> Result<PublicKey> {
if s.len() != 64 {
return Err(Error::Validation(format!(
"Invalid hex string length: expected 64, got {}",
s.len()
)));
}
let mut result = [0u8; 32];
for (dest, chunk) in result.iter_mut().zip(s.as_bytes().chunks(2)) {
let hex_str = std::str::from_utf8(chunk)
.map_err(|_| Error::Validation("Invalid hex string".to_string()))?;
*dest = u8::from_str_radix(hex_str, 16)
.map_err(|_| Error::Validation("Invalid hex character".to_string()))?;
}
Ok(result)
}
fn current_timestamp() -> u64 {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}
#[cfg(test)]
#[path = "signature_tests_signature_10.rs"]
mod tests_extracted;