use super::audit;
use crate::crypto::verify_signature;
use crate::database::universal_types::UniversalUuid;
use std::path::{Path, PathBuf};
use thiserror::Error;
use super::package_signer::{DbPackageSigner, DetachedSignature, PackageSigner};
use super::{DbKeyManager, KeyManager};
#[derive(Debug, Clone, Default)]
pub struct SecurityConfig {
pub require_signatures: bool,
pub key_encryption_key: Option<[u8; 32]>,
}
impl SecurityConfig {
pub fn require_signatures() -> Self {
Self {
require_signatures: true,
key_encryption_key: None,
}
}
pub fn development() -> Self {
Self::default()
}
pub fn with_encryption_key(mut self, key: [u8; 32]) -> Self {
self.key_encryption_key = Some(key);
self
}
}
#[derive(Debug, Error)]
pub enum VerificationError {
#[error("Package has been tampered with: hash mismatch (expected {expected}, got {actual})")]
TamperedPackage {
expected: String,
actual: String,
},
#[error("Package signed by untrusted key: {fingerprint}")]
UntrustedSigner {
fingerprint: String,
},
#[error("Invalid signature: cryptographic verification failed")]
InvalidSignature,
#[error("Signature not found for package (hash: {hash})")]
SignatureNotFound {
hash: String,
},
#[error("Signature file malformed: {reason}")]
MalformedSignature {
reason: String,
},
#[error("Failed to read package file: {error}")]
FileReadError {
error: String,
},
#[error("Failed to compute package hash: {error}")]
HashError {
error: String,
},
#[error("Database error: {error}")]
DatabaseError {
error: String,
},
#[error("Key manager error: {error}")]
KeyManagerError {
error: String,
},
}
#[derive(Debug, Clone, Default)]
pub enum SignatureSource {
Database,
DetachedFile {
path: PathBuf,
},
#[default]
Auto,
}
#[derive(Debug, Clone)]
pub struct VerificationResult {
pub package_hash: String,
pub signer_fingerprint: String,
pub signer_name: Option<String>,
}
pub async fn verify_package<P: AsRef<Path>>(
package_path: P,
org_id: UniversalUuid,
signature_source: SignatureSource,
package_signer: &DbPackageSigner,
key_manager: &DbKeyManager,
) -> Result<VerificationResult, VerificationError> {
let package_path = package_path.as_ref();
let package_data =
std::fs::read(package_path).map_err(|e| VerificationError::FileReadError {
error: e.to_string(),
})?;
let package_hash = compute_package_hash(&package_data)?;
let signature = match signature_source {
SignatureSource::Database => load_signature_from_db(&package_hash, package_signer).await?,
SignatureSource::DetachedFile { path } => load_signature_from_file(&path)?,
SignatureSource::Auto => {
let sig_path = package_path.with_extension(format!(
"{}.sig",
package_path
.extension()
.map(|e| e.to_str().unwrap_or(""))
.unwrap_or("")
));
if sig_path.exists() {
load_signature_from_file(&sig_path)?
} else {
load_signature_from_db(&package_hash, package_signer).await?
}
}
};
if signature.package_hash != package_hash {
audit::log_verification_failure(
org_id,
&package_hash,
"tampered",
Some(&signature.key_fingerprint),
);
return Err(VerificationError::TamperedPackage {
expected: signature.package_hash,
actual: package_hash,
});
}
let trusted_key = key_manager
.find_trusted_key(org_id, &signature.key_fingerprint)
.await
.map_err(|e| VerificationError::KeyManagerError {
error: e.to_string(),
})?
.ok_or_else(|| {
audit::log_verification_failure(
org_id,
&package_hash,
"untrusted_signer",
Some(&signature.key_fingerprint),
);
VerificationError::UntrustedSigner {
fingerprint: signature.key_fingerprint.clone(),
}
})?;
let hash_bytes = hex::decode(&package_hash).map_err(|e| VerificationError::HashError {
error: e.to_string(),
})?;
let sig_bytes = signature.signature_bytes().map_err(|_| {
audit::log_verification_failure(
org_id,
&package_hash,
"invalid_signature",
Some(&signature.key_fingerprint),
);
VerificationError::InvalidSignature
})?;
if verify_signature(&hash_bytes, &sig_bytes, &trusted_key.public_key).is_err() {
audit::log_verification_failure(
org_id,
&package_hash,
"invalid_signature",
Some(&signature.key_fingerprint),
);
return Err(VerificationError::InvalidSignature);
}
audit::log_verification_success(
org_id,
&package_hash,
&signature.key_fingerprint,
trusted_key.key_name.as_deref(),
);
Ok(VerificationResult {
package_hash,
signer_fingerprint: signature.key_fingerprint,
signer_name: trusted_key.key_name,
})
}
pub fn verify_package_offline<P: AsRef<Path>, S: AsRef<Path>>(
package_path: P,
signature_path: S,
public_key: &[u8],
) -> Result<VerificationResult, VerificationError> {
let package_path = package_path.as_ref();
let signature_path = signature_path.as_ref();
let package_data =
std::fs::read(package_path).map_err(|e| VerificationError::FileReadError {
error: e.to_string(),
})?;
let package_hash = compute_package_hash(&package_data)?;
let signature = load_signature_from_file(signature_path)?;
if signature.package_hash != package_hash {
return Err(VerificationError::TamperedPackage {
expected: signature.package_hash,
actual: package_hash,
});
}
let expected_fingerprint = crate::crypto::compute_key_fingerprint(public_key);
if signature.key_fingerprint != expected_fingerprint {
return Err(VerificationError::UntrustedSigner {
fingerprint: signature.key_fingerprint,
});
}
let hash_bytes = hex::decode(&package_hash).map_err(|e| VerificationError::HashError {
error: e.to_string(),
})?;
let sig_bytes = signature
.signature_bytes()
.map_err(|_| VerificationError::InvalidSignature)?;
verify_signature(&hash_bytes, &sig_bytes, public_key)
.map_err(|_| VerificationError::InvalidSignature)?;
tracing::info!(
event_type = "verification.success.offline",
package = %package_path.display(),
signer_fingerprint = %signature.key_fingerprint,
"Package signature verified (offline mode)"
);
Ok(VerificationResult {
package_hash,
signer_fingerprint: signature.key_fingerprint,
signer_name: None,
})
}
fn compute_package_hash(data: &[u8]) -> Result<String, VerificationError> {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(data);
Ok(hex::encode(hasher.finalize()))
}
async fn load_signature_from_db(
package_hash: &str,
package_signer: &DbPackageSigner,
) -> Result<DetachedSignature, VerificationError> {
let signature = package_signer
.find_signature(package_hash)
.await
.map_err(|e| VerificationError::DatabaseError {
error: e.to_string(),
})?
.ok_or_else(|| VerificationError::SignatureNotFound {
hash: package_hash.to_string(),
})?;
Ok(DetachedSignature::from_signature_info(&signature))
}
fn load_signature_from_file(path: &Path) -> Result<DetachedSignature, VerificationError> {
DetachedSignature::read_from_file(path).map_err(|e| VerificationError::MalformedSignature {
reason: e.to_string(),
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::crypto::generate_signing_keypair;
use base64::Engine;
use tempfile::NamedTempFile;
#[test]
fn test_security_config_default() {
let config = SecurityConfig::default();
assert!(!config.require_signatures);
assert!(config.key_encryption_key.is_none());
}
#[test]
fn test_security_config_require_signatures() {
let config = SecurityConfig::require_signatures();
assert!(config.require_signatures);
}
#[test]
fn test_security_config_with_encryption_key() {
let key = [0x42u8; 32];
let config = SecurityConfig::default().with_encryption_key(key);
assert_eq!(config.key_encryption_key, Some(key));
}
#[test]
fn test_verify_package_offline_with_invalid_signature() {
let package_file = NamedTempFile::new().unwrap();
std::fs::write(package_file.path(), b"test package content").unwrap();
let keypair = generate_signing_keypair();
let sig = DetachedSignature {
version: 1,
algorithm: "ed25519".to_string(),
package_hash: "wrong_hash".to_string(),
key_fingerprint: keypair.fingerprint.clone(),
signature: base64::engine::general_purpose::STANDARD.encode([0u8; 64]),
signed_at: chrono::Utc::now().to_rfc3339(),
};
let sig_file = NamedTempFile::new().unwrap();
sig.write_to_file(sig_file.path()).unwrap();
let result =
verify_package_offline(package_file.path(), sig_file.path(), &keypair.public_key);
assert!(matches!(
result,
Err(VerificationError::TamperedPackage { .. })
));
}
#[test]
fn test_signature_source_default() {
let source = SignatureSource::default();
assert!(matches!(source, SignatureSource::Auto));
}
#[test]
fn test_verify_package_offline_valid_signature() {
let package_file = NamedTempFile::new().unwrap();
let content = b"valid package content";
std::fs::write(package_file.path(), content).unwrap();
let keypair = generate_signing_keypair();
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(content);
let package_hash = hex::encode(hasher.finalize());
let hash_bytes = hex::decode(&package_hash).unwrap();
let signature_bytes =
crate::crypto::sign_package(&hash_bytes, &keypair.private_key).unwrap();
let sig = DetachedSignature {
version: 1,
algorithm: "ed25519".to_string(),
package_hash: package_hash.clone(),
key_fingerprint: keypair.fingerprint.clone(),
signature: base64::engine::general_purpose::STANDARD.encode(&signature_bytes),
signed_at: chrono::Utc::now().to_rfc3339(),
};
let sig_file = NamedTempFile::new().unwrap();
sig.write_to_file(sig_file.path()).unwrap();
let result =
verify_package_offline(package_file.path(), sig_file.path(), &keypair.public_key);
assert!(result.is_ok());
let verification = result.unwrap();
assert_eq!(verification.package_hash, package_hash);
assert_eq!(verification.signer_fingerprint, keypair.fingerprint);
}
#[test]
fn test_verify_package_offline_tampered_content() {
let package_file = NamedTempFile::new().unwrap();
let original = b"original content";
std::fs::write(package_file.path(), original).unwrap();
let keypair = generate_signing_keypair();
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(original);
let package_hash = hex::encode(hasher.finalize());
let hash_bytes = hex::decode(&package_hash).unwrap();
let signature_bytes =
crate::crypto::sign_package(&hash_bytes, &keypair.private_key).unwrap();
let sig = DetachedSignature {
version: 1,
algorithm: "ed25519".to_string(),
package_hash,
key_fingerprint: keypair.fingerprint.clone(),
signature: base64::engine::general_purpose::STANDARD.encode(&signature_bytes),
signed_at: chrono::Utc::now().to_rfc3339(),
};
let sig_file = NamedTempFile::new().unwrap();
sig.write_to_file(sig_file.path()).unwrap();
std::fs::write(package_file.path(), b"tampered content").unwrap();
let result =
verify_package_offline(package_file.path(), sig_file.path(), &keypair.public_key);
assert!(matches!(
result,
Err(VerificationError::TamperedPackage { .. })
));
}
#[test]
fn test_verify_package_offline_wrong_key() {
let package_file = NamedTempFile::new().unwrap();
let content = b"content";
std::fs::write(package_file.path(), content).unwrap();
let signer = generate_signing_keypair();
let wrong_verifier = generate_signing_keypair();
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(content);
let package_hash = hex::encode(hasher.finalize());
let hash_bytes = hex::decode(&package_hash).unwrap();
let signature_bytes =
crate::crypto::sign_package(&hash_bytes, &signer.private_key).unwrap();
let sig = DetachedSignature {
version: 1,
algorithm: "ed25519".to_string(),
package_hash,
key_fingerprint: signer.fingerprint.clone(),
signature: base64::engine::general_purpose::STANDARD.encode(&signature_bytes),
signed_at: chrono::Utc::now().to_rfc3339(),
};
let sig_file = NamedTempFile::new().unwrap();
sig.write_to_file(sig_file.path()).unwrap();
let result = verify_package_offline(
package_file.path(),
sig_file.path(),
&wrong_verifier.public_key,
);
assert!(result.is_err());
}
#[test]
fn test_verify_package_offline_nonexistent_package() {
let keypair = generate_signing_keypair();
let sig_file = NamedTempFile::new().unwrap();
let sig = DetachedSignature {
version: 1,
algorithm: "ed25519".to_string(),
package_hash: "hash".to_string(),
key_fingerprint: "fp".to_string(),
signature: "sig".to_string(),
signed_at: chrono::Utc::now().to_rfc3339(),
};
sig.write_to_file(sig_file.path()).unwrap();
let result = verify_package_offline(
std::path::Path::new("/nonexistent/package"),
sig_file.path(),
&keypair.public_key,
);
assert!(result.is_err());
}
#[test]
fn test_verify_package_offline_nonexistent_signature() {
let keypair = generate_signing_keypair();
let package_file = NamedTempFile::new().unwrap();
std::fs::write(package_file.path(), b"content").unwrap();
let result = verify_package_offline(
package_file.path(),
std::path::Path::new("/nonexistent/sig"),
&keypair.public_key,
);
assert!(result.is_err());
}
#[test]
fn test_load_signature_from_file_valid() {
let sig = DetachedSignature {
version: 1,
algorithm: "ed25519".to_string(),
package_hash: "abc".to_string(),
key_fingerprint: "def".to_string(),
signature: "c2lnbmF0dXJl".to_string(),
signed_at: chrono::Utc::now().to_rfc3339(),
};
let temp = NamedTempFile::new().unwrap();
sig.write_to_file(temp.path()).unwrap();
let result = load_signature_from_file(temp.path());
assert!(result.is_ok());
assert_eq!(result.unwrap().package_hash, "abc");
}
#[test]
fn test_load_signature_from_file_invalid() {
let temp = NamedTempFile::new().unwrap();
std::fs::write(temp.path(), "not json").unwrap();
let result = load_signature_from_file(temp.path());
assert!(matches!(
result,
Err(VerificationError::MalformedSignature { .. })
));
}
}