//! Authoring contracts use tiny synthetic payloads; real link/install checks are opt-in. #![cfg(all( target_os = "linux", any(target_arch = "x86_64", target_arch = "aarch64") ))] use std::collections::BTreeMap; use std::fs; use std::os::unix::fs::{MetadataExt, PermissionsExt, symlink}; use std::path::{Path, PathBuf}; use dever_cli::toolchain::{BuildTarget, packaging, platform_identity, runtime_pack, sha256_file}; use ring::rand::SystemRandom; use ring::signature::{ED25519, Ed25519KeyPair, KeyPair, UnparsedPublicKey}; use serde_json::{Value, json}; #[path = "../dever-tests/tests/support/temp.rs"] mod temp; use temp::TemporaryDirectory; #[cfg(all(target_os = "linux", target_arch = "x86_64"))] #[path = "native_release/acceptance.rs"] mod acceptance; #[path = "native_release/ecosystems.rs"] mod ecosystems; #[cfg(target_os = "linux")] #[path = "native_release/bootstrap.rs"] mod bootstrap; #[path = "support/native_elf.rs"] mod native_elf; #[path = "support/native_objects.rs"] mod native_objects; #[path = "native_release/targets.rs"] mod targets; #[test] fn sandbox_bootstrap_requires_static_elf_and_complete_guard_libraries() { let loader = format!("lib/{}", dever_sandbox::loader_name()); let mut files = vec![ ("bin/bwrap".into(), native_elf::static_executable()), ( "bin/guard".into(), native_elf::elf(None, &["libc.so.6"], None), ), ( loader, native_elf::library(dever_sandbox::loader_name(), &[]), ), ( "lib/libc.so.6".into(), native_elf::library("libc.so.6", &[]), ), ]; dever_sandbox::validate_assets(&files).unwrap(); files[0].1 = native_elf::elf(None, &[], None); assert!( dever_sandbox::validate_assets(&files) .unwrap_err() .contains("static target ELF") ); files[0].1 = native_elf::library("bwrap", &["libc.so.6"]); assert!( dever_sandbox::validate_assets(&files) .unwrap_err() .contains("static target ELF") ); files[0].1 = native_elf::static_executable(); files.pop(); assert!( dever_sandbox::validate_assets(&files) .unwrap_err() .contains("sandbox dependency 'lib/libc.so.6' is missing") ); } struct Author { directory: TemporaryDirectory, root: PathBuf, settings: Value, public_key: Vec, } impl Author { fn new() -> Self { let directory = TemporaryDirectory::new(); let root = directory.path().join("author"); fs::create_dir_all(root.join("config")).unwrap(); fs::create_dir(root.join("inputs")).unwrap(); let key = Ed25519KeyPair::generate_pkcs8(&SystemRandom::new()).unwrap(); fs::write(root.join("signing.pk8"), key.as_ref()).unwrap(); fs::set_permissions(root.join("signing.pk8"), fs::Permissions::from_mode(0o600)).unwrap(); let public_key = Ed25519KeyPair::from_pkcs8(key.as_ref()) .unwrap() .public_key() .as_ref() .to_vec(); let input = |name: &str, bytes: &[u8]| { let source = format!("inputs/{name}"); fs::write(root.join(&source), bytes).unwrap(); json!({ "source": source, "sha256": sha256_file(&root.join(source)).unwrap() }) }; let core = input( "core", &native_elf::compiler(&["libLLVM.so.18.1", "libc.so.6"]), ); let mut llvm = input( "llvm", &native_elf::library("libLLVM.so.18.1", &["libc.so.6"]), ); llvm["path"] = json!("libLLVM.so.18.1"); let mut profiles = json!({}); for name in ["base", "sqlite", "postgres", "both"] { profiles[name] = input( name, &native_objects::archive(BuildTarget::host().unwrap(), name), ); } let link_input = |name: &str, bytes: &[u8]| { let mut value = input(name, bytes); value["path"] = json!(name); value }; let start = link_input( "start.o", &native_objects::object(BuildTarget::host().unwrap()), ); let end = link_input( "end.o", &native_objects::object(BuildTarget::host().unwrap()), ); let library = link_input("system.a", b"!\n"); let skill = link_input( "SKILL.md", b"---\nname: dever-language\ndescription: Develop Dever applications.\n---\n", ); let settings = json!({ "format": "dever-native-release-input-v1", "version": env!("CARGO_PKG_VERSION"), "target": format!("{}-unknown-linux-gnu", std::env::consts::ARCH), "signing_key": "signing.pk8", "core": core, "core_libraries": [llvm], "skill": [skill], "profiles": profiles, "start": [start], "libraries": [library], "end": [end], }); Self { directory, root, settings, public_key, } } fn save(&self) { fs::write( self.root.join("config/setting.json"), serde_json::to_vec(&self.settings).unwrap(), ) .unwrap(); } fn output(&self, name: &str) -> PathBuf { self.directory.path().join(name) } fn machine(&self) -> dever_cli::toolchain::Layout { let layout = dever_cli::toolchain::Layout::new(self.output("machine")); layout.initialize().unwrap(); let key_path = layout.state().join("trusted-release-key"); fs::write( &key_path, self.public_key .iter() .map(|byte| format!("{byte:02x}")) .collect::(), ) .unwrap(); fs::set_permissions(key_path, fs::Permissions::from_mode(0o644)).unwrap(); layout } fn reject(&self) -> String { self.save(); let output = self.output("rejected"); let error = packaging::create(&self.root, &output).unwrap_err(); assert!(!output.exists()); assert_eq!( fs::read_dir(self.directory.path()).unwrap().count(), 1, "staging leaked: {error}" ); assert!( !error.contains("signing.pk8"), "private key location leaked: {error}" ); error } } fn contents(directory: &Path) -> BTreeMap> { fn visit(root: &Path, directory: &Path, files: &mut BTreeMap>) { for entry in fs::read_dir(directory).unwrap() { let entry = entry.unwrap(); let path = entry.path(); if entry.file_type().unwrap().is_dir() { visit(root, &path, files); } else { files.insert( path.strip_prefix(root).unwrap().to_owned(), fs::read(path).unwrap(), ); } } } let mut files = BTreeMap::new(); visit(directory, directory, &mut files); files } fn assert_signed(author: &Author, release: &Path) { let encoded = fs::read_to_string(release.join("manifest.sig")).unwrap(); let (pairs, remainder) = encoded.trim().as_bytes().as_chunks::<2>(); assert!(remainder.is_empty()); let signature = pairs .iter() .map(|pair| u8::from_str_radix(std::str::from_utf8(pair).unwrap(), 16).unwrap()) .collect::>(); UnparsedPublicKey::new(&ED25519, &author.public_key) .verify( &fs::read(release.join("manifest.json")).unwrap(), &signature, ) .unwrap(); } #[test] fn release_skill_rejects_missing_entry_and_oversized_files() { let mut author = Author::new(); author.settings["skill"] = json!([]); author.save(); assert!(packaging::create(&author.root, &author.output("missing")).is_err()); assert!(!author.output("missing").exists()); let mut author = Author::new(); fs::write( author.root.join("inputs/SKILL.md"), vec![b'x'; 1024 * 1024 + 1], ) .unwrap(); author.settings["skill"][0]["sha256"] = json!(sha256_file(&author.root.join("inputs/SKILL.md")).unwrap()); author.save(); assert!( packaging::create(&author.root, &author.output("large")) .unwrap_err() .contains("limit") ); } #[test] fn identical_inputs_produce_identical_signed_independent_releases() { let author = Author::new(); author.save(); let first = author.output("first"); let second = author.output("second"); let manifest = packaging::create(&author.root, &first).unwrap(); assert_eq!(packaging::create(&author.root, &second).unwrap(), manifest); let before = contents(&first); assert_eq!(contents(&second), before); assert_eq!(before.len(), manifest.artifacts.len() + 2); assert!( manifest .artifacts .windows(2) .all(|pair| pair[0].path < pair[1].path) ); for artifact in &manifest.artifacts { assert_eq!( sha256_file(&first.join(&artifact.path)).unwrap(), artifact.sha256 ); assert_eq!(fs::metadata(first.join(&artifact.path)).unwrap().nlink(), 1); } assert_signed(&author, &first); let runtime = runtime_pack::validate( &first, env!("CARGO_PKG_VERSION"), BuildTarget::host().unwrap(), ) .unwrap(); assert_eq!(runtime.len() + 3, manifest.artifacts.len()); let metadata: Value = serde_json::from_slice( &before[&PathBuf::from(format!("runtime/{}/manifest.json", platform_identity()))], ) .unwrap(); for profile in ["base", "sqlite", "postgres", "both"] { assert_eq!( metadata["profiles"][profile]["runtime"], format!("archives/{profile}.a") ); } fs::write(author.root.join("inputs/base"), b"changed source").unwrap(); assert_eq!(contents(&first), before); assert_eq!(contents(&second), before); } #[test] fn compiler_libraries_must_form_a_closed_target_and_search_path() { let invalid_libraries = [ ( native_elf::library("libLLVM.so.18.1", &["libz.so.1"]), "not supplied", ), ( native_elf::library("different.so", &["libc.so.6"]), "SONAME", ), ( native_elf::elf(Some("libLLVM.so.18.1"), &[], Some("/usr/lib")), "search path", ), ( native_elf::elf(Some("libLLVM.so.18.1"), &[], Some("$ORIGIN/../../lib")), "search path", ), ( native_elf::library("libLLVM.so.18.1", &["/usr/lib/libz.so.1"]), "plain library", ), ]; for (bytes, diagnostic) in invalid_libraries { let mut author = Author::new(); fs::write(author.root.join("inputs/llvm"), bytes).unwrap(); author.settings["core_libraries"][0]["sha256"] = json!(sha256_file(&author.root.join("inputs/llvm")).unwrap()); assert!(author.reject().contains(diagnostic)); } let mut author = Author::new(); let mut bytes = native_elf::compiler(&["libLLVM.so.18.1"]); bytes[18..20].copy_from_slice(&0u16.to_le_bytes()); fs::write(author.root.join("inputs/core"), bytes).unwrap(); author.settings["core"]["sha256"] = json!(sha256_file(&author.root.join("inputs/core")).unwrap()); assert!(author.reject().contains("incompatible target")); let mut author = Author::new(); author.settings["core_libraries"] = json!([]); assert!(author.reject().contains("not supplied")); let mut author = Author::new(); let bytes = native_elf::elf(None, &["libLLVM.so.18.1"], Some("/usr/lib")); fs::write(author.root.join("inputs/core"), bytes).unwrap(); author.settings["core"]["sha256"] = json!(sha256_file(&author.root.join("inputs/core")).unwrap()); assert!(author.reject().contains("inherited RPATH")); } #[test] fn installation_rechecks_the_signed_native_compiler_closure_before_health_execution() { let author = Author::new(); author.save(); let layout = author.machine(); let release = layout.downloads().join(env!("CARGO_PKG_VERSION")); let mut manifest = packaging::create(&author.root, &release).unwrap(); manifest .artifacts .retain(|artifact| artifact.path != "lib/libLLVM.so.18.1"); fs::remove_file(release.join("lib/libLLVM.so.18.1")).unwrap(); let encoded = serde_json::to_vec(&manifest).unwrap(); let key = Ed25519KeyPair::from_pkcs8(&fs::read(author.root.join("signing.pk8")).unwrap()).unwrap(); fs::write(release.join("manifest.json"), &encoded).unwrap(); fs::write( release.join("manifest.sig"), key.sign(&encoded) .as_ref() .iter() .map(|byte| format!("{byte:02x}")) .collect::(), ) .unwrap(); let error = dever_cli::toolchain::MachineManager::new(layout.clone()) .install_requested(env!("CARGO_PKG_VERSION")) .unwrap_err(); assert!(error.contains("not supplied"), "{error}"); assert!(!layout.versions().join(env!("CARGO_PKG_VERSION")).exists()); } #[test] fn native_core_resolution_rejects_unsigned_loader_search_entries() { for entry in [ "lib/libc.so.6", "lib/glibc-hwcaps/x86-64-v3/libLLVM.so.18.1", ] { let author = Author::new(); author.save(); let layout = author.machine(); let version = dever_cli::toolchain::Version::parse(env!("CARGO_PKG_VERSION")).unwrap(); let release = layout.versions().join(version.as_str()); packaging::create(&author.root, &release).unwrap(); let extra = release.join(entry); fs::create_dir_all(extra.parent().unwrap()).unwrap(); fs::write(extra, b"unsigned loader input").unwrap(); let error = dever_cli::toolchain::MachineManager::new(layout) .resolve_core(&version) .unwrap_err(); assert!( error.contains("unsigned file, directory or link"), "{error}" ); } } #[test] fn rejects_invalid_configuration_and_rolls_back_partial_payloads() { let changes = [ ("/format", json!("unknown")), ("/target", json!("x86_64-pc-windows-msvc")), ("/version", json!("1.0")), ("/unexpected", json!(true)), ("/core/unexpected", json!(true)), ("/core/source", json!("../outside")), ("/core/source", json!("/etc/passwd")), ("/core/source", json!("inputs/./core")), ("/core/source", json!("inputs/missing")), ("/profiles/base/sha256", json!("0".repeat(64))), ("/profiles/base/sha256", json!("A".repeat(64))), ("/profiles/postgres", Value::Null), ("/start", json!([])), ("/libraries", json!([])), ("/end", json!([])), ("/end/0/path", json!("start.o")), ("/end/0/path", json!("archives/base.a")), ("/end/0/path", json!("manifest.json")), ("/end/0/path", json!("../../escape.o")), ("/end/0/path", json!("end.txt")), ]; for (pointer, value) in changes { let mut author = Author::new(); if let Some(slot) = author.settings.pointer_mut(pointer) { *slot = value; } else if pointer == "/unexpected" { author.settings["unexpected"] = value; } else { author.settings["core"]["unexpected"] = value; } author.reject(); } let mut author = Author::new(); author.settings["profiles"] .as_object_mut() .unwrap() .remove("sqlite"); author.reject(); for bytes in [b"!\n".as_slice(), b"linker script".as_slice()] { let mut author = Author::new(); fs::write(author.root.join("inputs/base"), bytes).unwrap(); author.settings["profiles"]["base"]["sha256"] = json!(sha256_file(&author.root.join("inputs/base")).unwrap()); assert!(author.reject().contains("self-contained archive")); } } #[test] fn private_key_and_symlink_inputs_never_enter_the_release() { let author = Author::new(); fs::set_permissions( author.root.join("signing.pk8"), fs::Permissions::from_mode(0o644), ) .unwrap(); assert!(author.reject().contains("permissions")); let mut author = Author::new(); symlink("signing.pk8", author.root.join("key-link")).unwrap(); author.settings["signing_key"] = json!("key-link"); author.reject(); let mut author = Author::new(); symlink("inputs/core", author.root.join("payload-link")).unwrap(); author.settings["core"]["source"] = json!("payload-link"); author.reject(); let mut author = Author::new(); symlink("inputs", author.root.join("directory-link")).unwrap(); author.settings["core"]["source"] = json!("directory-link/core"); author.reject(); let mut author = Author::new(); fs::copy( author.root.join("signing.pk8"), author.root.join("inputs/key-copy"), ) .unwrap(); author.settings["core"] = json!({ "source":"inputs/key-copy", "sha256":sha256_file(&author.root.join("inputs/key-copy")).unwrap() }); assert!(author.reject().contains("cannot be included")); } #[test] fn existing_outputs_are_preserved_and_concurrent_creation_has_one_winner() { let author = Author::new(); author.save(); let existing = author.output("existing"); fs::create_dir(&existing).unwrap(); fs::write(existing.join("keep"), b"original").unwrap(); let before = contents(&existing); assert!(packaging::create(&author.root, &existing).is_err()); assert_eq!(contents(&existing), before); let empty = author.output("empty"); fs::create_dir(&empty).unwrap(); assert!(packaging::create(&author.root, &empty).is_err()); assert!(empty.is_dir()); let link = author.output("link"); symlink(&existing, &link).unwrap(); assert!(packaging::create(&author.root, &link).is_err()); assert_eq!(contents(&existing), before); let output = author.output("concurrent"); let outcomes = std::thread::scope(|scope| { let first = scope.spawn(|| packaging::create(&author.root, &output)); let second = scope.spawn(|| packaging::create(&author.root, &output)); [first.join().unwrap(), second.join().unwrap()] }); assert_eq!(outcomes.iter().filter(|result| result.is_ok()).count(), 1); runtime_pack::validate( &output, env!("CARGO_PKG_VERSION"), BuildTarget::host().unwrap(), ) .unwrap(); assert_eq!(fs::read_dir(author.directory.path()).unwrap().count(), 5); } #[test] fn duplicate_fields_and_invalid_private_keys_fail_before_publication() { let author = Author::new(); author.save(); let config = author.root.join("config/setting.json"); let original = fs::read_to_string(&config).unwrap(); let duplicate = original.replacen('{', "{\"signing_key\":\"signing.pk8\",", 1); fs::write(&config, duplicate).unwrap(); let error = packaging::create(&author.root, &author.output("duplicate")).unwrap_err(); assert!(error.contains("invalid author configuration")); assert!(!error.contains("signing.pk8")); assert_eq!(fs::read_dir(author.directory.path()).unwrap().count(), 1); fs::write(author.root.join("signing.pk8"), b"invalid private key").unwrap(); assert!(author.reject().contains("PKCS#8")); }