455 lines
17 KiB
Rust
455 lines
17 KiB
Rust
//! Bootstrap package validation and recovery use only owned fixture directories.
|
|
use super::*;
|
|
use dever_cli::toolchain::bootstrap;
|
|
use sha2::{Digest, Sha256};
|
|
|
|
#[test]
|
|
fn bootstrap_service_recovery_orders_actions_around_file_restoration() {
|
|
use bootstrap::RecoveryAction::{Reload, Restart, Stop};
|
|
for active in [false, true] {
|
|
for loaded in [false, true] {
|
|
assert_eq!(
|
|
bootstrap::recovery_actions(false, active, loaded),
|
|
(vec![], vec![])
|
|
);
|
|
}
|
|
}
|
|
assert_eq!(
|
|
bootstrap::recovery_actions(true, false, false),
|
|
(vec![], vec![Reload])
|
|
);
|
|
assert_eq!(
|
|
bootstrap::recovery_actions(true, false, true),
|
|
(vec![Stop], vec![Reload])
|
|
);
|
|
assert_eq!(
|
|
bootstrap::recovery_actions(true, true, true),
|
|
(vec![Stop], vec![Reload, Restart])
|
|
);
|
|
assert_eq!(
|
|
bootstrap::recovery_actions(true, true, false),
|
|
(vec![], vec![Reload, Restart])
|
|
);
|
|
}
|
|
|
|
fn add_bootstrap(author: &mut Author) {
|
|
let input = |name: &str, bytes: &[u8]| {
|
|
let relative = format!("inputs/{name}");
|
|
fs::write(author.root.join(&relative), bytes).unwrap();
|
|
json!({"source":relative,"sha256":sha256_file(&author.root.join(&relative)).unwrap()})
|
|
};
|
|
let launcher = input(
|
|
"dever",
|
|
&native_elf::compiler(&["libgcc_s.so.1", "libc.so.6"]),
|
|
);
|
|
let daemon = input(
|
|
"deverd",
|
|
&native_elf::compiler(&["libgcc_s.so.1", "libc.so.6"]),
|
|
);
|
|
let mut gcc = input("gcc", &native_elf::library("libgcc_s.so.1", &["libc.so.6"]));
|
|
gcc["path"] = json!("libgcc_s.so.1");
|
|
author.settings["bootstrap"] = json!({"launcher":launcher,"daemon":daemon,"libraries":[gcc]});
|
|
}
|
|
|
|
#[test]
|
|
fn bootstrap_payload_has_a_separate_signed_complete_loader_closure() {
|
|
let mut author = Author::new();
|
|
add_bootstrap(&mut author);
|
|
author.save();
|
|
let release = author.output("complete");
|
|
let manifest = packaging::create(&author.root, &release).unwrap();
|
|
assert_signed(&author, &release);
|
|
for path in [
|
|
"bootstrap/dever",
|
|
"bootstrap/deverd",
|
|
"bootstrap/lib/libgcc_s.so.1",
|
|
"bootstrap/deverd.service",
|
|
] {
|
|
let artifact = manifest
|
|
.artifacts
|
|
.iter()
|
|
.find(|artifact| artifact.path == path)
|
|
.unwrap();
|
|
assert_eq!(artifact.sha256, sha256_file(&release.join(path)).unwrap());
|
|
}
|
|
let unit = fs::read_to_string(release.join("bootstrap/deverd.service")).unwrap();
|
|
assert!(unit.contains("ExecStart=/opt/dever/bin/deverd --root /opt/dever"));
|
|
assert!(unit.contains("RuntimeDirectory=dever"));
|
|
assert!(!unit.contains("Environment"));
|
|
assert!(!unit.contains("PATH"));
|
|
assert!(!release.join("bootstrap/dever-process").exists());
|
|
assert!(
|
|
!manifest
|
|
.artifacts
|
|
.iter()
|
|
.any(|artifact| artifact.path.contains("bwrap") || artifact.path.contains("apparmor"))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn bootstrap_authoring_rejects_missing_libraries_wrong_sonames_and_search_paths() {
|
|
let mut author = Author::new();
|
|
add_bootstrap(&mut author);
|
|
author.settings["bootstrap"]["libraries"] = json!([]);
|
|
assert!(author.reject().contains("libgcc_s.so.1"));
|
|
|
|
let mut author = Author::new();
|
|
add_bootstrap(&mut author);
|
|
fs::write(
|
|
author.root.join("inputs/gcc"),
|
|
native_elf::library("libwrong.so", &["libc.so.6"]),
|
|
)
|
|
.unwrap();
|
|
author.settings["bootstrap"]["libraries"][0]["sha256"] =
|
|
json!(sha256_file(&author.root.join("inputs/gcc")).unwrap());
|
|
assert!(author.reject().contains("SONAME"));
|
|
|
|
let mut author = Author::new();
|
|
add_bootstrap(&mut author);
|
|
fs::write(
|
|
author.root.join("inputs/dever"),
|
|
native_elf::elf(None, &["libgcc_s.so.1"], Some("/usr/lib")),
|
|
)
|
|
.unwrap();
|
|
author.settings["bootstrap"]["launcher"]["sha256"] =
|
|
json!(sha256_file(&author.root.join("inputs/dever")).unwrap());
|
|
assert!(author.reject().contains("RPATH"));
|
|
}
|
|
|
|
#[test]
|
|
fn signed_installed_versions_keep_their_original_bootstrap_service_template() {
|
|
use dever_cli::toolchain::{Layout, MachineManager, Version};
|
|
let mut author = Author::new();
|
|
add_bootstrap(&mut author);
|
|
author.save();
|
|
let machine = TemporaryDirectory::new();
|
|
let layout = Layout::new(machine.path().join("machine"));
|
|
layout.initialize().unwrap();
|
|
let version = Version::parse(env!("CARGO_PKG_VERSION")).unwrap();
|
|
let installed = layout.versions().join(version.as_str());
|
|
let mut manifest = packaging::create(&author.root, &installed).unwrap();
|
|
let service = installed.join("bootstrap/deverd.service");
|
|
let current = fs::read_to_string(&service).unwrap();
|
|
let old = current.replace("RestartSec=1", "RestartSec=2");
|
|
assert_ne!(current, old);
|
|
fs::write(&service, old).unwrap();
|
|
let artifact = manifest
|
|
.artifacts
|
|
.iter_mut()
|
|
.find(|artifact| artifact.path == "bootstrap/deverd.service")
|
|
.unwrap();
|
|
artifact.bytes = fs::metadata(&service).unwrap().len();
|
|
artifact.sha256 = sha256_file(&service).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(installed.join("manifest.json"), &encoded).unwrap();
|
|
fs::write(
|
|
installed.join("manifest.sig"),
|
|
key.sign(&encoded)
|
|
.as_ref()
|
|
.iter()
|
|
.map(|byte| format!("{byte:02x}"))
|
|
.collect::<String>(),
|
|
)
|
|
.unwrap();
|
|
fs::write(
|
|
layout.state().join("trusted-release-key"),
|
|
author
|
|
.public_key
|
|
.iter()
|
|
.map(|byte| format!("{byte:02x}"))
|
|
.collect::<String>(),
|
|
)
|
|
.unwrap();
|
|
let manager = MachineManager::new(layout);
|
|
assert_eq!(
|
|
manager.resolve_core(&version).unwrap(),
|
|
installed.join("dever-core")
|
|
);
|
|
fs::write(service, b"unsigned modification").unwrap();
|
|
assert!(
|
|
manager
|
|
.resolve_core(&version)
|
|
.unwrap_err()
|
|
.contains("integrity verification")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn historical_bootstrap_profiles_are_rejected_regardless_of_version_number() {
|
|
use dever_cli::toolchain::{Artifact, Layout, MachineManager, Version};
|
|
for release_version in ["0.0.1", "0.1.4", "0.1.6"] {
|
|
let mut author = Author::new();
|
|
author.settings["version"] = json!(release_version);
|
|
add_bootstrap(&mut author);
|
|
author.save();
|
|
let machine = TemporaryDirectory::new();
|
|
let layout = Layout::new(machine.path().join("machine"));
|
|
layout.initialize().unwrap();
|
|
let version = Version::parse(release_version).unwrap();
|
|
let installed = layout.versions().join(release_version);
|
|
let mut manifest = packaging::create(&author.root, &installed).unwrap();
|
|
let profile = installed.join("bootstrap/dever-sandbox");
|
|
fs::write(&profile, b"historical profile fixture\n").unwrap();
|
|
manifest.artifacts.push(Artifact {
|
|
path: "bootstrap/dever-sandbox".into(),
|
|
bytes: fs::metadata(&profile).unwrap().len(),
|
|
sha256: sha256_file(&profile).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(installed.join("manifest.json"), &encoded).unwrap();
|
|
fs::write(
|
|
installed.join("manifest.sig"),
|
|
key.sign(&encoded)
|
|
.as_ref()
|
|
.iter()
|
|
.map(|byte| format!("{byte:02x}"))
|
|
.collect::<String>(),
|
|
)
|
|
.unwrap();
|
|
fs::write(
|
|
layout.state().join("trusted-release-key"),
|
|
author
|
|
.public_key
|
|
.iter()
|
|
.map(|byte| format!("{byte:02x}"))
|
|
.collect::<String>(),
|
|
)
|
|
.unwrap();
|
|
let manager = MachineManager::new(layout);
|
|
assert!(
|
|
manager
|
|
.resolve_core(&version)
|
|
.unwrap_err()
|
|
.contains("unsupported artifact")
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "root-only installer trust rejection; never touches the actual machine installation"]
|
|
fn bootstrap_installer_rejects_untrusted_inputs_before_publication() {
|
|
assert_eq!(rustix::process::geteuid().as_raw(), 0);
|
|
assert!(
|
|
bootstrap::install(Path::new("."))
|
|
.unwrap_err()
|
|
.contains("absolute paths")
|
|
);
|
|
assert!(
|
|
bootstrap::recover(Path::new("."))
|
|
.unwrap_err()
|
|
.contains("absolute paths")
|
|
);
|
|
let mut author = Author::new();
|
|
add_bootstrap(&mut author);
|
|
author.save();
|
|
let package = author.output("release");
|
|
packaging::create(&author.root, &package).unwrap();
|
|
let installer = TemporaryDirectory::new();
|
|
let system = TemporaryDirectory::new();
|
|
let key = installer.path().join("trusted.pub");
|
|
fs::write(
|
|
&key,
|
|
author
|
|
.public_key
|
|
.iter()
|
|
.map(|byte| format!("{byte:02x}"))
|
|
.collect::<String>(),
|
|
)
|
|
.unwrap();
|
|
fs::create_dir(installer.path().join("config")).unwrap();
|
|
let save = |key: &Path, activate: bool| {
|
|
fs::write(installer.path().join("config/setting.json"), serde_json::to_vec(&json!({"release":package,"system_root":system.path(),"trusted_key":key,"activate_service":activate})).unwrap()).unwrap()
|
|
};
|
|
save(&key, true);
|
|
assert!(
|
|
bootstrap::install(installer.path())
|
|
.unwrap_err()
|
|
.contains("actual system root")
|
|
);
|
|
let inside = package.join("trusted.pub");
|
|
fs::copy(&key, &inside).unwrap();
|
|
save(&inside, false);
|
|
assert!(
|
|
bootstrap::install(installer.path())
|
|
.unwrap_err()
|
|
.contains("independently")
|
|
);
|
|
save(&key, false);
|
|
fs::set_permissions(&key, fs::Permissions::from_mode(0o666)).unwrap();
|
|
assert!(
|
|
bootstrap::install(installer.path())
|
|
.unwrap_err()
|
|
.contains("root-owned")
|
|
);
|
|
fs::set_permissions(&key, fs::Permissions::from_mode(0o644)).unwrap();
|
|
let signature = fs::read(package.join("manifest.sig")).unwrap();
|
|
fs::write(package.join("manifest.sig"), "00".repeat(64)).unwrap();
|
|
assert!(
|
|
bootstrap::install(installer.path())
|
|
.unwrap_err()
|
|
.contains("signature verification")
|
|
);
|
|
fs::write(package.join("manifest.sig"), signature).unwrap();
|
|
// The key is pinned before the intentionally non-executable synthetic core
|
|
// fails its health check. Empty/partial temporary files model interrupted
|
|
// writes without injecting product failpoints or touching real services.
|
|
let state = system.path().join("opt/dever/state");
|
|
let temporary_key = state.join(".trusted-release-key.next");
|
|
let pinned_key = state.join("trusted-release-key");
|
|
for partial in ["", "abc"] {
|
|
fs::write(&temporary_key, partial).unwrap();
|
|
let error = bootstrap::install(installer.path()).unwrap_err();
|
|
assert!(!error.contains("temporary release key"), "{error}");
|
|
assert_eq!(
|
|
fs::read_to_string(&pinned_key).unwrap().trim(),
|
|
fs::read_to_string(&key).unwrap()
|
|
);
|
|
assert!(!temporary_key.exists());
|
|
fs::remove_file(&pinned_key).unwrap();
|
|
}
|
|
fs::write(&pinned_key, "11".repeat(32)).unwrap();
|
|
assert!(
|
|
bootstrap::install(installer.path())
|
|
.unwrap_err()
|
|
.contains("pinned release key")
|
|
);
|
|
assert_eq!(fs::read_to_string(&pinned_key).unwrap(), "11".repeat(32));
|
|
fs::remove_file(&pinned_key).unwrap();
|
|
fs::write(package.join("bootstrap/lib/libgcc_s.so.1"), b"tampered").unwrap();
|
|
assert!(
|
|
bootstrap::install(installer.path())
|
|
.unwrap_err()
|
|
.contains("integrity verification")
|
|
);
|
|
fs::remove_file(package.join("bootstrap/lib/libgcc_s.so.1")).unwrap();
|
|
assert!(bootstrap::install(installer.path()).is_err());
|
|
assert!(!system.path().join("usr/local/bin/dever").exists());
|
|
assert!(
|
|
!system
|
|
.path()
|
|
.join("opt/dever/state/active-version")
|
|
.exists()
|
|
);
|
|
}
|
|
|
|
// Independent construction of a one-node journal identity lets recovery tests
|
|
// simulate interruption at publication boundaries without product failpoints.
|
|
fn node_hash(kind: &str, bytes: &[u8]) -> String {
|
|
let mut hash = Sha256::new();
|
|
hash.update(0u64.to_le_bytes());
|
|
hash.update(
|
|
if kind == "directory" {
|
|
0o755u32
|
|
} else {
|
|
0o644u32
|
|
}
|
|
.to_le_bytes(),
|
|
);
|
|
hash.update(kind.as_bytes());
|
|
if kind == "file" {
|
|
hash.update((bytes.len() as u64).to_le_bytes());
|
|
hash.update(bytes);
|
|
}
|
|
format!("{:x}", hash.finalize())
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "root-only crash-recovery fixture; all journal targets are below an owned image root"]
|
|
fn bootstrap_recovery_is_idempotent_and_rejects_unrecorded_targets() {
|
|
assert_eq!(rustix::process::geteuid().as_raw(), 0);
|
|
let system = TemporaryDirectory::new();
|
|
let root = system.path();
|
|
let layout = dever_cli::toolchain::Layout::new(root.join("opt/dever"));
|
|
layout.initialize().unwrap();
|
|
for directory in [
|
|
"usr/local/bin",
|
|
"etc/systemd/system/multi-user.target.wants",
|
|
] {
|
|
fs::create_dir_all(root.join(directory)).unwrap();
|
|
}
|
|
let service = root.join("etc/systemd/system/deverd.service");
|
|
fs::write(&service, b"new service").unwrap();
|
|
let entry = root.join("usr/local/bin/dever");
|
|
symlink("/opt/dever/bin/dever", &entry).unwrap();
|
|
let enable_stage = root
|
|
.join("etc/systemd/system/multi-user.target.wants/.dever-bootstrap-deverd.service.next");
|
|
symlink("../deverd.service", &enable_stage).unwrap();
|
|
let active_stage = layout.state().join(".dever-bootstrap-active-version.next");
|
|
fs::write(&active_stage, b"new active").unwrap();
|
|
let sentinel = root.join("etc/systemd/system/unrelated.service");
|
|
fs::write(&sentinel, b"keep").unwrap();
|
|
let link_hash =
|
|
|target: &str| format!("{:x}", Sha256::digest(format!("link\0{target}").as_bytes()));
|
|
let journal = json!({"format":"dever-bootstrap-transaction-v1","phase":"prepared","service_was_active":null,"service_start_requested":false,"changes":[
|
|
{"target":"bin","before":node_hash("directory",b""),"after":node_hash("directory",b"")},
|
|
{"target":"service","before":null,"after":node_hash("file",b"new service")},
|
|
{"target":"entry","before":null,"after":link_hash("/opt/dever/bin/dever")},
|
|
{"target":"enable","before":null,"after":link_hash("../deverd.service")},
|
|
{"target":"active","before":null,"after":node_hash("file",b"new active")}
|
|
]});
|
|
let path = layout.state().join("bootstrap-install.json");
|
|
fs::write(&path, serde_json::to_vec(&journal).unwrap()).unwrap();
|
|
let mut invalid = journal.clone();
|
|
invalid["changes"][0]["target"] = json!("../../unrelated");
|
|
fs::write(&path, serde_json::to_vec(&invalid).unwrap()).unwrap();
|
|
assert!(bootstrap::recover(root).is_err());
|
|
assert_eq!(fs::read(&sentinel).unwrap(), b"keep");
|
|
fs::write(&path, serde_json::to_vec(&journal).unwrap()).unwrap();
|
|
fs::write(&service, b"not this transaction").unwrap();
|
|
assert!(
|
|
bootstrap::recover(root)
|
|
.unwrap_err()
|
|
.contains("outside this transaction")
|
|
);
|
|
assert_eq!(fs::read(&service).unwrap(), b"not this transaction");
|
|
fs::write(&service, b"new service").unwrap();
|
|
bootstrap::recover(root).unwrap();
|
|
bootstrap::recover(root).unwrap();
|
|
assert!(!service.exists());
|
|
assert!(fs::symlink_metadata(&entry).is_err());
|
|
assert!(!active_stage.exists());
|
|
assert!(fs::symlink_metadata(&enable_stage).is_err());
|
|
assert_eq!(fs::read(&sentinel).unwrap(), b"keep");
|
|
assert!(!path.exists());
|
|
|
|
// A committed publication cleans only its old staging copies; it must not
|
|
// undo the new active version, including after a failed cleanup retry.
|
|
fs::write(&service, b"new service").unwrap();
|
|
symlink("/opt/dever/bin/dever", &entry).unwrap();
|
|
symlink(
|
|
"../deverd.service",
|
|
root.join("etc/systemd/system/multi-user.target.wants/deverd.service"),
|
|
)
|
|
.unwrap();
|
|
fs::write(layout.state().join("active-version"), b"new active").unwrap();
|
|
fs::write(&active_stage, b"old active").unwrap();
|
|
let mut committed = journal;
|
|
committed["phase"] = json!("committed");
|
|
fs::write(&path, serde_json::to_vec(&committed).unwrap()).unwrap();
|
|
fs::write(&service, b"unexpected modification").unwrap();
|
|
assert!(
|
|
bootstrap::recover(root)
|
|
.unwrap_err()
|
|
.contains("committed bootstrap destination changed")
|
|
);
|
|
assert!(path.exists());
|
|
assert_eq!(
|
|
fs::read(layout.state().join("active-version")).unwrap(),
|
|
b"new active"
|
|
);
|
|
fs::write(&service, b"new service").unwrap();
|
|
bootstrap::recover(root).unwrap();
|
|
bootstrap::recover(root).unwrap();
|
|
assert_eq!(
|
|
fs::read(layout.state().join("active-version")).unwrap(),
|
|
b"new active"
|
|
);
|
|
assert!(!active_stage.exists());
|
|
assert!(!path.exists());
|
|
assert_eq!(fs::read(sentinel).unwrap(), b"keep");
|
|
}
|