use std::path::Path;
use std::process::Command;
use plecto_host::DevSigner;
use plecto_host::test_support::filter_hello_component;
fn run(args: &[&str], dir: &Path) -> std::process::Output {
Command::new(env!("CARGO_BIN_EXE_plecto"))
.args(args)
.current_dir(dir)
.output()
.unwrap()
}
fn write_key_pair(dir: &Path) {
let (signer, private_pem) = DevSigner::generate().unwrap();
std::fs::write(dir.join("key.pem"), private_pem.as_bytes()).unwrap();
std::fs::write(dir.join("trust.pem"), signer.public_key_pem()).unwrap();
}
fn write_component(dir: &Path) {
std::fs::write(dir.join("filter.wasm"), filter_hello_component()).unwrap();
}
fn manifest_pinning(digest: &str) -> String {
format!(
r#"[trust]
keys = ["trust.pem"]
[[filter]]
id = "hello"
source = "layout"
digest = "{digest}"
isolation = "trusted"
[[upstream]]
name = "app"
addresses = ["127.0.0.1:9000"]
[upstream.health]
path = "/healthz"
[[route]]
filters = ["hello"]
upstream = "app"
[route.match]
path_prefix = "/"
"#
)
}
fn package(dir: &Path) -> std::process::Output {
run(
&[
"package",
"filter.wasm",
"--key",
"key.pem",
"--out",
"layout",
],
dir,
)
}
#[test]
fn package_prints_only_the_pinned_digest_and_the_layout_passes_resolve() {
let dir = tempfile::tempdir().unwrap();
write_key_pair(dir.path());
write_component(dir.path());
let out = package(dir.path());
assert!(
out.status.success(),
"package exits 0, stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
let digest = stdout.trim_end();
assert!(
digest.starts_with("sha256:") && digest.len() == "sha256:".len() + 64,
"stdout is exactly one sha256:<hex> digest, got: {stdout:?}"
);
assert_eq!(
stdout.lines().count(),
1,
"no extra stdout lines: {stdout:?}"
);
std::fs::write(dir.path().join("plecto.toml"), manifest_pinning(digest)).unwrap();
let resolve = run(&["validate", "--resolve", "plecto.toml"], dir.path());
assert!(
resolve.status.success(),
"validate --resolve accepts the packaged layout, stderr: {}",
String::from_utf8_lossy(&resolve.stderr)
);
}
#[test]
fn package_gates_on_conformance_and_writes_nothing_for_junk() {
let dir = tempfile::tempdir().unwrap();
write_key_pair(dir.path());
std::fs::write(dir.path().join("filter.wasm"), b"not a component").unwrap();
let out = package(dir.path());
assert!(!out.status.success(), "junk input is rejected");
assert!(
out.stdout.is_empty(),
"no digest on stdout for a rejected component: {:?}",
String::from_utf8_lossy(&out.stdout)
);
assert!(
!dir.path().join("layout").exists(),
"a non-conformant component never produces a layout"
);
}
#[test]
fn package_accepts_a_supplied_bound_sbom() {
let dir = tempfile::tempdir().unwrap();
write_key_pair(dir.path());
write_component(dir.path());
let component = filter_hello_component();
let default_statement = plecto_host::bound_sbom(&component);
let mut statement: serde_json::Value = serde_json::from_slice(&default_statement).unwrap();
statement["predicate"] = serde_json::json!({"supplier": "moka-1"});
std::fs::write(
dir.path().join("sbom.json"),
serde_json::to_vec(&statement).unwrap(),
)
.unwrap();
let out = run(
&[
"package",
"filter.wasm",
"--key",
"key.pem",
"--out",
"layout",
"--sbom",
"sbom.json",
],
dir.path(),
);
assert!(
out.status.success(),
"package --sbom exits 0, stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let digest = String::from_utf8_lossy(&out.stdout).trim_end().to_string();
std::fs::write(dir.path().join("plecto.toml"), manifest_pinning(&digest)).unwrap();
let resolve = run(&["validate", "--resolve", "plecto.toml"], dir.path());
assert!(
resolve.status.success(),
"resolve accepts the supplier SBOM, stderr: {}",
String::from_utf8_lossy(&resolve.stderr)
);
}
#[test]
fn resolve_fails_closed_on_an_untrusted_signature() {
let dir = tempfile::tempdir().unwrap();
write_key_pair(dir.path());
write_component(dir.path());
let out = package(dir.path());
assert!(out.status.success());
let digest = String::from_utf8_lossy(&out.stdout).trim_end().to_string();
let (stranger, _) = DevSigner::generate().unwrap();
std::fs::write(dir.path().join("trust.pem"), stranger.public_key_pem()).unwrap();
std::fs::write(dir.path().join("plecto.toml"), manifest_pinning(&digest)).unwrap();
let stat = run(&["validate", "plecto.toml"], dir.path());
assert!(
stat.status.success(),
"static validate does not resolve artifacts"
);
let resolve = run(&["validate", "--resolve", "plecto.toml"], dir.path());
assert!(
!resolve.status.success(),
"--resolve rejects a layout no trusted key signed"
);
}
#[test]
fn resolve_fails_closed_on_a_tampered_blob() {
let dir = tempfile::tempdir().unwrap();
write_key_pair(dir.path());
write_component(dir.path());
let out = package(dir.path());
assert!(out.status.success());
let digest = String::from_utf8_lossy(&out.stdout).trim_end().to_string();
std::fs::write(dir.path().join("plecto.toml"), manifest_pinning(&digest)).unwrap();
let blobs = dir.path().join("layout/blobs/sha256");
let component_blob = std::fs::read_dir(&blobs)
.unwrap()
.map(|e| e.unwrap().path())
.max_by_key(|p| std::fs::metadata(p).unwrap().len())
.unwrap();
let mut bytes = std::fs::read(&component_blob).unwrap();
bytes[0] ^= 0xFF;
std::fs::write(&component_blob, bytes).unwrap();
let resolve = run(&["validate", "--resolve", "plecto.toml"], dir.path());
assert!(
!resolve.status.success(),
"--resolve rejects a blob whose content diverges from its digest"
);
}