use super::audit;
use crate::crypto::{compute_key_fingerprint, sign_package as crypto_sign, verify_signature};
use crate::dal::unified::models::{NewUnifiedPackageSignature, UnifiedPackageSignature};
use crate::dal::unified::DAL;
use crate::database::schema::unified::package_signatures;
use crate::database::universal_types::{UniversalBinary, UniversalTimestamp, UniversalUuid};
use async_trait::async_trait;
use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
use diesel::prelude::*;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::path::Path;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum PackageSignError {
#[error("Failed to read package file: {0}")]
FileReadError(#[from] std::io::Error),
#[error("Signing failed: {0}")]
SigningFailed(String),
#[error("Key not found: {0}")]
KeyNotFound(UniversalUuid),
#[error("Key has been revoked")]
KeyRevoked,
#[error("Database error: {0}")]
Database(String),
#[error("Signature not found for package hash: {0}")]
SignatureNotFound(String),
#[error("Verification failed: {0}")]
VerificationFailed(String),
#[error("Invalid signature file format: {0}")]
InvalidSignatureFile(String),
}
#[derive(Debug, Clone)]
pub struct PackageSignatureInfo {
pub package_hash: String,
pub key_fingerprint: String,
pub signature: Vec<u8>,
pub signed_at: UniversalTimestamp,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DetachedSignature {
pub version: u32,
pub algorithm: String,
pub package_hash: String,
pub key_fingerprint: String,
pub signature: String,
pub signed_at: String,
}
impl DetachedSignature {
pub const VERSION: u32 = 1;
pub const ALGORITHM: &'static str = "ed25519";
pub fn from_signature_info(info: &PackageSignatureInfo) -> Self {
Self {
version: Self::VERSION,
algorithm: Self::ALGORITHM.to_string(),
package_hash: info.package_hash.clone(),
key_fingerprint: info.key_fingerprint.clone(),
signature: BASE64.encode(&info.signature),
signed_at: info.signed_at.to_rfc3339(),
}
}
pub fn from_json(json: &str) -> Result<Self, PackageSignError> {
serde_json::from_str(json)
.map_err(|e| PackageSignError::InvalidSignatureFile(e.to_string()))
}
pub fn to_json(&self) -> Result<String, PackageSignError> {
serde_json::to_string_pretty(self)
.map_err(|e| PackageSignError::InvalidSignatureFile(e.to_string()))
}
pub fn signature_bytes(&self) -> Result<Vec<u8>, PackageSignError> {
BASE64
.decode(&self.signature)
.map_err(|e| PackageSignError::InvalidSignatureFile(e.to_string()))
}
pub fn write_to_file(&self, path: &Path) -> Result<(), PackageSignError> {
let json = self.to_json()?;
std::fs::write(path, json)?;
Ok(())
}
pub fn read_from_file(path: &Path) -> Result<Self, PackageSignError> {
let json = std::fs::read_to_string(path)?;
Self::from_json(&json)
}
}
#[async_trait]
pub trait PackageSigner: Send + Sync {
async fn sign_package_with_db_key(
&self,
package_path: &Path,
key_id: UniversalUuid,
master_key: &[u8],
store_signature: bool,
) -> Result<PackageSignatureInfo, PackageSignError>;
fn sign_package_with_raw_key(
&self,
package_path: &Path,
private_key: &[u8],
public_key: &[u8],
) -> Result<PackageSignatureInfo, PackageSignError>;
fn sign_package_data(
&self,
package_data: &[u8],
private_key: &[u8],
public_key: &[u8],
) -> Result<PackageSignatureInfo, PackageSignError>;
async fn store_signature(
&self,
signature: &PackageSignatureInfo,
) -> Result<UniversalUuid, PackageSignError>;
async fn find_signature(
&self,
package_hash: &str,
) -> Result<Option<PackageSignatureInfo>, PackageSignError>;
async fn find_signatures(
&self,
package_hash: &str,
) -> Result<Vec<PackageSignatureInfo>, PackageSignError>;
async fn verify_package(
&self,
package_path: &Path,
org_id: UniversalUuid,
) -> Result<PackageSignatureInfo, PackageSignError>;
fn verify_package_with_detached_signature(
&self,
package_path: &Path,
signature: &DetachedSignature,
public_key: &[u8],
) -> Result<(), PackageSignError>;
}
#[derive(Clone)]
pub struct DbPackageSigner {
dal: DAL,
}
impl DbPackageSigner {
pub fn new(dal: DAL) -> Self {
Self { dal }
}
fn compute_file_hash(path: &Path) -> Result<String, PackageSignError> {
let data = std::fs::read(path)?;
Self::compute_data_hash(&data)
}
fn compute_data_hash(data: &[u8]) -> Result<String, PackageSignError> {
let mut hasher = Sha256::new();
hasher.update(data);
Ok(hex::encode(hasher.finalize()))
}
fn to_signature_info(sig: UnifiedPackageSignature) -> PackageSignatureInfo {
PackageSignatureInfo {
package_hash: sig.package_hash,
key_fingerprint: sig.key_fingerprint,
signature: sig.signature.into_inner(),
signed_at: sig.signed_at,
}
}
}
#[async_trait]
impl PackageSigner for DbPackageSigner {
async fn sign_package_with_db_key(
&self,
package_path: &Path,
key_id: UniversalUuid,
master_key: &[u8],
store_signature: bool,
) -> Result<PackageSignatureInfo, PackageSignError> {
use super::db_key_manager::DbKeyManager;
use super::key_manager::KeyManager;
let path_str = package_path.display().to_string();
let key_manager = DbKeyManager::new(self.dal.clone());
let (public_key, private_key) = key_manager
.get_signing_key(key_id, master_key)
.await
.map_err(|e| {
audit::log_package_sign_failed(&path_str, &e.to_string());
match e {
super::key_manager::KeyError::NotFound(_) => {
PackageSignError::KeyNotFound(key_id)
}
super::key_manager::KeyError::Revoked(_) => PackageSignError::KeyRevoked,
e => PackageSignError::SigningFailed(e.to_string()),
}
})?;
let signature = self
.sign_package_with_raw_key(package_path, &private_key, &public_key)
.inspect_err(|e| {
audit::log_package_sign_failed(&path_str, &e.to_string());
})?;
audit::log_package_signed(
&path_str,
&signature.package_hash,
&signature.key_fingerprint,
);
if store_signature {
self.store_signature(&signature).await?;
}
Ok(signature)
}
fn sign_package_with_raw_key(
&self,
package_path: &Path,
private_key: &[u8],
public_key: &[u8],
) -> Result<PackageSignatureInfo, PackageSignError> {
let package_data = std::fs::read(package_path)?;
self.sign_package_data(&package_data, private_key, public_key)
}
fn sign_package_data(
&self,
package_data: &[u8],
private_key: &[u8],
public_key: &[u8],
) -> Result<PackageSignatureInfo, PackageSignError> {
let package_hash = Self::compute_data_hash(package_data)?;
let hash_bytes = hex::decode(&package_hash)
.map_err(|e| PackageSignError::SigningFailed(e.to_string()))?;
let signature = crypto_sign(&hash_bytes, private_key)
.map_err(|e| PackageSignError::SigningFailed(e.to_string()))?;
let fingerprint = compute_key_fingerprint(public_key);
Ok(PackageSignatureInfo {
package_hash,
key_fingerprint: fingerprint,
signature,
signed_at: UniversalTimestamp::now(),
})
}
async fn store_signature(
&self,
signature: &PackageSignatureInfo,
) -> Result<UniversalUuid, PackageSignError> {
let id = UniversalUuid::new_v4();
let new_sig = NewUnifiedPackageSignature {
id,
package_hash: signature.package_hash.clone(),
key_fingerprint: signature.key_fingerprint.clone(),
signature: UniversalBinary::new(signature.signature.clone()),
signed_at: signature.signed_at,
org_id: None,
};
#[cfg(all(feature = "postgres", feature = "sqlite"))]
{
match self.dal.backend() {
crate::database::BackendType::Postgres => {
self.store_signature_postgres(new_sig).await?
}
crate::database::BackendType::Sqlite => {
self.store_signature_sqlite(new_sig).await?
}
}
}
#[cfg(all(feature = "postgres", not(feature = "sqlite")))]
{
self.store_signature_postgres(new_sig).await?
}
#[cfg(all(feature = "sqlite", not(feature = "postgres")))]
{
self.store_signature_sqlite(new_sig).await?
}
Ok(id)
}
async fn find_signature(
&self,
package_hash: &str,
) -> Result<Option<PackageSignatureInfo>, PackageSignError> {
crate::dispatch_backend!(
self.dal.backend(),
self.find_signature_postgres(package_hash).await,
self.find_signature_sqlite(package_hash).await
)
}
async fn find_signatures(
&self,
package_hash: &str,
) -> Result<Vec<PackageSignatureInfo>, PackageSignError> {
crate::dispatch_backend!(
self.dal.backend(),
self.find_signatures_postgres(package_hash).await,
self.find_signatures_sqlite(package_hash).await
)
}
async fn verify_package(
&self,
package_path: &Path,
org_id: UniversalUuid,
) -> Result<PackageSignatureInfo, PackageSignError> {
use super::db_key_manager::DbKeyManager;
use super::key_manager::KeyManager;
let package_hash = Self::compute_file_hash(package_path)?;
let signatures = self.find_signatures(&package_hash).await?;
if signatures.is_empty() {
return Err(PackageSignError::SignatureNotFound(package_hash));
}
let key_manager = DbKeyManager::new(self.dal.clone());
let hash_bytes = hex::decode(&package_hash)
.map_err(|e| PackageSignError::VerificationFailed(e.to_string()))?;
for sig in signatures {
if let Ok(Some(trusted_key)) = key_manager
.find_trusted_key(org_id, &sig.key_fingerprint)
.await
{
if verify_signature(&hash_bytes, &sig.signature, &trusted_key.public_key).is_ok() {
return Ok(sig);
}
}
}
Err(PackageSignError::VerificationFailed(
"No valid signature from a trusted key".to_string(),
))
}
fn verify_package_with_detached_signature(
&self,
package_path: &Path,
signature: &DetachedSignature,
public_key: &[u8],
) -> Result<(), PackageSignError> {
if signature.algorithm != DetachedSignature::ALGORITHM {
return Err(PackageSignError::InvalidSignatureFile(format!(
"Unsupported algorithm: {}",
signature.algorithm
)));
}
let package_hash = Self::compute_file_hash(package_path)?;
if package_hash != signature.package_hash {
return Err(PackageSignError::VerificationFailed(
"Package hash does not match signature".to_string(),
));
}
let expected_fingerprint = compute_key_fingerprint(public_key);
if expected_fingerprint != signature.key_fingerprint {
return Err(PackageSignError::VerificationFailed(
"Key fingerprint does not match signature".to_string(),
));
}
let hash_bytes = hex::decode(&package_hash)
.map_err(|e| PackageSignError::VerificationFailed(e.to_string()))?;
let sig_bytes = signature.signature_bytes()?;
verify_signature(&hash_bytes, &sig_bytes, public_key)
.map_err(|_| PackageSignError::VerificationFailed("Invalid signature".to_string()))?;
Ok(())
}
}
#[cfg(feature = "postgres")]
impl DbPackageSigner {
async fn store_signature_postgres(
&self,
new_sig: NewUnifiedPackageSignature,
) -> Result<(), PackageSignError> {
let conn = self
.dal
.database
.get_postgres_connection()
.await
.map_err(|e| PackageSignError::Database(e.to_string()))?;
conn.interact(move |conn| {
diesel::insert_into(package_signatures::table)
.values(&new_sig)
.execute(conn)
})
.await
.map_err(|e| PackageSignError::Database(e.to_string()))?
.map_err(|e| PackageSignError::Database(e.to_string()))?;
Ok(())
}
async fn find_signature_postgres(
&self,
package_hash: &str,
) -> Result<Option<PackageSignatureInfo>, PackageSignError> {
let conn = self
.dal
.database
.get_postgres_connection()
.await
.map_err(|e| PackageSignError::Database(e.to_string()))?;
let hash = package_hash.to_string();
let sig: Option<UnifiedPackageSignature> = conn
.interact(move |conn| {
package_signatures::table
.filter(package_signatures::package_hash.eq(&hash))
.first(conn)
.optional()
})
.await
.map_err(|e| PackageSignError::Database(e.to_string()))?
.map_err(|e| PackageSignError::Database(e.to_string()))?;
Ok(sig.map(Self::to_signature_info))
}
async fn find_signatures_postgres(
&self,
package_hash: &str,
) -> Result<Vec<PackageSignatureInfo>, PackageSignError> {
let conn = self
.dal
.database
.get_postgres_connection()
.await
.map_err(|e| PackageSignError::Database(e.to_string()))?;
let hash = package_hash.to_string();
let sigs: Vec<UnifiedPackageSignature> = conn
.interact(move |conn| {
package_signatures::table
.filter(package_signatures::package_hash.eq(&hash))
.load(conn)
})
.await
.map_err(|e| PackageSignError::Database(e.to_string()))?
.map_err(|e| PackageSignError::Database(e.to_string()))?;
Ok(sigs.into_iter().map(Self::to_signature_info).collect())
}
}
#[cfg(feature = "sqlite")]
impl DbPackageSigner {
async fn store_signature_sqlite(
&self,
new_sig: NewUnifiedPackageSignature,
) -> Result<(), PackageSignError> {
let conn = self
.dal
.database
.get_sqlite_connection()
.await
.map_err(|e| PackageSignError::Database(e.to_string()))?;
conn.interact(move |conn| {
diesel::insert_into(package_signatures::table)
.values(&new_sig)
.execute(conn)
})
.await
.map_err(|e| PackageSignError::Database(e.to_string()))?
.map_err(|e| PackageSignError::Database(e.to_string()))?;
Ok(())
}
async fn find_signature_sqlite(
&self,
package_hash: &str,
) -> Result<Option<PackageSignatureInfo>, PackageSignError> {
let conn = self
.dal
.database
.get_sqlite_connection()
.await
.map_err(|e| PackageSignError::Database(e.to_string()))?;
let hash = package_hash.to_string();
let sig: Option<UnifiedPackageSignature> = conn
.interact(move |conn| {
package_signatures::table
.filter(package_signatures::package_hash.eq(&hash))
.first(conn)
.optional()
})
.await
.map_err(|e| PackageSignError::Database(e.to_string()))?
.map_err(|e| PackageSignError::Database(e.to_string()))?;
Ok(sig.map(Self::to_signature_info))
}
async fn find_signatures_sqlite(
&self,
package_hash: &str,
) -> Result<Vec<PackageSignatureInfo>, PackageSignError> {
let conn = self
.dal
.database
.get_sqlite_connection()
.await
.map_err(|e| PackageSignError::Database(e.to_string()))?;
let hash = package_hash.to_string();
let sigs: Vec<UnifiedPackageSignature> = conn
.interact(move |conn| {
package_signatures::table
.filter(package_signatures::package_hash.eq(&hash))
.load(conn)
})
.await
.map_err(|e| PackageSignError::Database(e.to_string()))?
.map_err(|e| PackageSignError::Database(e.to_string()))?;
Ok(sigs.into_iter().map(Self::to_signature_info).collect())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::crypto::generate_signing_keypair;
use tempfile::NamedTempFile;
#[test]
fn test_sign_and_verify_with_raw_key() {
let temp_file = NamedTempFile::new().unwrap();
std::fs::write(temp_file.path(), b"test package content").unwrap();
let keypair = generate_signing_keypair();
let package_data = std::fs::read(temp_file.path()).unwrap();
let package_hash = DbPackageSigner::compute_data_hash(&package_data).unwrap();
let hash_bytes = hex::decode(&package_hash).unwrap();
let signature = crate::crypto::sign_package(&hash_bytes, &keypair.private_key).unwrap();
let result = crate::crypto::verify_signature(&hash_bytes, &signature, &keypair.public_key);
assert!(result.is_ok());
}
#[test]
fn test_detached_signature_roundtrip() {
let info = PackageSignatureInfo {
package_hash: "abc123".to_string(),
key_fingerprint: "def456".to_string(),
signature: vec![0u8; 64],
signed_at: UniversalTimestamp::now(),
};
let detached = DetachedSignature::from_signature_info(&info);
let json = detached.to_json().unwrap();
let parsed = DetachedSignature::from_json(&json).unwrap();
assert_eq!(parsed.version, DetachedSignature::VERSION);
assert_eq!(parsed.algorithm, DetachedSignature::ALGORITHM);
assert_eq!(parsed.package_hash, info.package_hash);
assert_eq!(parsed.key_fingerprint, info.key_fingerprint);
assert_eq!(parsed.signature_bytes().unwrap(), info.signature);
}
#[test]
fn test_detached_signature_file_io() {
let info = PackageSignatureInfo {
package_hash: "abc123".to_string(),
key_fingerprint: "def456".to_string(),
signature: vec![0u8; 64],
signed_at: UniversalTimestamp::now(),
};
let detached = DetachedSignature::from_signature_info(&info);
let temp_file = NamedTempFile::new().unwrap();
detached.write_to_file(temp_file.path()).unwrap();
let loaded = DetachedSignature::read_from_file(temp_file.path()).unwrap();
assert_eq!(loaded.package_hash, info.package_hash);
}
#[test]
fn test_compute_data_hash_deterministic() {
let data = b"hello world";
let hash1 = DbPackageSigner::compute_data_hash(data).unwrap();
let hash2 = DbPackageSigner::compute_data_hash(data).unwrap();
assert_eq!(hash1, hash2);
}
#[test]
fn test_compute_data_hash_different_inputs() {
let hash1 = DbPackageSigner::compute_data_hash(b"aaa").unwrap();
let hash2 = DbPackageSigner::compute_data_hash(b"bbb").unwrap();
assert_ne!(hash1, hash2);
}
#[test]
fn test_compute_data_hash_empty_input() {
let result = DbPackageSigner::compute_data_hash(b"");
assert!(result.is_ok());
assert!(!result.unwrap().is_empty());
}
#[test]
fn test_compute_data_hash_large_payload() {
let data = vec![0xFFu8; 10 * 1024 * 1024]; let result = DbPackageSigner::compute_data_hash(&data);
assert!(result.is_ok());
}
#[test]
fn test_compute_file_hash_matches_data_hash() {
let temp_file = NamedTempFile::new().unwrap();
let content = b"test content for hash comparison";
std::fs::write(temp_file.path(), content).unwrap();
let file_hash = DbPackageSigner::compute_file_hash(temp_file.path()).unwrap();
let data_hash = DbPackageSigner::compute_data_hash(content).unwrap();
assert_eq!(file_hash, data_hash);
}
#[test]
fn test_compute_file_hash_nonexistent_file() {
let result = DbPackageSigner::compute_file_hash(std::path::Path::new("/nonexistent/file"));
assert!(result.is_err());
}
#[test]
fn test_detached_signature_invalid_json() {
let result = DetachedSignature::from_json("not valid json");
assert!(result.is_err());
}
#[test]
fn test_detached_signature_version_and_algorithm() {
let info = PackageSignatureInfo {
package_hash: "hash".to_string(),
key_fingerprint: "fp".to_string(),
signature: vec![1, 2, 3],
signed_at: UniversalTimestamp::now(),
};
let detached = DetachedSignature::from_signature_info(&info);
assert_eq!(detached.version, DetachedSignature::VERSION);
assert_eq!(detached.algorithm, DetachedSignature::ALGORITHM);
}
#[test]
fn test_detached_signature_corrupted_base64() {
let info = PackageSignatureInfo {
package_hash: "hash".to_string(),
key_fingerprint: "fp".to_string(),
signature: vec![1, 2, 3],
signed_at: UniversalTimestamp::now(),
};
let mut detached = DetachedSignature::from_signature_info(&info);
detached.signature = "not!valid!base64!!!".to_string();
let result = detached.signature_bytes();
assert!(result.is_err());
}
#[test]
fn test_sign_verify_roundtrip_different_data() {
let keypair = generate_signing_keypair();
for content in &[
b"small".as_slice(),
b"medium content here",
&vec![0xAB; 1024],
] {
let hash = DbPackageSigner::compute_data_hash(content).unwrap();
let hash_bytes = hex::decode(&hash).unwrap();
let signature = crate::crypto::sign_package(&hash_bytes, &keypair.private_key).unwrap();
let result =
crate::crypto::verify_signature(&hash_bytes, &signature, &keypair.public_key);
assert!(
result.is_ok(),
"verification failed for content of len {}",
content.len()
);
}
}
#[test]
fn test_sign_verify_wrong_key_fails() {
let keypair1 = generate_signing_keypair();
let keypair2 = generate_signing_keypair();
let data = b"test data";
let hash = DbPackageSigner::compute_data_hash(data).unwrap();
let hash_bytes = hex::decode(&hash).unwrap();
let signature = crate::crypto::sign_package(&hash_bytes, &keypair1.private_key).unwrap();
let result = crate::crypto::verify_signature(&hash_bytes, &signature, &keypair2.public_key);
assert!(result.is_err());
}
#[test]
fn test_sign_verify_tampered_data_fails() {
let keypair = generate_signing_keypair();
let data = b"original data";
let hash = DbPackageSigner::compute_data_hash(data).unwrap();
let hash_bytes = hex::decode(&hash).unwrap();
let signature = crate::crypto::sign_package(&hash_bytes, &keypair.private_key).unwrap();
let tampered_hash = DbPackageSigner::compute_data_hash(b"tampered data").unwrap();
let tampered_bytes = hex::decode(&tampered_hash).unwrap();
let result =
crate::crypto::verify_signature(&tampered_bytes, &signature, &keypair.public_key);
assert!(result.is_err());
}
#[cfg(feature = "sqlite")]
mod db_tests {
use super::*;
use crate::dal::unified::DAL;
use crate::database::Database;
use crate::security::db_key_manager::DbKeyManager;
use crate::security::key_manager::KeyManager;
async fn unique_dal() -> DAL {
let url = format!(
"file:pkgsign_test_{}?mode=memory&cache=shared",
uuid::Uuid::new_v4()
);
let db = Database::new(&url, "", 5);
db.run_migrations()
.await
.expect("migrations should succeed");
DAL::new(db)
}
fn master_key() -> [u8; 32] {
[0u8; 32]
}
#[tokio::test]
async fn test_sign_package_data_with_raw_key() {
let dal = unique_dal().await;
let signer = DbPackageSigner::new(dal);
let keypair = generate_signing_keypair();
let data = b"package binary content";
let sig_info = signer
.sign_package_data(data, &keypair.private_key, &keypair.public_key)
.unwrap();
assert!(!sig_info.package_hash.is_empty());
assert_eq!(sig_info.signature.len(), 64);
assert!(!sig_info.key_fingerprint.is_empty());
}
#[tokio::test]
async fn test_sign_package_with_raw_key_file() {
let dal = unique_dal().await;
let signer = DbPackageSigner::new(dal);
let keypair = generate_signing_keypair();
let temp_file = NamedTempFile::new().unwrap();
std::fs::write(temp_file.path(), b"file based content").unwrap();
let sig_info = signer
.sign_package_with_raw_key(
temp_file.path(),
&keypair.private_key,
&keypair.public_key,
)
.unwrap();
assert!(!sig_info.package_hash.is_empty());
assert_eq!(sig_info.signature.len(), 64);
}
#[tokio::test]
async fn test_store_and_find_signature() {
let dal = unique_dal().await;
let signer = DbPackageSigner::new(dal);
let keypair = generate_signing_keypair();
let data = b"storable package";
let sig_info = signer
.sign_package_data(data, &keypair.private_key, &keypair.public_key)
.unwrap();
let _sig_id = signer.store_signature(&sig_info).await.unwrap();
let found = signer.find_signature(&sig_info.package_hash).await.unwrap();
assert!(found.is_some());
let found = found.unwrap();
assert_eq!(found.package_hash, sig_info.package_hash);
assert_eq!(found.key_fingerprint, sig_info.key_fingerprint);
}
#[tokio::test]
async fn test_find_signature_not_found() {
let dal = unique_dal().await;
let signer = DbPackageSigner::new(dal);
let found = signer.find_signature("nonexistent_hash").await.unwrap();
assert!(found.is_none());
}
#[tokio::test]
async fn test_find_signatures_multiple() {
let dal = unique_dal().await;
let signer = DbPackageSigner::new(dal);
let kp1 = generate_signing_keypair();
let kp2 = generate_signing_keypair();
let data = b"multi-signed package";
let sig1 = signer
.sign_package_data(data, &kp1.private_key, &kp1.public_key)
.unwrap();
let sig2 = signer
.sign_package_data(data, &kp2.private_key, &kp2.public_key)
.unwrap();
signer.store_signature(&sig1).await.unwrap();
signer.store_signature(&sig2).await.unwrap();
let sigs = signer.find_signatures(&sig1.package_hash).await.unwrap();
assert_eq!(sigs.len(), 2);
}
#[tokio::test]
async fn test_sign_package_with_db_key() {
let dal = unique_dal().await;
let km = DbKeyManager::new(dal.clone());
let signer = DbPackageSigner::new(dal);
let org_id = UniversalUuid::new_v4();
let mk = master_key();
let key_info = km
.create_signing_key(org_id, "signer-key", &mk)
.await
.unwrap();
let temp_file = NamedTempFile::new().unwrap();
std::fs::write(temp_file.path(), b"db-key-signed package").unwrap();
let sig_info = signer
.sign_package_with_db_key(temp_file.path(), key_info.id, &mk, false)
.await
.unwrap();
assert!(!sig_info.package_hash.is_empty());
assert_eq!(sig_info.signature.len(), 64);
}
#[tokio::test]
async fn test_sign_package_with_db_key_and_store() {
let dal = unique_dal().await;
let km = DbKeyManager::new(dal.clone());
let signer = DbPackageSigner::new(dal);
let org_id = UniversalUuid::new_v4();
let mk = master_key();
let key_info = km
.create_signing_key(org_id, "store-key", &mk)
.await
.unwrap();
let temp_file = NamedTempFile::new().unwrap();
std::fs::write(temp_file.path(), b"stored signature content").unwrap();
let sig_info = signer
.sign_package_with_db_key(temp_file.path(), key_info.id, &mk, true)
.await
.unwrap();
let found = signer.find_signature(&sig_info.package_hash).await.unwrap();
assert!(found.is_some());
}
#[tokio::test]
async fn test_sign_package_with_db_key_revoked_fails() {
let dal = unique_dal().await;
let km = DbKeyManager::new(dal.clone());
let signer = DbPackageSigner::new(dal);
let org_id = UniversalUuid::new_v4();
let mk = master_key();
let key_info = km
.create_signing_key(org_id, "revoked-signer", &mk)
.await
.unwrap();
km.revoke_signing_key(key_info.id).await.unwrap();
let temp_file = NamedTempFile::new().unwrap();
std::fs::write(temp_file.path(), b"content").unwrap();
let result = signer
.sign_package_with_db_key(temp_file.path(), key_info.id, &mk, false)
.await;
assert!(matches!(result, Err(PackageSignError::KeyRevoked)));
}
#[tokio::test]
async fn test_sign_package_with_db_key_not_found() {
let dal = unique_dal().await;
let signer = DbPackageSigner::new(dal);
let temp_file = NamedTempFile::new().unwrap();
std::fs::write(temp_file.path(), b"content").unwrap();
let result = signer
.sign_package_with_db_key(
temp_file.path(),
UniversalUuid::new_v4(),
&[0u8; 32],
false,
)
.await;
assert!(matches!(result, Err(PackageSignError::KeyNotFound(_))));
}
#[tokio::test]
async fn test_verify_package_with_detached_signature() {
let dal = unique_dal().await;
let signer = DbPackageSigner::new(dal);
let keypair = generate_signing_keypair();
let temp_file = NamedTempFile::new().unwrap();
let content = b"detached-sig verification content";
std::fs::write(temp_file.path(), content).unwrap();
let sig_info = signer
.sign_package_with_raw_key(
temp_file.path(),
&keypair.private_key,
&keypair.public_key,
)
.unwrap();
let detached = DetachedSignature::from_signature_info(&sig_info);
let result = signer.verify_package_with_detached_signature(
temp_file.path(),
&detached,
&keypair.public_key,
);
assert!(result.is_ok());
}
#[tokio::test]
async fn test_verify_package_detached_tampered_fails() {
let dal = unique_dal().await;
let signer = DbPackageSigner::new(dal);
let keypair = generate_signing_keypair();
let temp_file = NamedTempFile::new().unwrap();
std::fs::write(temp_file.path(), b"original content").unwrap();
let sig_info = signer
.sign_package_with_raw_key(
temp_file.path(),
&keypair.private_key,
&keypair.public_key,
)
.unwrap();
let detached = DetachedSignature::from_signature_info(&sig_info);
std::fs::write(temp_file.path(), b"tampered content").unwrap();
let result = signer.verify_package_with_detached_signature(
temp_file.path(),
&detached,
&keypair.public_key,
);
assert!(matches!(
result,
Err(PackageSignError::VerificationFailed(_))
));
}
#[tokio::test]
async fn test_verify_package_detached_wrong_key_fails() {
let dal = unique_dal().await;
let signer = DbPackageSigner::new(dal);
let keypair1 = generate_signing_keypair();
let keypair2 = generate_signing_keypair();
let temp_file = NamedTempFile::new().unwrap();
std::fs::write(temp_file.path(), b"signed content").unwrap();
let sig_info = signer
.sign_package_with_raw_key(
temp_file.path(),
&keypair1.private_key,
&keypair1.public_key,
)
.unwrap();
let detached = DetachedSignature::from_signature_info(&sig_info);
let result = signer.verify_package_with_detached_signature(
temp_file.path(),
&detached,
&keypair2.public_key,
);
assert!(matches!(
result,
Err(PackageSignError::VerificationFailed(_))
));
}
#[tokio::test]
async fn test_verify_package_detached_wrong_algorithm() {
let dal = unique_dal().await;
let signer = DbPackageSigner::new(dal);
let keypair = generate_signing_keypair();
let temp_file = NamedTempFile::new().unwrap();
std::fs::write(temp_file.path(), b"content").unwrap();
let sig_info = signer
.sign_package_with_raw_key(
temp_file.path(),
&keypair.private_key,
&keypair.public_key,
)
.unwrap();
let mut detached = DetachedSignature::from_signature_info(&sig_info);
detached.algorithm = "rsa".to_string();
let result = signer.verify_package_with_detached_signature(
temp_file.path(),
&detached,
&keypair.public_key,
);
assert!(matches!(
result,
Err(PackageSignError::InvalidSignatureFile(_))
));
}
#[tokio::test]
async fn test_verify_package_trusted_key() {
let dal = unique_dal().await;
let km = DbKeyManager::new(dal.clone());
let signer = DbPackageSigner::new(dal);
let org_id = UniversalUuid::new_v4();
let mk = master_key();
let key_info = km
.create_signing_key(org_id, "verify-key", &mk)
.await
.unwrap();
let temp_file = NamedTempFile::new().unwrap();
std::fs::write(temp_file.path(), b"verifiable content").unwrap();
let sig_info = signer
.sign_package_with_db_key(temp_file.path(), key_info.id, &mk, true)
.await
.unwrap();
km.trust_public_key(org_id, &key_info.public_key, Some("self-trust"))
.await
.unwrap();
let result = signer.verify_package(temp_file.path(), org_id).await;
assert!(result.is_ok());
let verified = result.unwrap();
assert_eq!(verified.package_hash, sig_info.package_hash);
}
#[tokio::test]
async fn test_verify_package_no_signature_fails() {
let dal = unique_dal().await;
let signer = DbPackageSigner::new(dal);
let org_id = UniversalUuid::new_v4();
let temp_file = NamedTempFile::new().unwrap();
std::fs::write(temp_file.path(), b"unsigned content").unwrap();
let result = signer.verify_package(temp_file.path(), org_id).await;
assert!(matches!(
result,
Err(PackageSignError::SignatureNotFound(_))
));
}
}
}