use std::io::Cursor;
use ed25519_dalek::{Signature, VerifyingKey};
use crate::error::PackError;
use crate::manifest::PackageManifest;
use crate::util::{read_zip_entry, sha256_hex};
#[derive(Debug, Clone)]
pub struct VerifiedPackage {
pub manifest: PackageManifest,
pub manifest_raw: Vec<u8>,
pub sig_raw: Vec<u8>,
}
pub fn verify(actr_bytes: &[u8], pubkey: &VerifyingKey) -> Result<VerifiedPackage, PackError> {
let cursor = Cursor::new(actr_bytes);
let mut archive = zip::ZipArchive::new(cursor)?;
let sig_raw =
read_zip_entry(&mut archive, "manifest.sig").map_err(|_| PackError::SignatureNotFound)?;
if sig_raw.len() != 64 {
return Err(PackError::SignatureVerificationFailed(format!(
"manifest.sig must be exactly 64 bytes, got {}",
sig_raw.len()
)));
}
let sig_arr: [u8; 64] = sig_raw.clone().try_into().unwrap();
let signature = Signature::from_bytes(&sig_arr);
let manifest_bytes =
read_zip_entry(&mut archive, "manifest.toml").map_err(|_| PackError::ManifestNotFound)?;
pubkey
.verify_strict(&manifest_bytes, &signature)
.map_err(|e| {
PackError::SignatureVerificationFailed(format!("Ed25519 verification failed: {e}"))
})?;
tracing::debug!("package signature verified");
let manifest_str = std::str::from_utf8(&manifest_bytes)
.map_err(|e| PackError::ManifestParseError(format!("manifest is not valid UTF-8: {e}")))?;
let manifest = PackageManifest::from_toml(manifest_str)?;
let binary_bytes = read_zip_entry(&mut archive, &manifest.binary.path)
.map_err(|_| PackError::BinaryNotFound(manifest.binary.path.clone()))?;
let computed_hash = sha256_hex(&binary_bytes);
if computed_hash != manifest.binary.hash {
tracing::warn!(
expected = %manifest.binary.hash,
computed = %computed_hash,
path = %manifest.binary.path,
"binary hash mismatch"
);
return Err(PackError::BinaryHashMismatch {
path: manifest.binary.path.clone(),
});
}
for resource in &manifest.resources {
let res_bytes = read_zip_entry(&mut archive, &resource.path)
.map_err(|_| PackError::BinaryNotFound(resource.path.clone()))?;
let computed = sha256_hex(&res_bytes);
if computed != resource.hash {
tracing::warn!(
expected = %resource.hash,
computed = %computed,
path = %resource.path,
"resource hash mismatch"
);
return Err(PackError::ResourceHashMismatch {
path: resource.path.clone(),
});
}
}
for proto in &manifest.proto_files {
let proto_bytes = read_zip_entry(&mut archive, &proto.path)
.map_err(|_| PackError::BinaryNotFound(proto.path.clone()))?;
let computed = sha256_hex(&proto_bytes);
if computed != proto.hash {
tracing::warn!(
expected = %proto.hash,
computed = %computed,
path = %proto.path,
"proto file hash mismatch"
);
return Err(PackError::ProtoHashMismatch {
path: proto.path.clone(),
});
}
}
if let Some(lock_file) = &manifest.lock_file {
let lock_bytes = read_zip_entry(&mut archive, &lock_file.path)
.map_err(|_| PackError::BinaryNotFound(lock_file.path.clone()))?;
let computed = sha256_hex(&lock_bytes);
if computed != lock_file.hash {
tracing::warn!(
expected = %lock_file.hash,
computed = %computed,
path = %lock_file.path,
"manifest lock hash mismatch"
);
return Err(PackError::LockFileHashMismatch {
path: lock_file.path.clone(),
});
}
}
tracing::info!(
actr_type = %manifest.actr_type_str(),
"package verification passed"
);
Ok(VerifiedPackage {
manifest,
manifest_raw: manifest_bytes,
sig_raw,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::manifest::{BinaryEntry, ManifestMetadata, PackageManifest, ResourceEntry};
use crate::pack::{PackOptions, pack};
use ed25519_dalek::SigningKey;
use rand::rngs::OsRng;
use std::io::Write;
fn test_manifest() -> PackageManifest {
PackageManifest {
manufacturer: "test-mfr".to_string(),
name: "TestActor".to_string(),
version: "1.0.0".to_string(),
binary: BinaryEntry {
path: "bin/actor.wasm".to_string(),
target: "wasm32-wasip1".to_string(),
hash: String::new(),
size: None,
kind: None,
},
signature_algorithm: "ed25519".to_string(),
signing_key_id: None,
resources: vec![],
proto_files: vec![],
lock_file: None,
metadata: ManifestMetadata::default(),
}
}
fn make_package(
signing_key: &SigningKey,
binary: &[u8],
resources: Vec<(String, Vec<u8>)>,
) -> Vec<u8> {
let mut manifest = test_manifest();
manifest.resources = resources
.iter()
.map(|(path, _)| ResourceEntry {
path: path.clone(),
hash: String::new(),
})
.collect();
let opts = PackOptions {
manifest,
binary_bytes: binary.to_vec(),
resources,
proto_files: vec![],
signing_key: signing_key.clone(),
lock_file: None,
};
pack(&opts).unwrap()
}
#[test]
fn roundtrip_succeeds() {
let key = SigningKey::generate(&mut OsRng);
let pkg = make_package(&key, b"wasm bytes", vec![]);
let result = verify(&pkg, &key.verifying_key()).unwrap();
assert_eq!(result.manifest.manufacturer, "test-mfr");
assert_eq!(result.manifest.name, "TestActor");
assert_eq!(result.sig_raw.len(), 64);
assert!(!result.manifest_raw.is_empty());
}
#[test]
fn tampered_binary_detected() {
let key = SigningKey::generate(&mut OsRng);
let pkg_bytes = make_package(&key, b"original", vec![]);
let mut tampered = pkg_bytes.clone();
if let Some(pos) = tampered.windows(8).position(|w| w == b"original") {
tampered[pos] ^= 0xFF;
}
let result = verify(&tampered, &key.verifying_key());
assert!(
result.is_err(),
"tampered package should fail: {:?}",
result
);
}
#[test]
fn wrong_key_rejected() {
let key1 = SigningKey::generate(&mut OsRng);
let key2 = SigningKey::generate(&mut OsRng);
let pkg = make_package(&key1, b"wasm", vec![]);
let result = verify(&pkg, &key2.verifying_key());
assert!(matches!(
result,
Err(PackError::SignatureVerificationFailed(_))
));
}
#[test]
fn missing_signature_detected() {
let cursor = std::io::Cursor::new(Vec::new());
let mut zip = zip::ZipWriter::new(cursor);
let opts = zip::write::SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Stored);
zip.start_file("manifest.toml", opts).unwrap();
zip.write_all(b"[fake]").unwrap();
let data = zip.finish().unwrap().into_inner();
let key = SigningKey::generate(&mut OsRng);
let result = verify(&data, &key.verifying_key());
assert!(matches!(result, Err(PackError::SignatureNotFound)));
}
#[test]
fn resource_hash_mismatch_detected() {
let key = SigningKey::generate(&mut OsRng);
let pkg = make_package(
&key,
b"wasm",
vec![(
"config/settings.toml".to_string(),
b"key = \"value\"".to_vec(),
)],
);
let mut tampered = pkg.clone();
if let Some(pos) = tampered.windows(5).position(|w| w == b"value") {
tampered[pos] ^= 0xFF;
}
let result = verify(&tampered, &key.verifying_key());
assert!(result.is_err());
}
#[test]
fn with_resources_roundtrip() {
let key = SigningKey::generate(&mut OsRng);
let resources = vec![
("config/a.toml".to_string(), b"data_a".to_vec()),
("config/b.toml".to_string(), b"data_b".to_vec()),
];
let pkg = make_package(&key, b"wasm", resources);
let result = verify(&pkg, &key.verifying_key()).unwrap();
assert_eq!(result.manifest.resources.len(), 2);
}
fn make_package_with_protos(
signing_key: &SigningKey,
binary: &[u8],
protos: Vec<(String, Vec<u8>)>,
) -> Vec<u8> {
use crate::manifest::ProtoFileEntry;
let manifest = test_manifest();
let proto_entries: Vec<ProtoFileEntry> = protos
.iter()
.map(|(name, _)| ProtoFileEntry {
name: name.clone(),
path: format!("proto/{name}"),
hash: String::new(),
})
.collect();
let mut m = manifest;
m.proto_files = proto_entries;
let opts = PackOptions {
manifest: m,
binary_bytes: binary.to_vec(),
resources: vec![],
proto_files: protos,
signing_key: signing_key.clone(),
lock_file: None,
};
pack(&opts).unwrap()
}
#[test]
fn with_proto_files_roundtrip() {
let key = SigningKey::generate(&mut OsRng);
let protos = vec![
(
"echo.proto".to_string(),
b"syntax = \"proto3\";\nservice Echo {}".to_vec(),
),
(
"common.proto".to_string(),
b"syntax = \"proto3\";\nmessage Empty {}".to_vec(),
),
];
let pkg = make_package_with_protos(&key, b"wasm", protos);
let result = verify(&pkg, &key.verifying_key()).unwrap();
assert_eq!(result.manifest.proto_files.len(), 2);
assert_eq!(result.manifest.proto_files[0].name, "echo.proto");
assert_eq!(result.manifest.proto_files[1].name, "common.proto");
}
#[test]
fn tampered_proto_detected() {
let key = SigningKey::generate(&mut OsRng);
let protos = vec![(
"echo.proto".to_string(),
b"syntax = \"proto3\";\nservice Echo {}".to_vec(),
)];
let pkg = make_package_with_protos(&key, b"wasm", protos);
let mut tampered = pkg.clone();
let needle = b"Echo";
if let Some(pos) = tampered.windows(needle.len()).position(|w| w == needle) {
tampered[pos] ^= 0xFF;
}
let result = verify(&tampered, &key.verifying_key());
assert!(result.is_err(), "tampered proto should fail: {:?}", result);
}
}