use std::fs; use std::path::PathBuf; use clawmates_bundler::{assemble, generate_keypair, verify, BundleError}; struct Fixture { root: PathBuf, public_key: PathBuf, bundle: PathBuf, } fn fixture() -> Fixture { let root = std::env::temp_dir().join(format!("tc-bundle-{}", uuid::Uuid::now_v7())); fs::create_dir_all(&root).unwrap(); let private_key = root.join("release.key"); let public_key = root.join("release.pub"); generate_keypair(&private_key, &public_key).unwrap(); fs::write(root.join("server.tar"), b"pretend image bytes").unwrap(); fs::write(root.join("compose.yml"), b"services: {}").unwrap(); fs::write(root.join("0001_init.sql"), b"CREATE TABLE t ();").unwrap(); let bundle = root.join("bundle"); assemble( &bundle, "1.2.3", &[ (root.join("server.tar"), "images/server.tar".into()), ( root.join("compose.yml"), "compose/docker-compose.yml".into(), ), ( root.join("0001_init.sql"), "migrations/0001_init.sql".into(), ), ], &private_key, ) .unwrap(); Fixture { root, public_key, bundle, } } #[test] fn assembled_bundles_verify_offline() { let fx = fixture(); // 3 artifacts + manifest.json. assert_eq!(verify(&fx.bundle, &fx.public_key).unwrap(), 4); let manifest: serde_json::Value = serde_json::from_str(&fs::read_to_string(fx.bundle.join("manifest.json")).unwrap()) .unwrap(); assert_eq!(manifest["version"], "1.2.3"); assert_eq!(manifest["artifacts"].as_array().unwrap().len(), 3); } #[test] fn a_tampered_artifact_is_detected() { let fx = fixture(); fs::write(fx.bundle.join("images/server.tar"), b"EVIL image bytes").unwrap(); let err = verify(&fx.bundle, &fx.public_key).unwrap_err(); assert!(matches!(err, BundleError::ChecksumMismatch(p) if p == "images/server.tar")); } #[test] fn a_tampered_checksum_list_fails_the_signature() { let fx = fixture(); // Attacker edits both the artifact AND its checksum line — the // signature over checksums.txt catches it. let listing = fs::read_to_string(fx.bundle.join("checksums.txt")).unwrap(); fs::write( fx.bundle.join("checksums.txt"), listing.replace("images/server.tar", "images/sneaky.tar"), ) .unwrap(); let err = verify(&fx.bundle, &fx.public_key).unwrap_err(); assert!(matches!(err, BundleError::BadSignature)); } #[test] fn the_wrong_public_key_is_refused() { let fx = fixture(); let other_private = fx.root.join("other.key"); let other_public = fx.root.join("other.pub"); generate_keypair(&other_private, &other_public).unwrap(); let err = verify(&fx.bundle, &other_public).unwrap_err(); assert!(matches!(err, BundleError::BadSignature)); } #[test] fn a_missing_artifact_is_reported() { let fx = fixture(); fs::remove_file(fx.bundle.join("migrations/0001_init.sql")).unwrap(); let err = verify(&fx.bundle, &fx.public_key).unwrap_err(); assert!(matches!(err, BundleError::Missing(p) if p.contains("0001_init.sql"))); } #[test] fn the_public_key_rederives_from_the_signing_key() { let fx = fixture(); let derived = fx.root.join("derived.pub"); clawmates_bundler::derive_public_key(&fx.root.join("release.key"), &derived).unwrap(); assert_eq!( fs::read_to_string(&derived).unwrap(), fs::read_to_string(&fx.public_key).unwrap() ); // And it verifies the bundle, same as the original. assert_eq!(verify(&fx.bundle, &derived).unwrap(), 4); }