lib.rs (40444B)
1 #![forbid(unsafe_code)] 2 3 pub mod error; 4 pub mod model; 5 pub mod resolve; 6 pub mod service; 7 mod service_artifact; 8 9 pub use error::RadrootsRuntimeDistributionError; 10 pub use model::{ 11 ArchiveFormat, ArtifactAdapter, ChannelSet, DistributionFamily, 12 RadrootsRuntimeDistributionContract, RuntimeDistributionEntry, TargetSet, TargetSpec, 13 }; 14 pub use resolve::{ 15 RUNTIME_DISTRIBUTION_CONTRACT_MAX_UTF8_BYTES, RUNTIME_DISTRIBUTION_SCHEMA, 16 RUNTIME_DISTRIBUTION_SCHEMA_VERSION, RadrootsRuntimeDistributionResolver, 17 ResolvedRuntimeArtifact, ResolvedServiceArtifact, ResolvedServiceTarget, 18 RuntimeArtifactRequest, ServiceArtifactRequest, ServiceTargetRequest, 19 }; 20 pub use service::{ 21 HardenedServiceTarget, HardenedServiceTargets, ServiceAdminBasePath, ServiceAdminTransport, 22 ServiceConfigurationFormat, ServiceInstanceSupport, ServiceOperationsSurface, 23 ServiceRunStatePolicy, ServiceStateInitialization, ServiceStatusSurface, ServiceSupportPosture, 24 ServiceTier1Target, 25 }; 26 pub use service_artifact::{ 27 HardenedServiceArtifact, HardenedServiceArtifacts, ServiceArtifactChannel, 28 ServiceArtifactSha256, 29 }; 30 31 #[cfg(test)] 32 mod tests { 33 use radroots_runtime_paths::ServiceId; 34 use toml::Value; 35 36 use super::{ 37 HardenedServiceTarget, RUNTIME_DISTRIBUTION_CONTRACT_MAX_UTF8_BYTES, 38 RUNTIME_DISTRIBUTION_SCHEMA, RadrootsRuntimeDistributionContract, 39 RadrootsRuntimeDistributionError, RadrootsRuntimeDistributionResolver, 40 RuntimeArtifactRequest, RuntimeDistributionEntry, ServiceAdminBasePath, 41 ServiceAdminTransport, ServiceArtifactRequest, ServiceArtifactSha256, 42 ServiceConfigurationFormat, ServiceInstanceSupport, ServiceOperationsSurface, 43 ServiceRunStatePolicy, ServiceStateInitialization, ServiceStatusSurface, 44 ServiceSupportPosture, ServiceTargetRequest, ServiceTier1Target, 45 }; 46 47 const HARDENED_SERVICE_CONTRACT: &str = 48 include_str!("../tests/fixtures/hardened_service_targets.v1.toml"); 49 50 const CONTRACT: &str = r#" 51 schema = "radroots-runtime-distribution" 52 schema_version = 1 53 owner_doc = "docs/execution/rcl/radroots-modular-runtime-management-bootstrap-rcl.md" 54 runtime_registry = "registry.toml" 55 56 [family] 57 id = "radroots_runtime-family" 58 canonical_installer_engine = "single_runtime_selected" 59 human_install_facade = "delivery_publication_only" 60 tooling_consumption = "shared_distribution_library" 61 independent_runtime_versions = true 62 version_resolution = "runtime_scoped_channel_latest" 63 artifact_verification_required = true 64 65 [channels] 66 active = ["stable"] 67 defined = ["stable", "candidate", "nightly"] 68 69 [artifact_adapters.rust_binary_archive] 70 kind = "binary_archive" 71 supported_archive_formats = ["tar.gz", "zip"] 72 layout = "single_binary_plus_supporting_files" 73 74 [artifact_adapters.desktop_bundle] 75 kind = "desktop_bundle" 76 supported_archive_formats = ["tar.gz", "zip", "dmg"] 77 layout = "host_native_bundle" 78 79 [artifact_adapters.mobile_store_package] 80 kind = "mobile_store_package" 81 supported_archive_formats = [] 82 layout = "platform_store_managed" 83 84 [artifact_adapters.mojo_workspace_archive] 85 kind = "workspace_archive" 86 supported_archive_formats = ["tar.gz"] 87 layout = "workspace_tree" 88 89 [archive_formats.tar_gz] 90 extension = ".tar.gz" 91 platforms = ["linux", "macos"] 92 93 [archive_formats.zip] 94 extension = ".zip" 95 platforms = ["windows"] 96 97 [archive_formats.dmg] 98 extension = ".dmg" 99 platforms = ["macos"] 100 101 [target_sets.server_default] 102 targets = [ 103 "x86_64-unknown-linux-gnu", 104 "aarch64-unknown-linux-gnu", 105 "x86_64-apple-darwin", 106 "aarch64-apple-darwin", 107 ] 108 109 [target_sets.cli_default] 110 targets = [ 111 "x86_64-unknown-linux-gnu", 112 "aarch64-unknown-linux-gnu", 113 "x86_64-apple-darwin", 114 "aarch64-apple-darwin", 115 ] 116 117 [target_sets.desktop_default] 118 targets = [ 119 "x86_64-apple-darwin", 120 "aarch64-apple-darwin", 121 ] 122 123 [target_sets.mojo_workspace_default] 124 targets = [ 125 "osx-arm64", 126 "linux-64", 127 ] 128 129 [targets.x86_64-unknown-linux-gnu] 130 os = "linux" 131 arch = "amd64" 132 archive_format = "tar.gz" 133 134 [targets.aarch64-unknown-linux-gnu] 135 os = "linux" 136 arch = "arm64" 137 archive_format = "tar.gz" 138 139 [targets.x86_64-apple-darwin] 140 os = "macos" 141 arch = "amd64" 142 archive_format = "tar.gz" 143 144 [targets.aarch64-apple-darwin] 145 os = "macos" 146 arch = "arm64" 147 archive_format = "tar.gz" 148 149 [targets.osx-arm64] 150 os = "macos" 151 arch = "arm64" 152 archive_format = "tar.gz" 153 154 [targets.linux-64] 155 os = "linux" 156 arch = "amd64" 157 archive_format = "tar.gz" 158 159 [[runtime]] 160 id = "cli" 161 distribution_state = "active" 162 release_unit = "cli" 163 package_name = "radroots_cli" 164 binary_name = "radroots" 165 artifact_adapter = "rust_binary_archive" 166 target_set = "cli_default" 167 default_channel = "stable" 168 human_installable = true 169 170 [[runtime]] 171 id = "radrootsd" 172 distribution_state = "active" 173 release_unit = "radrootsd" 174 package_name = "radrootsd" 175 binary_name = "radrootsd" 176 artifact_adapter = "rust_binary_archive" 177 target_set = "server_default" 178 default_channel = "stable" 179 human_installable = true 180 181 [[runtime]] 182 id = "fixture-desktop-bundle" 183 distribution_state = "defined" 184 release_unit = "fixture-desktop-bundle" 185 package_name = "fixture_desktop_bundle" 186 binary_name = "fixture_desktop_bundle" 187 artifact_adapter = "desktop_bundle" 188 target_set = "desktop_default" 189 default_channel = "stable" 190 human_installable = true 191 192 [[runtime]] 193 id = "fixture-mobile-package" 194 distribution_state = "external_platform_managed" 195 release_unit = "fixture-mobile-package" 196 package_name = "fixture_mobile_package" 197 artifact_adapter = "mobile_store_package" 198 default_channel = "stable" 199 human_installable = false 200 201 [[runtime]] 202 id = "hyf" 203 distribution_state = "bootstrap_only" 204 release_unit = "hyf" 205 package_name = "hyf" 206 binary_name = "hyf" 207 artifact_adapter = "mojo_workspace_archive" 208 target_set = "mojo_workspace_default" 209 default_channel = "stable" 210 human_installable = false 211 212 [service_targets.myc] 213 service_id = "myc" 214 instance_support = "multiple" 215 config_format = "toml" 216 state_initialization = "explicit" 217 run_state_policy = "existing_only" 218 admin_transport = "http11_over_unix_domain_socket" 219 admin_base_path = "/v1" 220 admin_contract_version = 1 221 status_surface = "local_admin_service_status_v1" 222 operations_surface = "cached_livez_readyz_metrics" 223 support_posture = "target" 224 tier_1_targets = ["x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu"] 225 226 [service_targets.rhi] 227 service_id = "rhi" 228 instance_support = "multiple" 229 config_format = "toml" 230 state_initialization = "explicit" 231 run_state_policy = "existing_only" 232 admin_transport = "http11_over_unix_domain_socket" 233 admin_base_path = "/v1" 234 admin_contract_version = 1 235 status_surface = "local_admin_service_status_v1" 236 operations_surface = "cached_livez_readyz_metrics" 237 support_posture = "target" 238 tier_1_targets = ["x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu"] 239 240 [service_artifacts.myc] 241 service_id = "myc" 242 service_revision = "77b381648ed1e586efb696888beb05b9215c69cf" 243 release_contract = "contracts/services_hardening/native_release.v2.json" 244 release_contract_sha256 = "4b3ba5789fac6aa219e84e1e5c002cf8230b72f95fd6d95a6419d2fdf2915f83" 245 source_lock_sha256 = "f5ebb390a480830d51d502facc623bd1b10eda27b12dad9f3dbb6a1f1f949217" 246 package_name = "myc" 247 binary_name = "myc" 248 version = "0.1.0" 249 channel = "stable" 250 binary_archive_name = "binary.tar.gz" 251 artifact_manifest_name = "artifact-manifest.v1.json" 252 checksums_name = "SHA256SUMS" 253 output_inventory = ["LICENSE", "SHA256SUMS", "THIRD-PARTY-NOTICES.txt", "artifact-manifest.v1.json", "binary.tar.gz", "config.example.toml", "config.schema.json", "provenance-input.v1.json", "radroots.service.source-lock.v2.toml", "sbom.cdx.json", "service-source.tar.gz", "systemd.service"] 254 tier_1_targets = ["x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu"] 255 256 [service_artifacts.rhi] 257 service_id = "rhi" 258 service_revision = "07aa6ea988da5372654bb3d1ee183ac099a77cae" 259 release_contract = "contracts/services_hardening/native_release.v1.json" 260 release_contract_sha256 = "06a973176b4b8c11dad13000604576527df829dd0bbe2f501158662f75e70b94" 261 source_lock_sha256 = "3cc8bfac0d98730937754abae2ccfe20e40d0a9bbdefe02ebd94264c20f0d0ff" 262 package_name = "rhi" 263 binary_name = "rhi" 264 version = "0.1.0" 265 channel = "stable" 266 binary_archive_name = "binary.tar.gz" 267 artifact_manifest_name = "artifact-manifest.v1.json" 268 checksums_name = "SHA256SUMS" 269 output_inventory = ["LICENSE", "SHA256SUMS", "THIRD-PARTY-NOTICES.txt", "artifact-manifest.v1.json", "binary.tar.gz", "config.example.toml", "config.schema.json", "provenance-input.v1.json", "radroots.service.source-lock.v2.toml", "sbom.cdx.json", "service-source.tar.gz", "systemd.service"] 270 tier_1_targets = ["x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu"] 271 "#; 272 273 fn contract_value() -> Value { 274 toml::from_str(CONTRACT).expect("parse contract value") 275 } 276 277 fn resolver_from_value(value: Value) -> RadrootsRuntimeDistributionResolver { 278 let raw = toml::to_string(&value).expect("serialize contract"); 279 RadrootsRuntimeDistributionResolver::parse_str(&raw).expect("parse resolver") 280 } 281 282 fn resolve_error( 283 resolver: &RadrootsRuntimeDistributionResolver, 284 request: RuntimeArtifactRequest<'_>, 285 ) -> RadrootsRuntimeDistributionError { 286 resolver 287 .resolve_artifact(&request) 288 .expect_err("request should fail") 289 } 290 291 #[test] 292 fn parse_str_accepts_the_expected_schema() { 293 let resolver = 294 RadrootsRuntimeDistributionResolver::parse_str(CONTRACT).expect("parse contract"); 295 296 assert_eq!(resolver.contract().schema, RUNTIME_DISTRIBUTION_SCHEMA); 297 assert_eq!(resolver.contract().runtime.len(), 5); 298 } 299 300 #[test] 301 fn parse_str_rejects_invalid_toml() { 302 let err = RadrootsRuntimeDistributionResolver::parse_str("schema = [") 303 .expect_err("invalid toml should fail"); 304 assert_eq!(err, RadrootsRuntimeDistributionError::Parse); 305 } 306 307 #[test] 308 fn parse_str_caps_the_complete_document_before_toml_parsing() { 309 let mut exact = CONTRACT.to_owned(); 310 exact.push('#'); 311 exact.extend(std::iter::repeat_n( 312 'x', 313 RUNTIME_DISTRIBUTION_CONTRACT_MAX_UTF8_BYTES - exact.len(), 314 )); 315 assert_eq!(exact.len(), RUNTIME_DISTRIBUTION_CONTRACT_MAX_UTF8_BYTES); 316 RadrootsRuntimeDistributionResolver::parse_str(&exact) 317 .expect("exact maximum contract remains admissible"); 318 319 exact.push('x'); 320 assert_eq!( 321 RadrootsRuntimeDistributionResolver::parse_str(&exact) 322 .expect_err("maximum plus one must fail"), 323 RadrootsRuntimeDistributionError::ContractTooLarge 324 ); 325 326 let very_large = format!("{}#{}", CONTRACT, "x".repeat(4 * 1024 * 1024)); 327 assert_eq!( 328 RadrootsRuntimeDistributionResolver::parse_str(&very_large) 329 .expect_err("very large contract must fail"), 330 RadrootsRuntimeDistributionError::ContractTooLarge 331 ); 332 } 333 334 #[test] 335 fn new_rejects_unexpected_schema() { 336 let mut contract = contract_value(); 337 contract["schema"] = Value::String("wrong-schema".to_string()); 338 339 let raw = toml::to_string(&contract).expect("serialize contract"); 340 let err = RadrootsRuntimeDistributionResolver::parse_str(&raw) 341 .expect_err("unexpected schema should fail"); 342 343 assert_eq!(err, RadrootsRuntimeDistributionError::UnexpectedSchema); 344 } 345 346 #[test] 347 fn resolves_cli_linux_artifact_with_explicit_channel() { 348 let resolver = 349 RadrootsRuntimeDistributionResolver::parse_str(CONTRACT).expect("parse contract"); 350 351 let artifact = resolver 352 .resolve_artifact(&RuntimeArtifactRequest { 353 runtime_id: "cli", 354 os: "linux", 355 arch: "amd64", 356 version: "1.0.0-alpha.1", 357 channel: Some("stable"), 358 }) 359 .expect("resolve cli artifact"); 360 361 assert_eq!(artifact.binary_name.as_deref(), Some("radroots")); 362 assert_eq!(artifact.target_id, "x86_64-unknown-linux-gnu"); 363 assert_eq!(artifact.archive_extension, ".tar.gz"); 364 assert_eq!( 365 artifact.artifact_file_name, 366 "cli-1.0.0-alpha.1-x86_64-unknown-linux-gnu.tar.gz" 367 ); 368 } 369 370 #[test] 371 fn resolves_radrootsd_linux_arm64_using_default_channel() { 372 let resolver = 373 RadrootsRuntimeDistributionResolver::parse_str(CONTRACT).expect("parse contract"); 374 375 let artifact = resolver 376 .resolve_artifact(&RuntimeArtifactRequest { 377 runtime_id: "radrootsd", 378 os: "linux", 379 arch: "arm64", 380 version: "1.0.0-alpha.1", 381 channel: None, 382 }) 383 .expect("resolve radrootsd artifact"); 384 385 assert_eq!(artifact.channel, "stable"); 386 assert_eq!(artifact.target_id, "aarch64-unknown-linux-gnu"); 387 assert_eq!(artifact.binary_name.as_deref(), Some("radrootsd")); 388 } 389 390 #[test] 391 fn resolves_desktop_bundle_for_macos_arm64() { 392 let resolver = 393 RadrootsRuntimeDistributionResolver::parse_str(CONTRACT).expect("parse contract"); 394 395 let artifact = resolver 396 .resolve_artifact(&RuntimeArtifactRequest { 397 runtime_id: "fixture-desktop-bundle", 398 os: "macos", 399 arch: "arm64", 400 version: "1.0.0-alpha.1", 401 channel: Some("stable"), 402 }) 403 .expect("resolve desktop artifact"); 404 405 assert_eq!(artifact.target_id, "aarch64-apple-darwin"); 406 assert_eq!(artifact.package_name, "fixture_desktop_bundle"); 407 } 408 409 #[test] 410 fn rejects_non_installable_mobile_runtime() { 411 let resolver = 412 RadrootsRuntimeDistributionResolver::parse_str(CONTRACT).expect("parse contract"); 413 414 let err = resolver 415 .resolve_artifact(&RuntimeArtifactRequest { 416 runtime_id: "fixture-mobile-package", 417 os: "macos", 418 arch: "arm64", 419 version: "1.0.0-alpha.1", 420 channel: Some("stable"), 421 }) 422 .expect_err("mobile runtime should not be installable"); 423 424 assert_eq!(err, RadrootsRuntimeDistributionError::RuntimeNotInstallable); 425 } 426 427 #[test] 428 fn rejects_bootstrap_only_runtime() { 429 let resolver = 430 RadrootsRuntimeDistributionResolver::parse_str(CONTRACT).expect("parse contract"); 431 432 let err = resolver 433 .resolve_artifact(&RuntimeArtifactRequest { 434 runtime_id: "hyf", 435 os: "macos", 436 arch: "arm64", 437 version: "0.1.0", 438 channel: Some("stable"), 439 }) 440 .expect_err("bootstrap runtime should not be installable"); 441 442 assert_eq!(err, RadrootsRuntimeDistributionError::RuntimeNotInstallable); 443 } 444 445 #[test] 446 fn rejects_inactive_channel() { 447 let resolver = 448 RadrootsRuntimeDistributionResolver::parse_str(CONTRACT).expect("parse contract"); 449 450 let err = resolver 451 .resolve_artifact(&RuntimeArtifactRequest { 452 runtime_id: "cli", 453 os: "linux", 454 arch: "amd64", 455 version: "1.0.0-alpha.1", 456 channel: Some("candidate"), 457 }) 458 .expect_err("candidate channel should be inactive"); 459 460 assert_eq!(err, RadrootsRuntimeDistributionError::InactiveChannel); 461 } 462 463 #[test] 464 fn rejects_unknown_runtime() { 465 let resolver = 466 RadrootsRuntimeDistributionResolver::parse_str(CONTRACT).expect("parse contract"); 467 468 let err = resolve_error( 469 &resolver, 470 RuntimeArtifactRequest { 471 runtime_id: "missing-runtime", 472 os: "linux", 473 arch: "amd64", 474 version: "1.0.0-alpha.1", 475 channel: Some("stable"), 476 }, 477 ); 478 479 assert_eq!(err, RadrootsRuntimeDistributionError::UnknownRuntime); 480 } 481 482 #[test] 483 fn rejects_unknown_channel() { 484 let resolver = 485 RadrootsRuntimeDistributionResolver::parse_str(CONTRACT).expect("parse contract"); 486 487 let err = resolve_error( 488 &resolver, 489 RuntimeArtifactRequest { 490 runtime_id: "cli", 491 os: "linux", 492 arch: "amd64", 493 version: "1.0.0-alpha.1", 494 channel: Some("beta"), 495 }, 496 ); 497 498 assert_eq!(err, RadrootsRuntimeDistributionError::UnknownChannel); 499 } 500 501 #[test] 502 fn rejects_unsupported_platform() { 503 let resolver = 504 RadrootsRuntimeDistributionResolver::parse_str(CONTRACT).expect("parse contract"); 505 506 let err = resolver 507 .resolve_artifact(&RuntimeArtifactRequest { 508 runtime_id: "radrootsd", 509 os: "windows", 510 arch: "amd64", 511 version: "1.0.0-alpha.1", 512 channel: Some("stable"), 513 }) 514 .expect_err("windows target should be unsupported"); 515 516 assert_eq!(err, RadrootsRuntimeDistributionError::UnsupportedPlatform); 517 } 518 519 #[test] 520 fn rejects_runtime_with_missing_target_set() { 521 let mut contract = contract_value(); 522 let runtime = contract["runtime"] 523 .as_array_mut() 524 .expect("runtime array") 525 .iter_mut() 526 .find(|runtime| runtime["id"].as_str() == Some("fixture-mobile-package")) 527 .expect("ios runtime"); 528 runtime["human_installable"] = Value::Boolean(true); 529 530 let resolver = resolver_from_value(contract); 531 let err = resolve_error( 532 &resolver, 533 RuntimeArtifactRequest { 534 runtime_id: "fixture-mobile-package", 535 os: "ios", 536 arch: "arm64", 537 version: "1.0.0-alpha.1", 538 channel: Some("stable"), 539 }, 540 ); 541 542 assert_eq!(err, RadrootsRuntimeDistributionError::MissingTargetSet); 543 } 544 545 #[test] 546 fn rejects_unknown_artifact_adapter() { 547 let mut contract = contract_value(); 548 let runtime = contract["runtime"] 549 .as_array_mut() 550 .expect("runtime array") 551 .iter_mut() 552 .find(|runtime| runtime["id"].as_str() == Some("cli")) 553 .expect("cli runtime"); 554 runtime["artifact_adapter"] = Value::String("missing_adapter".to_string()); 555 556 let resolver = resolver_from_value(contract); 557 let err = resolve_error( 558 &resolver, 559 RuntimeArtifactRequest { 560 runtime_id: "cli", 561 os: "linux", 562 arch: "amd64", 563 version: "1.0.0-alpha.1", 564 channel: Some("stable"), 565 }, 566 ); 567 568 assert_eq!( 569 err, 570 RadrootsRuntimeDistributionError::UnknownArtifactAdapter 571 ); 572 } 573 574 #[test] 575 fn rejects_missing_target_set_definition() { 576 let mut contract = contract_value(); 577 let runtime = contract["runtime"] 578 .as_array_mut() 579 .expect("runtime array") 580 .iter_mut() 581 .find(|runtime| runtime["id"].as_str() == Some("cli")) 582 .expect("cli runtime"); 583 runtime["target_set"] = Value::String("missing-target-set".to_string()); 584 585 let resolver = resolver_from_value(contract); 586 let err = resolve_error( 587 &resolver, 588 RuntimeArtifactRequest { 589 runtime_id: "cli", 590 os: "linux", 591 arch: "amd64", 592 version: "1.0.0-alpha.1", 593 channel: Some("stable"), 594 }, 595 ); 596 597 assert_eq!(err, RadrootsRuntimeDistributionError::UnsupportedPlatform); 598 } 599 600 #[test] 601 fn rejects_target_set_with_unknown_target() { 602 let mut contract = contract_value(); 603 contract["target_sets"]["cli_default"]["targets"] = 604 Value::Array(vec![Value::String("missing-target".to_string())]); 605 606 let resolver = resolver_from_value(contract); 607 let err = resolve_error( 608 &resolver, 609 RuntimeArtifactRequest { 610 runtime_id: "cli", 611 os: "linux", 612 arch: "amd64", 613 version: "1.0.0-alpha.1", 614 channel: Some("stable"), 615 }, 616 ); 617 618 assert_eq!(err, RadrootsRuntimeDistributionError::UnknownTarget); 619 } 620 621 #[test] 622 fn infers_archive_format_from_single_supported_adapter_format() { 623 let mut contract = contract_value(); 624 contract["targets"]["x86_64-unknown-linux-gnu"] 625 .as_table_mut() 626 .expect("target table") 627 .remove("archive_format"); 628 contract["artifact_adapters"]["rust_binary_archive"]["supported_archive_formats"] = 629 Value::Array(vec![Value::String("tar.gz".to_string())]); 630 631 let resolver = resolver_from_value(contract); 632 let artifact = resolver 633 .resolve_artifact(&RuntimeArtifactRequest { 634 runtime_id: "cli", 635 os: "linux", 636 arch: "amd64", 637 version: "1.0.0-alpha.1", 638 channel: Some("stable"), 639 }) 640 .expect("single supported format should be inferred"); 641 642 assert_eq!(artifact.archive_format, "tar.gz"); 643 assert_eq!(artifact.archive_extension, ".tar.gz"); 644 } 645 646 #[test] 647 fn rejects_unknown_archive_format_reference() { 648 let mut contract = contract_value(); 649 contract["targets"]["x86_64-unknown-linux-gnu"]["archive_format"] = 650 Value::String("tar.xz".to_string()); 651 652 let resolver = resolver_from_value(contract); 653 let err = resolve_error( 654 &resolver, 655 RuntimeArtifactRequest { 656 runtime_id: "cli", 657 os: "linux", 658 arch: "amd64", 659 version: "1.0.0-alpha.1", 660 channel: Some("stable"), 661 }, 662 ); 663 664 assert_eq!(err, RadrootsRuntimeDistributionError::UnknownArchiveFormat); 665 } 666 667 #[test] 668 fn rejects_missing_archive_format_when_adapter_is_ambiguous() { 669 let mut contract = contract_value(); 670 contract["targets"]["aarch64-apple-darwin"] 671 .as_table_mut() 672 .expect("target table") 673 .remove("archive_format"); 674 675 let resolver = resolver_from_value(contract); 676 let err = resolve_error( 677 &resolver, 678 RuntimeArtifactRequest { 679 runtime_id: "fixture-desktop-bundle", 680 os: "macos", 681 arch: "arm64", 682 version: "1.0.0-alpha.1", 683 channel: Some("stable"), 684 }, 685 ); 686 687 assert_eq!(err, RadrootsRuntimeDistributionError::MissingArchiveFormat); 688 } 689 690 #[test] 691 fn durable_contract_resolves_exact_hardened_service_metadata() { 692 let resolver = RadrootsRuntimeDistributionResolver::parse_str(HARDENED_SERVICE_CONTRACT) 693 .expect("hardened service contract"); 694 695 for service in ["myc", "rhi"] { 696 let service_id = ServiceId::new(service).expect("service id"); 697 let metadata = resolver 698 .service_target(&service_id) 699 .expect("service metadata"); 700 assert_eq!(metadata.service_id(), &service_id); 701 assert_eq!( 702 metadata.instance_support(), 703 ServiceInstanceSupport::Multiple 704 ); 705 assert_eq!(metadata.config_format(), ServiceConfigurationFormat::Toml); 706 assert_eq!(metadata.config_format().as_str(), "toml"); 707 assert_eq!( 708 metadata.state_initialization(), 709 ServiceStateInitialization::Explicit 710 ); 711 assert_eq!( 712 metadata.run_state_policy(), 713 ServiceRunStatePolicy::ExistingOnly 714 ); 715 assert_eq!( 716 metadata.admin_transport(), 717 ServiceAdminTransport::Http11OverUnixDomainSocket 718 ); 719 assert_eq!(metadata.admin_base_path(), ServiceAdminBasePath::V1); 720 assert_eq!(metadata.admin_contract_version(), 1); 721 assert_eq!( 722 metadata.status_surface(), 723 ServiceStatusSurface::LocalAdminServiceStatusV1 724 ); 725 assert_eq!( 726 metadata.operations_surface(), 727 ServiceOperationsSurface::CachedLivezReadyzMetrics 728 ); 729 assert_eq!( 730 metadata.operations_surface().routes(), 731 ["/livez", "/readyz", "/metrics"] 732 ); 733 assert_eq!(metadata.support_posture(), ServiceSupportPosture::Target); 734 assert_eq!(metadata.tier_1_targets(), ServiceTier1Target::ALL); 735 736 for target in ServiceTier1Target::ALL { 737 let resolved = resolver 738 .resolve_service_target(&ServiceTargetRequest { 739 service_id: &service_id, 740 target_id: target.as_str(), 741 }) 742 .expect("eligible target"); 743 assert_eq!(resolved.service_id(), &service_id); 744 assert_eq!(resolved.target(), target); 745 } 746 } 747 748 let targets = &resolver.contract().service_targets; 749 assert_eq!(targets.len(), 2); 750 assert!(!targets.is_empty()); 751 assert_eq!( 752 targets 753 .iter() 754 .map(|(service, _)| service) 755 .collect::<Vec<_>>(), 756 ["myc", "rhi"] 757 ); 758 let artifacts = &resolver.contract().service_artifacts; 759 assert_eq!(artifacts.len(), 2); 760 assert!(!artifacts.is_empty()); 761 assert_eq!( 762 artifacts 763 .iter() 764 .map(|(service, _)| service) 765 .collect::<Vec<_>>(), 766 ["myc", "rhi"] 767 ); 768 769 let literal = ServiceArtifactSha256::from_bytes([0x5a; 32]); 770 assert_eq!(literal.as_bytes(), &[0x5a; 32]); 771 assert_eq!(format!("{literal:?}"), "ServiceArtifactSha256(<redacted>)"); 772 } 773 774 #[test] 775 fn hardened_services_resolve_exact_native_artifacts_and_reject_unsupported_targets() { 776 let resolver = RadrootsRuntimeDistributionResolver::parse_str(HARDENED_SERVICE_CONTRACT) 777 .expect("hardened service contract"); 778 let myc = ServiceId::new("myc").expect("myc"); 779 780 for target_id in ["aarch64-apple-darwin", "x86_64-pc-windows-msvc", "linux-64"] { 781 assert_eq!( 782 resolver.resolve_service_target(&ServiceTargetRequest { 783 service_id: &myc, 784 target_id, 785 }), 786 Err(RadrootsRuntimeDistributionError::UnsupportedServiceTarget) 787 ); 788 } 789 let artifact = resolver 790 .resolve_service_artifact(&ServiceArtifactRequest { 791 service_id: &myc, 792 target_id: "x86_64-unknown-linux-gnu", 793 }) 794 .expect("Myc artifact"); 795 assert_eq!(artifact.service_id(), &myc); 796 assert_eq!(artifact.target(), ServiceTier1Target::X86_64UnknownLinuxGnu); 797 assert_eq!(artifact.version(), "0.1.0"); 798 assert_eq!(artifact.package_name(), "myc"); 799 assert_eq!(artifact.binary_name(), "myc"); 800 assert_eq!(artifact.channel(), "stable"); 801 assert_eq!(artifact.binary_archive_name(), "binary.tar.gz"); 802 assert_eq!(artifact.binary_archive_format(), "tar.gz"); 803 assert_eq!( 804 artifact.binary_archive_member(), 805 "myc-0.1.0-x86_64-unknown-linux-gnu/myc" 806 ); 807 assert_eq!( 808 artifact.artifact_manifest_name(), 809 "artifact-manifest.v1.json" 810 ); 811 assert_eq!(artifact.checksums_name(), "SHA256SUMS"); 812 assert_eq!(artifact.checksum_algorithm(), "sha256"); 813 assert_eq!( 814 artifact.checksum_format(), 815 "sha256_lower_hex_two_spaces_path_lf_sorted_by_path" 816 ); 817 assert_eq!(artifact.output_inventory().len(), 12); 818 assert_eq!(artifact.output_inventory()[0], "LICENSE"); 819 assert_eq!(artifact.output_inventory()[11], "systemd.service"); 820 assert_eq!( 821 artifact.release_contract_sha256(), 822 resolver 823 .service_artifact(&myc) 824 .expect("Myc release") 825 .release_contract_sha256() 826 ); 827 assert_eq!( 828 artifact.source_lock_sha256(), 829 resolver 830 .service_artifact(&myc) 831 .expect("Myc release") 832 .source_lock_sha256() 833 ); 834 let rendered = format!("{artifact:?}"); 835 assert!(!rendered.contains("4b3ba578")); 836 assert!(!rendered.contains("f5ebb390")); 837 let rhi = ServiceId::new("rhi").expect("rhi"); 838 let rhi_artifact = resolver 839 .resolve_service_artifact(&ServiceArtifactRequest { 840 service_id: &rhi, 841 target_id: "aarch64-unknown-linux-gnu", 842 }) 843 .expect("RHI artifact"); 844 assert_eq!( 845 rhi_artifact.binary_archive_member(), 846 "rhi-0.1.0-aarch64-unknown-linux-gnu/rhi" 847 ); 848 assert_eq!( 849 resolver.resolve_artifact(&RuntimeArtifactRequest { 850 runtime_id: "myc", 851 os: "linux", 852 arch: "amd64", 853 version: "1.0.0", 854 channel: None, 855 }), 856 Err(RadrootsRuntimeDistributionError::UnknownRuntime) 857 ); 858 } 859 860 #[test] 861 fn hardened_services_reject_parsed_and_direct_artifact_authority() { 862 let raw = format!( 863 "{HARDENED_SERVICE_CONTRACT}\n\ 864 [[runtime]]\n\ 865 id = \"myc\"\n\ 866 distribution_state = \"defined\"\n\ 867 release_unit = \"myc\"\n\ 868 package_name = \"radroots_myc\"\n\ 869 artifact_adapter = \"rust_binary_archive\"\n\ 870 default_channel = \"stable\"\n\ 871 human_installable = true\n" 872 ); 873 assert_eq!( 874 RadrootsRuntimeDistributionResolver::parse_str(&raw).expect_err("parsed bypass"), 875 RadrootsRuntimeDistributionError::HardenedServiceLegacyArtifactRow 876 ); 877 878 let mut contract = 879 toml::from_str::<RadrootsRuntimeDistributionContract>(HARDENED_SERVICE_CONTRACT) 880 .expect("direct contract"); 881 contract.runtime.push(RuntimeDistributionEntry { 882 id: "rhi".to_owned(), 883 distribution_state: "defined".to_owned(), 884 release_unit: "rhi".to_owned(), 885 package_name: "radroots_rhi".to_owned(), 886 binary_name: None, 887 artifact_adapter: "rust_binary_archive".to_owned(), 888 target_set: None, 889 default_channel: "stable".to_owned(), 890 human_installable: true, 891 notes: None, 892 }); 893 assert_eq!( 894 RadrootsRuntimeDistributionResolver::new(contract).expect_err("direct bypass"), 895 RadrootsRuntimeDistributionError::HardenedServiceLegacyArtifactRow 896 ); 897 } 898 899 #[test] 900 fn hardened_service_artifacts_reject_every_identity_and_inventory_drift() { 901 for (needle, replacement) in [ 902 ( 903 "release_contract = \"contracts/services_hardening/native_release.v2.json\"", 904 "release_contract = \"contracts/services_hardening/untrusted.json\"", 905 ), 906 ( 907 "f5ebb390a480830d51d502facc623bd1b10eda27b12dad9f3dbb6a1f1f949217", 908 "f5ebb390a480830d51d502facc623bd1b10eda27b12dad9f3dbb6a1f1f949218", 909 ), 910 ("package_name = \"myc\"", "package_name = \"other\""), 911 ("binary_name = \"myc\"", "binary_name = \"other\""), 912 ("version = \"0.1.0\"", "version = \"0.1.1\""), 913 ("channel = \"stable\"", "channel = \"candidate\""), 914 ( 915 "binary_archive_name = \"binary.tar.gz\"", 916 "binary_archive_name = \"myc.tar.gz\"", 917 ), 918 ( 919 "artifact_manifest_name = \"artifact-manifest.v1.json\"", 920 "artifact_manifest_name = \"untrusted.json\"", 921 ), 922 ( 923 "checksums_name = \"SHA256SUMS\"", 924 "checksums_name = \"checksums.txt\"", 925 ), 926 ( 927 "77b381648ed1e586efb696888beb05b9215c69cf", 928 "77b381648ed1e586efb696888beb05b9215c69ce", 929 ), 930 ( 931 "4b3ba5789fac6aa219e84e1e5c002cf8230b72f95fd6d95a6419d2fdf2915f83", 932 "4b3ba5789fac6aa219e84e1e5c002cf8230b72f95fd6d95a6419d2fdf2915f84", 933 ), 934 ] { 935 let drift = HARDENED_SERVICE_CONTRACT.replacen(needle, replacement, 1); 936 assert!( 937 RadrootsRuntimeDistributionResolver::parse_str(&drift).is_err(), 938 "drift `{needle}` must fail" 939 ); 940 } 941 942 let missing_member = 943 HARDENED_SERVICE_CONTRACT.replacen("\"systemd.service\"", "\"unexpected\"", 1); 944 assert!(RadrootsRuntimeDistributionResolver::parse_str(&missing_member).is_err()); 945 let uppercase_hash = HARDENED_SERVICE_CONTRACT.replacen("4b3ba578", "4B3BA578", 1); 946 assert!(RadrootsRuntimeDistributionResolver::parse_str(&uppercase_hash).is_err()); 947 let short_hash = HARDENED_SERVICE_CONTRACT.replacen( 948 "4b3ba5789fac6aa219e84e1e5c002cf8230b72f95fd6d95a6419d2fdf2915f83", 949 "00", 950 1, 951 ); 952 assert!(RadrootsRuntimeDistributionResolver::parse_str(&short_hash).is_err()); 953 let unknown = HARDENED_SERVICE_CONTRACT.replacen( 954 "[service_artifacts.myc]", 955 "[service_artifacts.myc]\nunknown = true", 956 1, 957 ); 958 assert!(RadrootsRuntimeDistributionResolver::parse_str(&unknown).is_err()); 959 960 let mut unsupported_service: Value = 961 toml::from_str(HARDENED_SERVICE_CONTRACT).expect("contract fixture value"); 962 unsupported_service["service_artifacts"]["myc"]["service_id"] = 963 Value::String("unsupported".to_owned()); 964 assert!( 965 RadrootsRuntimeDistributionResolver::parse_str( 966 &toml::to_string(&unsupported_service).expect("unsupported-service contract") 967 ) 968 .is_err() 969 ); 970 971 let mut missing_artifact: Value = 972 toml::from_str(HARDENED_SERVICE_CONTRACT).expect("contract fixture value"); 973 let artifacts = missing_artifact["service_artifacts"] 974 .as_table_mut() 975 .expect("service artifact table"); 976 let rhi = artifacts.remove("rhi").expect("RHI artifact"); 977 artifacts.insert("other".to_owned(), rhi); 978 assert!( 979 RadrootsRuntimeDistributionResolver::parse_str( 980 &toml::to_string(&missing_artifact).expect("missing-artifact contract") 981 ) 982 .is_err() 983 ); 984 985 let mut empty_artifacts: Value = 986 toml::from_str(HARDENED_SERVICE_CONTRACT).expect("contract fixture value"); 987 empty_artifacts["service_artifacts"] 988 .as_table_mut() 989 .expect("service artifact table") 990 .clear(); 991 assert!( 992 RadrootsRuntimeDistributionResolver::parse_str( 993 &toml::to_string(&empty_artifacts).expect("empty-artifact contract") 994 ) 995 .is_err() 996 ); 997 } 998 999 #[test] 1000 fn hardened_service_contract_rejects_schema_drift_unknown_fields_and_inventory_drift() { 1001 let mut target_drift: Value = 1002 toml::from_str(HARDENED_SERVICE_CONTRACT).expect("contract fixture value"); 1003 target_drift["service_targets"]["myc"]["tier_1_targets"][1] = 1004 Value::String("aarch64-apple-darwin".to_owned()); 1005 1006 for raw in [ 1007 HARDENED_SERVICE_CONTRACT.replace("schema_version = 1", "schema_version = 2"), 1008 format!("{HARDENED_SERVICE_CONTRACT}\nunknown = true\n"), 1009 HARDENED_SERVICE_CONTRACT.replace("service_id = \"rhi\"", "service_id = \"other\""), 1010 toml::to_string(&target_drift).expect("target-drift contract"), 1011 ] { 1012 assert!(RadrootsRuntimeDistributionResolver::parse_str(&raw).is_err()); 1013 } 1014 1015 let mut missing_service: Value = 1016 toml::from_str(HARDENED_SERVICE_CONTRACT).expect("contract fixture value"); 1017 let targets = missing_service["service_targets"] 1018 .as_table_mut() 1019 .expect("service target table"); 1020 let rhi = targets.remove("rhi").expect("RHI target"); 1021 targets.insert("other".to_owned(), rhi); 1022 assert!( 1023 RadrootsRuntimeDistributionResolver::parse_str( 1024 &toml::to_string(&missing_service).expect("missing-service contract") 1025 ) 1026 .is_err() 1027 ); 1028 1029 let mut empty_targets: Value = 1030 toml::from_str(HARDENED_SERVICE_CONTRACT).expect("contract fixture value"); 1031 empty_targets["service_targets"] 1032 .as_table_mut() 1033 .expect("service target table") 1034 .clear(); 1035 assert!( 1036 RadrootsRuntimeDistributionResolver::parse_str( 1037 &toml::to_string(&empty_targets).expect("empty-target contract") 1038 ) 1039 .is_err() 1040 ); 1041 1042 let mut mismatched_service: Value = 1043 toml::from_str(HARDENED_SERVICE_CONTRACT).expect("contract fixture value"); 1044 let targets = mismatched_service["service_targets"] 1045 .as_table_mut() 1046 .expect("service target table"); 1047 targets["myc"]["service_id"] = Value::String("rhi".to_owned()); 1048 assert!( 1049 RadrootsRuntimeDistributionResolver::parse_str( 1050 &toml::to_string(&mismatched_service).expect("mismatched-service contract") 1051 ) 1052 .is_err() 1053 ); 1054 } 1055 1056 #[test] 1057 fn hardened_service_contract_requires_stable_in_both_channel_inventories() { 1058 for channel_inventory in ["active", "defined"] { 1059 let mut contract: Value = 1060 toml::from_str(HARDENED_SERVICE_CONTRACT).expect("contract fixture value"); 1061 contract["channels"][channel_inventory] = 1062 Value::Array(vec![Value::String("candidate".to_owned())]); 1063 assert_eq!( 1064 RadrootsRuntimeDistributionResolver::parse_str( 1065 &toml::to_string(&contract).expect("channel-drift contract") 1066 ) 1067 .expect_err("stable channel is mandatory"), 1068 RadrootsRuntimeDistributionError::InvalidServiceArtifactContract 1069 ); 1070 } 1071 } 1072 1073 #[test] 1074 fn standalone_hardened_service_target_rejects_contract_drift() { 1075 let contract: Value = 1076 toml::from_str(HARDENED_SERVICE_CONTRACT).expect("contract fixture value"); 1077 let target = 1078 toml::to_string(&contract["service_targets"]["myc"]).expect("standalone target"); 1079 let parsed = toml::from_str::<HardenedServiceTarget>(&target).expect("valid target"); 1080 assert_eq!(parsed.service_id().as_str(), "myc"); 1081 1082 for raw in [ 1083 target.replace("admin_contract_version = 1", "admin_contract_version = 99"), 1084 target.replace("service_id = \"myc\"", "service_id = \"unsupported\""), 1085 target.replace( 1086 "\"x86_64-unknown-linux-gnu\", \"aarch64-unknown-linux-gnu\"", 1087 "\"aarch64-unknown-linux-gnu\"", 1088 ), 1089 ] { 1090 assert!(toml::from_str::<HardenedServiceTarget>(&raw).is_err()); 1091 } 1092 } 1093 1094 #[test] 1095 fn distribution_errors_do_not_expose_contract_values_or_parser_causes() { 1096 use std::error::Error as _; 1097 1098 for (raw, secret) in [ 1099 ( 1100 HARDENED_SERVICE_CONTRACT.replace( 1101 "schema = \"radroots-runtime-distribution\"", 1102 "schema = \"secret-contract-value\"", 1103 ), 1104 "secret-contract-value", 1105 ), 1106 ( 1107 "credential = 'secret-value'\ninvalid = [".to_owned(), 1108 "secret-value", 1109 ), 1110 ] { 1111 let error = 1112 RadrootsRuntimeDistributionResolver::parse_str(&raw).expect_err("invalid contract"); 1113 let rendered = format!("{error} {error:?}"); 1114 assert!(!rendered.contains(secret)); 1115 assert!(error.source().is_none()); 1116 } 1117 } 1118 }