use actr_hyper::{Hyper, HyperConfig, HyperError, MfrCertCache, RegistryTrust, WorkloadPackage};
use base64::Engine;
use ed25519_dalek::SigningKey;
use rand::rngs::OsRng;
use std::sync::Arc;
use tempfile::TempDir;
fn minimal_wasm() -> Vec<u8> {
b"\0asm\x01\x00\x00\x00".to_vec()
}
fn prod_config(dir: &TempDir, ais_endpoint: &str) -> HyperConfig {
HyperConfig::new(dir.path(), Arc::new(RegistryTrust::new(ais_endpoint)))
}
fn make_signed_package(
manufacturer: &str,
actr_name: &str,
version: &str,
signing_key: &SigningKey,
) -> Vec<u8> {
let wasm = minimal_wasm();
let key_id = actr_pack::compute_key_id(&signing_key.verifying_key().to_bytes());
let manifest = actr_pack::PackageManifest {
manufacturer: manufacturer.to_string(),
name: actr_name.to_string(),
version: version.to_string(),
binary: actr_pack::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: Some(key_id),
resources: vec![],
proto_files: vec![],
lock_file: None,
metadata: actr_pack::ManifestMetadata::default(),
};
let opts = actr_pack::PackOptions {
manifest,
binary_bytes: wasm,
resources: vec![],
proto_files: vec![],
lock_file: None,
signing_key: signing_key.clone(),
};
actr_pack::pack(&opts).unwrap()
}
fn verifying_key_response(verifying_key: &ed25519_dalek::VerifyingKey) -> String {
let key_b64 = base64::engine::general_purpose::STANDARD.encode(verifying_key.to_bytes());
format!(r#"{{"public_key":"{key_b64}"}}"#)
}
#[tokio::test]
async fn production_mode_fetches_mfr_key_and_verifies() {
let signing_key = SigningKey::generate(&mut OsRng);
let verifying_key = signing_key.verifying_key();
let key_id = actr_pack::compute_key_id(&verifying_key.to_bytes());
let mut server = mockito::Server::new_async().await;
let mock = server
.mock(
"GET",
format!("/mfr/acme/verifying_key?key_id={}", key_id).as_str(),
)
.with_status(200)
.with_header("content-type", "application/json")
.with_body(verifying_key_response(&verifying_key))
.expect(1) .create_async()
.await;
let package = make_signed_package("acme", "Sensor", "1.0.0", &signing_key);
let dir = TempDir::new().unwrap();
let hyper = Hyper::new(prod_config(&dir, &server.url())).await.unwrap();
let verified = hyper
.verify_package(&WorkloadPackage::new(package))
.await
.unwrap();
mock.assert_async().await;
assert_eq!(verified.manifest.manufacturer, "acme");
assert_eq!(verified.manifest.name, "Sensor");
assert_eq!(verified.manifest.version, "1.0.0");
}
#[tokio::test]
async fn production_mode_caches_mfr_key_on_second_verify() {
let signing_key = SigningKey::generate(&mut OsRng);
let verifying_key = signing_key.verifying_key();
let key_id = actr_pack::compute_key_id(&verifying_key.to_bytes());
let mut server = mockito::Server::new_async().await;
let mock = server
.mock(
"GET",
format!("/mfr/cached-mfr/verifying_key?key_id={}", key_id).as_str(),
)
.with_status(200)
.with_header("content-type", "application/json")
.with_body(verifying_key_response(&verifying_key))
.expect(1) .create_async()
.await;
let package = make_signed_package("cached-mfr", "App", "1.0.0", &signing_key);
let dir = TempDir::new().unwrap();
let hyper = Hyper::new(prod_config(&dir, &server.url())).await.unwrap();
hyper
.verify_package(&WorkloadPackage::new(package.clone()))
.await
.unwrap();
hyper
.verify_package(&WorkloadPackage::new(package))
.await
.unwrap();
mock.assert_async().await; }
#[tokio::test]
async fn production_mode_returns_untrusted_for_unknown_mfr() {
let signing_key = SigningKey::generate(&mut OsRng);
let key_id = actr_pack::compute_key_id(&signing_key.verifying_key().to_bytes());
let mut server = mockito::Server::new_async().await;
server
.mock(
"GET",
format!("/mfr/unknown-mfr/verifying_key?key_id={}", key_id).as_str(),
)
.with_status(404)
.create_async()
.await;
let package = make_signed_package("unknown-mfr", "App", "1.0.0", &signing_key);
let dir = TempDir::new().unwrap();
let hyper = Hyper::new(prod_config(&dir, &server.url())).await.unwrap();
let result = hyper.verify_package(&WorkloadPackage::new(package)).await;
assert!(
matches!(result, Err(HyperError::UntrustedManufacturer(_))),
"unknown MFR should return UntrustedManufacturer, got: {result:?}"
);
}
#[tokio::test]
async fn production_mode_rejects_wrong_cached_key() {
let real_signing_key = SigningKey::generate(&mut OsRng);
let wrong_key = SigningKey::generate(&mut OsRng);
let key_id = actr_pack::compute_key_id(&real_signing_key.verifying_key().to_bytes());
let mut server = mockito::Server::new_async().await;
server
.mock(
"GET",
format!("/mfr/mfr-x/verifying_key?key_id={}", key_id).as_str(),
)
.with_status(200)
.with_header("content-type", "application/json")
.with_body(verifying_key_response(&wrong_key.verifying_key()))
.create_async()
.await;
let package = make_signed_package("mfr-x", "X", "1.0.0", &real_signing_key);
let dir = TempDir::new().unwrap();
let hyper = Hyper::new(prod_config(&dir, &server.url())).await.unwrap();
let result = hyper.verify_package(&WorkloadPackage::new(package)).await;
assert!(
matches!(result, Err(HyperError::SignatureVerificationFailed(_))),
"wrong public key should return SignatureVerificationFailed, got: {result:?}"
);
}
#[tokio::test]
async fn production_mode_independent_caches_per_manufacturer() {
let key_a = SigningKey::generate(&mut OsRng);
let key_b = SigningKey::generate(&mut OsRng);
let key_id_a = actr_pack::compute_key_id(&key_a.verifying_key().to_bytes());
let key_id_b = actr_pack::compute_key_id(&key_b.verifying_key().to_bytes());
let mut server = mockito::Server::new_async().await;
server
.mock(
"GET",
format!("/mfr/mfr-a/verifying_key?key_id={}", key_id_a).as_str(),
)
.with_status(200)
.with_header("content-type", "application/json")
.with_body(verifying_key_response(&key_a.verifying_key()))
.create_async()
.await;
server
.mock(
"GET",
format!("/mfr/mfr-b/verifying_key?key_id={}", key_id_b).as_str(),
)
.with_status(200)
.with_header("content-type", "application/json")
.with_body(verifying_key_response(&key_b.verifying_key()))
.create_async()
.await;
let pkg_a = make_signed_package("mfr-a", "ActorA", "1.0.0", &key_a);
let pkg_b = make_signed_package("mfr-b", "ActorB", "1.0.0", &key_b);
let dir = TempDir::new().unwrap();
let hyper = Hyper::new(prod_config(&dir, &server.url())).await.unwrap();
let manifest_a = hyper
.verify_package(&WorkloadPackage::new(pkg_a))
.await
.unwrap();
let manifest_b = hyper
.verify_package(&WorkloadPackage::new(pkg_b))
.await
.unwrap();
assert_eq!(manifest_a.manifest.manufacturer, "mfr-a");
assert_eq!(manifest_b.manifest.manufacturer, "mfr-b");
}
#[tokio::test]
async fn cert_cache_get_from_cache_after_prefetch() {
use ed25519_dalek::VerifyingKey;
let signing_key = SigningKey::generate(&mut OsRng);
let verifying_key = signing_key.verifying_key();
let key_id = actr_pack::compute_key_id(&verifying_key.to_bytes());
let mut server = mockito::Server::new_async().await;
server
.mock(
"GET",
format!("/mfr/sync-mfr/verifying_key?key_id={}", key_id).as_str(),
)
.with_status(200)
.with_header("content-type", "application/json")
.with_body(verifying_key_response(&verifying_key))
.create_async()
.await;
let cache = MfrCertCache::new(server.url());
let before = cache.get_from_cache("sync-mfr", Some(&key_id));
assert!(before.is_none(), "cache should be empty before prefetch");
cache.get_or_fetch("sync-mfr", Some(&key_id)).await.unwrap();
let after: Option<VerifyingKey> = cache.get_from_cache("sync-mfr", Some(&key_id));
assert!(after.is_some(), "cache should hit after prefetch");
assert_eq!(
after.unwrap().to_bytes(),
verifying_key.to_bytes(),
"cached public key should match the signing key"
);
}
#[tokio::test]
async fn production_mode_no_http_for_unknown_format() {
let server = mockito::Server::new_async().await;
let dir = TempDir::new().unwrap();
let hyper = Hyper::new(prod_config(&dir, &server.url())).await.unwrap();
let result = hyper
.verify_package(&WorkloadPackage::new(b"this is not a package".to_vec()))
.await;
assert!(
matches!(result, Err(HyperError::InvalidManifest(_))),
"unknown format should return InvalidManifest, got: {result:?}"
);
}