services_hardening_native_release.rs (15903B)
1 #![forbid(unsafe_code)] 2 3 use std::collections::BTreeSet; 4 5 use serde_json::json; 6 7 const CONTRACT: &str = include_str!("../contracts/services_hardening/native_release.v1.json"); 8 const ACTIVE_CONTRACT: &str = 9 include_str!("../contracts/services_hardening/native_release.v2.json"); 10 const MANIFEST: &str = include_str!("../Cargo.toml"); 11 const LOCK: &str = include_str!("../Cargo.lock"); 12 const FLAKE: &str = include_str!("../flake.nix"); 13 const FLAKE_LOCK: &str = include_str!("../flake.lock"); 14 const SOURCE_LOCK: &str = include_str!("../radroots.service.source-lock.v3.toml"); 15 const CARGO_CONFIG: &str = include_str!("../.cargo/config.toml"); 16 const SYSTEMD_UNIT: &str = include_str!("../packaging/systemd/rhi@.service"); 17 const RELEASE_ACCEPTANCE: &str = include_str!("../scripts/release-acceptance.sh"); 18 const XTASK_MANIFEST: &str = include_str!("../tools/xtask/Cargo.toml"); 19 20 const LIB_REVISION: &str = "055096853fca95e15d0f813d33a14aca13be3881"; 21 const LIB_REPOSITORY: &str = "https://github.com/radrootslabs/lib"; 22 23 #[test] 24 fn native_release_contract_and_manifest_metadata_are_exact() { 25 let contract: serde_json::Value = serde_json::from_str(CONTRACT).expect("release contract"); 26 assert_eq!( 27 contract, 28 json!({ 29 "schema": "radroots.rhi.native-release", 30 "schema_version": 1, 31 "contract_version": 1, 32 "service": "rhi", 33 "package": { 34 "name": "rhi", 35 "binary": "rhi", 36 "version": "0.1.0", 37 "repository": "https://github.com/radrootslabs/rhi", 38 "publish_to_crates_io": false 39 }, 40 "generator": { 41 "command": "cargo xtask native-release", 42 "modes": ["check", "write"], 43 "required_arguments": [ 44 "mode", "target", "binary", "output", "source_date_epoch" 45 ], 46 "source_date_epoch_range": "1..=4294967295", 47 "clean_exact_head": true, 48 "target_binary_validation": "executable_elf64_little_endian_exact_machine", 49 "canonical_json": "compact_utf8_json_with_one_final_lf", 50 "deterministic_archives": true, 51 "output_directory_mode": "0755", 52 "output_file_mode": "0644", 53 "durability": "sync_files_then_output_directory_then_parent" 54 }, 55 "toolchain": { 56 "rust_version": "1.97.1", 57 "edition": "2024", 58 "resolver": "3", 59 "host_feature_profile": "service-host" 60 }, 61 "release_profile": { 62 "lto": "thin", 63 "codegen_units": 1, 64 "overflow_checks": true, 65 "strip": "symbols", 66 "panic": "unwind" 67 }, 68 "source_lock": { 69 "filename": "radroots.service.source-lock.v2.toml", 70 "schema": "radroots.service.source-lock.v2", 71 "lib_repository": LIB_REPOSITORY, 72 "architecture": "radroots.crates.release.v2", 73 "nix_material": "absent" 74 }, 75 "contract_versions": { 76 "config": 1, 77 "state": 11, 78 "admin": 1, 79 "status": 1, 80 "provider": 1 81 }, 82 "native_targets": [ 83 { "target": "aarch64-unknown-linux-gnu", "posture": "target" }, 84 { "target": "x86_64-unknown-linux-gnu", "posture": "target" } 85 ], 86 "output_inventory": [ 87 "LICENSE", 88 "SHA256SUMS", 89 "THIRD-PARTY-NOTICES.txt", 90 "artifact-manifest.v1.json", 91 "binary.tar.gz", 92 "config.example.toml", 93 "config.schema.json", 94 "provenance-input.v1.json", 95 "radroots.service.source-lock.v2.toml", 96 "sbom.cdx.json", 97 "service-source.tar.gz", 98 "systemd.service" 99 ], 100 "signing_inputs": [ 101 "SHA256SUMS", 102 "artifact-manifest.v1.json", 103 "provenance-input.v1.json" 104 ], 105 "provenance_posture": "deterministic_unsigned_slsa_v1_input_external_keys_only", 106 "sbom_format": "cyclonedx_json_1_5_locked_cargo_graph", 107 "sbom_component_identity": "domain_separated_sha256_of_framed_name_version_source_checksum", 108 "protected_material_scan": "tracked_source_binary_copied_and_generated_material_fixed_patterns", 109 "source_archive": "locked_offline_cargo_build_with_vendored_dependencies", 110 "checksum_format": "sha256_lower_hex_two_spaces_path_lf_sorted_by_path", 111 "protected_material_included": false, 112 "maximums": { 113 "text_input_bytes": 1048576, 114 "generated_document_bytes": 16777216, 115 "cargo_metadata_bytes": 33554432, 116 "binary_bytes": 536870912, 117 "source_archive_bytes": 1073741824, 118 "packages": 8192, 119 "tracked_files": 4096 120 }, 121 "deferred_through_rcld_rshr_170": [ 122 "nix_evaluation", 123 "nix_build", 124 "nixos_module_qualification", 125 "oci_artifact" 126 ], 127 "forbidden": [ 128 "nix_input", 129 "nixos_module_output", 130 "oci_input", 131 "oci_output", 132 "protected_material", 133 "parent_owned_human_docs", 134 "private_harness", 135 "local_or_path_lib_dependency", 136 "floating_or_branch_lib_dependency", 137 "mixed_lib_revision", 138 "crates_io_publication", 139 "signing", 140 "tagging", 141 "release_publication", 142 "deployment" 143 ] 144 }) 145 ); 146 147 let manifest: toml::Value = toml::from_str(MANIFEST).expect("Cargo manifest"); 148 let package = manifest["package"].as_table().expect("package"); 149 assert_eq!( 150 package["repository"].as_str(), 151 Some("https://github.com/radrootslabs/rhi") 152 ); 153 assert_eq!(package["readme"].as_str(), Some("README")); 154 assert_eq!(package["publish"].as_bool(), Some(false)); 155 assert_eq!( 156 manifest["workspace"]["members"], 157 toml::Value::Array(vec![ 158 toml::Value::String(".".to_owned()), 159 toml::Value::String("tools/xtask".to_owned()) 160 ]) 161 ); 162 assert_eq!( 163 manifest["workspace"]["metadata"]["radroots"]["service_release"], 164 toml::Value::Table(toml::toml! { 165 service = "rhi" 166 service_package = "rhi" 167 binary_name = "rhi" 168 version = "0.1.0" 169 }) 170 ); 171 assert_eq!( 172 manifest["workspace"]["metadata"]["radroots"]["service_source_lock"]["nix_material"] 173 .as_str(), 174 Some("qualified") 175 ); 176 let source_lock: toml::Value = toml::from_str(SOURCE_LOCK).expect("source lock"); 177 assert_eq!( 178 contract["contract_versions"]["state"].as_u64(), 179 source_lock["contract_versions"]["state"] 180 .as_integer() 181 .and_then(|value| u64::try_from(value).ok()) 182 ); 183 assert_eq!( 184 contract["contract_versions"]["state"].as_u64(), 185 manifest["workspace"]["metadata"]["radroots"]["service_source_lock"] 186 ["state_contract_version"] 187 .as_integer() 188 .and_then(|value| u64::try_from(value).ok()) 189 ); 190 assert_eq!( 191 manifest["profile"]["release"], 192 toml::Value::Table(toml::toml! { 193 lto = "thin" 194 codegen-units = 1 195 overflow-checks = true 196 strip = "symbols" 197 panic = "unwind" 198 }) 199 ); 200 assert_eq!( 201 CARGO_CONFIG, 202 "[alias]\nxtask = \"run --locked -p rhi_xtask --\"\n" 203 ); 204 for required in [ 205 "ExecStart=/usr/bin/rhi --profile service-host --instance %i run", 206 "ConfigurationDirectory=radroots/services/rhi/%i", 207 "StateDirectory=radroots/services/rhi/%i", 208 "CacheDirectory=radroots/services/rhi/%i", 209 "LogsDirectory=radroots/services/rhi/%i", 210 "RuntimeDirectory=radroots/services/rhi/%i", 211 "UMask=0077", 212 "NoNewPrivileges=yes", 213 "ProtectSystem=strict", 214 "CapabilityBoundingSet=", 215 ] { 216 assert!(SYSTEMD_UNIT.contains(required), "missing `{required}`"); 217 } 218 } 219 220 #[test] 221 fn every_radroots_dependency_is_exactly_source_locked() { 222 let manifest: toml::Value = toml::from_str(MANIFEST).expect("Cargo manifest"); 223 let dependencies = manifest["dependencies"].as_table().expect("dependencies"); 224 let radroots = dependencies 225 .iter() 226 .filter(|(name, _)| name.starts_with("radroots_")) 227 .collect::<Vec<_>>(); 228 assert_eq!(radroots.len(), 12); 229 for (name, dependency) in radroots { 230 let dependency = dependency.as_table().expect("detailed dependency"); 231 assert_eq!( 232 dependency.get("git").and_then(toml::Value::as_str), 233 Some(LIB_REPOSITORY), 234 "{name}" 235 ); 236 assert_eq!( 237 dependency.get("rev").and_then(toml::Value::as_str), 238 Some(LIB_REVISION), 239 "{name}" 240 ); 241 assert_eq!( 242 dependency.get("version").and_then(toml::Value::as_str), 243 Some("=0.1.0-alpha"), 244 "{name}" 245 ); 246 for forbidden in ["path", "branch", "tag"] { 247 assert!( 248 !dependency.contains_key(forbidden), 249 "{name} contains `{forbidden}`" 250 ); 251 } 252 } 253 assert!(!MANIFEST.contains("[patch.")); 254 255 let sources = LOCK 256 .lines() 257 .filter_map(|line| line.strip_prefix("source = \"git+")) 258 .filter_map(|line| line.strip_suffix('"')) 259 .filter(|source| source.contains("radrootslabs/lib")) 260 .collect::<BTreeSet<_>>(); 261 assert_eq!(sources.len(), 1); 262 let source = sources.into_iter().next().expect("Lib source"); 263 assert!(source.contains(&format!("?rev={LIB_REVISION}#{LIB_REVISION}"))); 264 265 for required in [ 266 "url = \"github:radrootslabs/lib/055096853fca95e15d0f813d33a14aca13be3881\";", 267 "systems = lib.lib.supportedSystems;", 268 "nixosModules.default", 269 ] { 270 assert!( 271 FLAKE.contains(required), 272 "flake source is missing `{required}`" 273 ); 274 } 275 let flake_lock: serde_json::Value = 276 serde_json::from_str(FLAKE_LOCK).expect("flake source lock"); 277 assert_eq!(flake_lock["version"], 7); 278 assert_eq!(flake_lock["root"], "root"); 279 assert_eq!(flake_lock["nodes"]["root"]["inputs"]["lib"], "lib"); 280 assert_eq!(flake_lock["nodes"]["lib"]["locked"]["rev"], LIB_REVISION); 281 assert_eq!(flake_lock["nodes"]["lib"]["original"]["rev"], LIB_REVISION); 282 } 283 284 #[test] 285 fn native_release_surfaces_remain_external_and_nix_qualified() { 286 let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")); 287 for required in [ 288 ".cargo/config.toml", 289 "packaging/systemd/rhi@.service", 290 "radroots.service.source-lock.v3.toml", 291 "scripts/release-acceptance.sh", 292 "tools/xtask/Cargo.toml", 293 "tools/xtask/src/main.rs", 294 "contracts/services_hardening/native_release.v1.json", 295 "contracts/services_hardening/native_release.v2.json", 296 "contracts/release/rhi-artifact-contract.v3.json", 297 "flake.nix", 298 "flake.lock", 299 ] { 300 assert!(root.join(required).is_file(), "missing `{required}`"); 301 } 302 assert!(!root.join("radroots.service.source-lock.v2.toml").exists()); 303 for forbidden in [ 304 ".github", 305 ".act", 306 "target", 307 "result", 308 "artifacts", 309 "dist", 310 "sbom.cdx.json", 311 "provenance-input.v1.json", 312 "oci-image.tar.gz", 313 ] { 314 assert!( 315 !root.join(forbidden).exists(), 316 "forbidden generated or deferred surface `{forbidden}` exists" 317 ); 318 } 319 assert!(!CONTRACT.contains("qualified")); 320 assert!(!CONTRACT.contains("production_ready")); 321 assert!(!CONTRACT.contains("oci-image")); 322 assert!(!CONTRACT.contains("nixos-module")); 323 let active: serde_json::Value = 324 serde_json::from_str(ACTIVE_CONTRACT).expect("active release contract"); 325 assert_eq!(active["schema_version"], 2); 326 assert_eq!(active["contract_version"], 2); 327 assert_eq!(active["predecessor"]["filename"], "native_release.v1.json"); 328 assert_eq!( 329 active["source_lock"]["filename"], 330 "radroots.service.source-lock.v3.toml" 331 ); 332 assert_eq!(active["source_lock"]["nix_material"], "qualified"); 333 assert_eq!(active["nix_outputs"]["bundled_sqlite"], true); 334 assert_eq!(active["nix_outputs"]["native_linkage_count"], 1); 335 } 336 337 #[test] 338 fn release_acceptance_is_standalone_and_has_no_deferred_toolchain() { 339 for required in [ 340 "cargo fmt --all --check", 341 "cargo metadata --locked --format-version 1 --no-deps", 342 "cargo check --locked --all-targets --no-default-features", 343 "cargo check --locked --all-targets --no-default-features --features service-host", 344 "cargo clippy --locked --all-targets -- -D warnings", 345 "cargo test --locked", 346 "cargo test --locked -p rhi_xtask", 347 "git diff --check", 348 ] { 349 assert!( 350 RELEASE_ACCEPTANCE.contains(required), 351 "release acceptance is missing `{required}`" 352 ); 353 } 354 for forbidden in [ 355 "nix ", 356 "nix-", 357 "docker ", 358 "podman ", 359 "cosign ", 360 "gh release", 361 ] { 362 assert!( 363 !RELEASE_ACCEPTANCE.contains(forbidden), 364 "release acceptance contains deferred or unauthorized command `{forbidden}`" 365 ); 366 } 367 } 368 369 #[test] 370 fn release_tool_dependency_and_file_mode_boundaries_are_exact() { 371 let manifest: toml::Value = toml::from_str(XTASK_MANIFEST).expect("xtask manifest"); 372 assert_eq!(manifest["package"]["name"].as_str(), Some("rhi_xtask")); 373 assert_eq!(manifest["package"]["publish"].as_bool(), Some(false)); 374 let dependencies = manifest["dependencies"] 375 .as_table() 376 .expect("xtask dependencies") 377 .keys() 378 .map(String::as_str) 379 .collect::<BTreeSet<_>>(); 380 assert_eq!( 381 dependencies, 382 BTreeSet::from([ 383 "flate2", 384 "hex", 385 "serde", 386 "serde_json", 387 "sha2", 388 "tar", 389 "tempfile", 390 "toml", 391 ]) 392 ); 393 assert_eq!( 394 manifest["target"]["cfg(unix)"]["dependencies"] 395 .as_table() 396 .expect("Unix dependencies") 397 .keys() 398 .map(String::as_str) 399 .collect::<BTreeSet<_>>(), 400 BTreeSet::from(["rustix"]) 401 ); 402 for forbidden in [ 403 "sqlx", "rusqlite", "reqwest", "tokio", "\nnix =", "nix::", "oci", "cosign", "path =", 404 "git =", 405 ] { 406 assert!( 407 !XTASK_MANIFEST.contains(forbidden), 408 "release tool contains unauthorized dependency or authority `{forbidden}`" 409 ); 410 } 411 412 #[cfg(unix)] 413 { 414 use std::os::unix::fs::PermissionsExt as _; 415 416 let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")); 417 assert_eq!( 418 std::fs::metadata(root.join("scripts/release-acceptance.sh")) 419 .expect("release script metadata") 420 .permissions() 421 .mode() 422 & 0o777, 423 0o755 424 ); 425 assert_eq!( 426 std::fs::metadata(root.join("packaging/systemd/rhi@.service")) 427 .expect("systemd unit metadata") 428 .permissions() 429 .mode() 430 & 0o777, 431 0o644 432 ); 433 } 434 }