lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

commit cbefdbd5b663bc241c762a6efeba74d7e9f34eb4
parent 6d61cfc2b4fe9c3b1914c2d6a8077b52b29e02e1
Author: triesap <tyson@radroots.org>
Date:   Tue, 25 Aug 2026 00:30:50 +0000

runtime-distribution: resolve hardened service artifacts

- bind exact Myc and RHI native release contracts and source locks
- resolve closed Linux archive members and checksum inventories
- reject legacy runtime-row bypasses and all governed metadata drift
- document metadata-only authority with Nix and OCI unclaimed

Diffstat:
Mcrates/runtime_distribution/README | 14+++++++++++---
Mcrates/runtime_distribution/src/error.rs | 6++++--
Mcrates/runtime_distribution/src/lib.rs | 168+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Mcrates/runtime_distribution/src/model.rs | 2++
Mcrates/runtime_distribution/src/resolve.rs | 186++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Acrates/runtime_distribution/src/service_artifact.rs | 311+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/runtime_distribution/tests/fixtures/hardened_service_targets.v1.toml | 36++++++++++++++++++++++++++++++++++--
Mcrates/runtime_distribution/tests/package_boundary.rs | 26++++++++++++++++++++------
8 files changed, 728 insertions(+), 21 deletions(-)

diff --git a/crates/runtime_distribution/README b/crates/runtime_distribution/README @@ -12,15 +12,23 @@ distribution contract resolution for the `radroots` core libraries. * an exact, typed Myc/RHI service-target inventory for multiple-instance TOML services with explicit existing-state startup, Unix local administration, cached operations endpoints, and Linux x86_64/aarch64 Tier-1 eligibility; + * a sealed Myc/RHI native-artifact inventory binding each service's exact + remotely reachable revision, native-release contract and SHA-256, source + lock and SHA-256, stable channel, package/binary identity, deterministic + twelve-member output inventory, checksum manifest, and target-specific + binary archive member; * TOML-backed contract handling for modular runtime deployment metadata. Complete TOML documents are rejected before parsing when they exceed exactly 1,048,576 UTF-8 bytes. Schema, version, unknown-field, and exact-inventory validation remains fail closed after bounded admission. -The hardened service-target inventory is metadata-only. It deliberately does -not define Myc or RHI binaries, packages, archives, channels, artifact names, -or qualified support claims. +The hardened service artifact entries are resolution metadata only. They do +not fetch, install, execute, sign, publish, or deploy an artifact, and target +eligibility is not a qualified-support claim. Actual target-build byte hashes +remain generated evidence in the exact `artifact-manifest.v1.json` and +`SHA256SUMS` output; the resolver does not fabricate a digest before that +deterministic build exists. Nix and OCI outputs remain deferred and unclaimed. ## Copyright diff --git a/crates/runtime_distribution/src/error.rs b/crates/runtime_distribution/src/error.rs @@ -14,8 +14,10 @@ pub enum RadrootsRuntimeDistributionError { UnknownRuntime, #[error("runtime is not installable through the distribution contract")] RuntimeNotInstallable, - #[error("hardened service artifact authority is deferred")] - HardenedServiceArtifactDeferred, + #[error("hardened service artifact authority cannot use a legacy runtime row")] + HardenedServiceLegacyArtifactRow, + #[error("hardened service artifact contract is invalid")] + InvalidServiceArtifactContract, #[error("runtime has no target set in the distribution contract")] MissingTargetSet, #[error("runtime references an unknown artifact adapter")] diff --git a/crates/runtime_distribution/src/lib.rs b/crates/runtime_distribution/src/lib.rs @@ -4,6 +4,7 @@ pub mod error; pub mod model; pub mod resolve; pub mod service; +mod service_artifact; pub use error::RadrootsRuntimeDistributionError; pub use model::{ @@ -13,7 +14,8 @@ pub use model::{ pub use resolve::{ RUNTIME_DISTRIBUTION_CONTRACT_MAX_UTF8_BYTES, RUNTIME_DISTRIBUTION_SCHEMA, RUNTIME_DISTRIBUTION_SCHEMA_VERSION, RadrootsRuntimeDistributionResolver, - ResolvedRuntimeArtifact, ResolvedServiceTarget, RuntimeArtifactRequest, ServiceTargetRequest, + ResolvedRuntimeArtifact, ResolvedServiceArtifact, ResolvedServiceTarget, + RuntimeArtifactRequest, ServiceArtifactRequest, ServiceTargetRequest, }; pub use service::{ HardenedServiceTarget, HardenedServiceTargets, ServiceAdminBasePath, ServiceAdminTransport, @@ -21,6 +23,10 @@ pub use service::{ ServiceRunStatePolicy, ServiceStateInitialization, ServiceStatusSurface, ServiceSupportPosture, ServiceTier1Target, }; +pub use service_artifact::{ + HardenedServiceArtifact, HardenedServiceArtifacts, ServiceArtifactChannel, + ServiceArtifactSha256, +}; #[cfg(test)] mod tests { @@ -32,9 +38,10 @@ mod tests { RUNTIME_DISTRIBUTION_SCHEMA, RadrootsRuntimeDistributionContract, RadrootsRuntimeDistributionError, RadrootsRuntimeDistributionResolver, RuntimeArtifactRequest, RuntimeDistributionEntry, ServiceAdminBasePath, - ServiceAdminTransport, ServiceConfigurationFormat, ServiceInstanceSupport, - ServiceOperationsSurface, ServiceRunStatePolicy, ServiceStateInitialization, - ServiceStatusSurface, ServiceSupportPosture, ServiceTargetRequest, ServiceTier1Target, + ServiceAdminTransport, ServiceArtifactRequest, ServiceArtifactSha256, + ServiceConfigurationFormat, ServiceInstanceSupport, ServiceOperationsSurface, + ServiceRunStatePolicy, ServiceStateInitialization, ServiceStatusSurface, + ServiceSupportPosture, ServiceTargetRequest, ServiceTier1Target, }; const HARDENED_SERVICE_CONTRACT: &str = @@ -229,6 +236,38 @@ status_surface = "local_admin_service_status_v1" operations_surface = "cached_livez_readyz_metrics" support_posture = "target" tier_1_targets = ["x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu"] + +[service_artifacts.myc] +service_id = "myc" +service_revision = "77b381648ed1e586efb696888beb05b9215c69cf" +release_contract = "contracts/services_hardening/native_release.v2.json" +release_contract_sha256 = "4b3ba5789fac6aa219e84e1e5c002cf8230b72f95fd6d95a6419d2fdf2915f83" +source_lock_sha256 = "f5ebb390a480830d51d502facc623bd1b10eda27b12dad9f3dbb6a1f1f949217" +package_name = "myc" +binary_name = "myc" +version = "0.1.0" +channel = "stable" +binary_archive_name = "binary.tar.gz" +artifact_manifest_name = "artifact-manifest.v1.json" +checksums_name = "SHA256SUMS" +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"] +tier_1_targets = ["x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu"] + +[service_artifacts.rhi] +service_id = "rhi" +service_revision = "07aa6ea988da5372654bb3d1ee183ac099a77cae" +release_contract = "contracts/services_hardening/native_release.v1.json" +release_contract_sha256 = "06a973176b4b8c11dad13000604576527df829dd0bbe2f501158662f75e70b94" +source_lock_sha256 = "3cc8bfac0d98730937754abae2ccfe20e40d0a9bbdefe02ebd94264c20f0d0ff" +package_name = "rhi" +binary_name = "rhi" +version = "0.1.0" +channel = "stable" +binary_archive_name = "binary.tar.gz" +artifact_manifest_name = "artifact-manifest.v1.json" +checksums_name = "SHA256SUMS" +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"] +tier_1_targets = ["x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu"] "#; fn contract_value() -> Value { @@ -716,10 +755,24 @@ tier_1_targets = ["x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu"] .collect::<Vec<_>>(), ["myc", "rhi"] ); + let artifacts = &resolver.contract().service_artifacts; + assert_eq!(artifacts.len(), 2); + assert!(!artifacts.is_empty()); + assert_eq!( + artifacts + .iter() + .map(|(service, _)| service) + .collect::<Vec<_>>(), + ["myc", "rhi"] + ); + + let literal = ServiceArtifactSha256::from_bytes([0x5a; 32]); + assert_eq!(literal.as_bytes(), &[0x5a; 32]); + assert_eq!(format!("{literal:?}"), "ServiceArtifactSha256(<redacted>)"); } #[test] - fn hardened_services_are_metadata_only_and_reject_unsupported_targets() { + fn hardened_services_resolve_exact_native_artifacts_and_reject_unsupported_targets() { let resolver = RadrootsRuntimeDistributionResolver::parse_str(HARDENED_SERVICE_CONTRACT) .expect("hardened service contract"); let myc = ServiceId::new("myc").expect("myc"); @@ -733,6 +786,65 @@ tier_1_targets = ["x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu"] Err(RadrootsRuntimeDistributionError::UnsupportedServiceTarget) ); } + let artifact = resolver + .resolve_service_artifact(&ServiceArtifactRequest { + service_id: &myc, + target_id: "x86_64-unknown-linux-gnu", + }) + .expect("Myc artifact"); + assert_eq!(artifact.service_id(), &myc); + assert_eq!(artifact.target(), ServiceTier1Target::X86_64UnknownLinuxGnu); + assert_eq!(artifact.version(), "0.1.0"); + assert_eq!(artifact.package_name(), "myc"); + assert_eq!(artifact.binary_name(), "myc"); + assert_eq!(artifact.channel(), "stable"); + assert_eq!(artifact.binary_archive_name(), "binary.tar.gz"); + assert_eq!(artifact.binary_archive_format(), "tar.gz"); + assert_eq!( + artifact.binary_archive_member(), + "myc-0.1.0-x86_64-unknown-linux-gnu/myc" + ); + assert_eq!( + artifact.artifact_manifest_name(), + "artifact-manifest.v1.json" + ); + assert_eq!(artifact.checksums_name(), "SHA256SUMS"); + assert_eq!(artifact.checksum_algorithm(), "sha256"); + assert_eq!( + artifact.checksum_format(), + "sha256_lower_hex_two_spaces_path_lf_sorted_by_path" + ); + assert_eq!(artifact.output_inventory().len(), 12); + assert_eq!(artifact.output_inventory()[0], "LICENSE"); + assert_eq!(artifact.output_inventory()[11], "systemd.service"); + assert_eq!( + artifact.release_contract_sha256(), + resolver + .service_artifact(&myc) + .expect("Myc release") + .release_contract_sha256() + ); + assert_eq!( + artifact.source_lock_sha256(), + resolver + .service_artifact(&myc) + .expect("Myc release") + .source_lock_sha256() + ); + let rendered = format!("{artifact:?}"); + assert!(!rendered.contains("4b3ba578")); + assert!(!rendered.contains("f5ebb390")); + let rhi = ServiceId::new("rhi").expect("rhi"); + let rhi_artifact = resolver + .resolve_service_artifact(&ServiceArtifactRequest { + service_id: &rhi, + target_id: "aarch64-unknown-linux-gnu", + }) + .expect("RHI artifact"); + assert_eq!( + rhi_artifact.binary_archive_member(), + "rhi-0.1.0-aarch64-unknown-linux-gnu/rhi" + ); assert_eq!( resolver.resolve_artifact(&RuntimeArtifactRequest { runtime_id: "myc", @@ -760,7 +872,7 @@ tier_1_targets = ["x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu"] ); assert_eq!( RadrootsRuntimeDistributionResolver::parse_str(&raw).expect_err("parsed bypass"), - RadrootsRuntimeDistributionError::HardenedServiceArtifactDeferred + RadrootsRuntimeDistributionError::HardenedServiceLegacyArtifactRow ); let mut contract = @@ -780,8 +892,50 @@ tier_1_targets = ["x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu"] }); assert_eq!( RadrootsRuntimeDistributionResolver::new(contract).expect_err("direct bypass"), - RadrootsRuntimeDistributionError::HardenedServiceArtifactDeferred + RadrootsRuntimeDistributionError::HardenedServiceLegacyArtifactRow + ); + } + + #[test] + fn hardened_service_artifacts_reject_every_identity_and_inventory_drift() { + for (needle, replacement) in [ + ("version = \"0.1.0\"", "version = \"0.1.1\""), + ("channel = \"stable\"", "channel = \"candidate\""), + ( + "binary_archive_name = \"binary.tar.gz\"", + "binary_archive_name = \"myc.tar.gz\"", + ), + ( + "checksums_name = \"SHA256SUMS\"", + "checksums_name = \"checksums.txt\"", + ), + ( + "77b381648ed1e586efb696888beb05b9215c69cf", + "77b381648ed1e586efb696888beb05b9215c69ce", + ), + ( + "4b3ba5789fac6aa219e84e1e5c002cf8230b72f95fd6d95a6419d2fdf2915f83", + "4b3ba5789fac6aa219e84e1e5c002cf8230b72f95fd6d95a6419d2fdf2915f84", + ), + ] { + let drift = HARDENED_SERVICE_CONTRACT.replacen(needle, replacement, 1); + assert!( + RadrootsRuntimeDistributionResolver::parse_str(&drift).is_err(), + "drift `{needle}` must fail" + ); + } + + let missing_member = + HARDENED_SERVICE_CONTRACT.replacen("\"systemd.service\"", "\"unexpected\"", 1); + assert!(RadrootsRuntimeDistributionResolver::parse_str(&missing_member).is_err()); + let uppercase_hash = HARDENED_SERVICE_CONTRACT.replacen("4b3ba578", "4B3BA578", 1); + assert!(RadrootsRuntimeDistributionResolver::parse_str(&uppercase_hash).is_err()); + let unknown = HARDENED_SERVICE_CONTRACT.replacen( + "[service_artifacts.myc]", + "[service_artifacts.myc]\nunknown = true", + 1, ); + assert!(RadrootsRuntimeDistributionResolver::parse_str(&unknown).is_err()); } #[test] diff --git a/crates/runtime_distribution/src/model.rs b/crates/runtime_distribution/src/model.rs @@ -3,6 +3,7 @@ use std::collections::BTreeMap; use serde::Deserialize; use crate::service::HardenedServiceTargets; +use crate::service_artifact::HardenedServiceArtifacts; #[derive(Debug, Clone, Deserialize, PartialEq, Eq)] #[serde(deny_unknown_fields)] @@ -24,6 +25,7 @@ pub struct RadrootsRuntimeDistributionContract { #[serde(default)] pub runtime: Vec<RuntimeDistributionEntry>, pub service_targets: HardenedServiceTargets, + pub service_artifacts: HardenedServiceArtifacts, } #[derive(Debug, Clone, Deserialize, PartialEq, Eq)] diff --git a/crates/runtime_distribution/src/resolve.rs b/crates/runtime_distribution/src/resolve.rs @@ -3,6 +3,7 @@ use crate::model::{ ArtifactAdapter, RadrootsRuntimeDistributionContract, RuntimeDistributionEntry, TargetSpec, }; use crate::service::{HardenedServiceTarget, ServiceTier1Target}; +use crate::service_artifact::{HardenedServiceArtifact, ServiceArtifactSha256}; use radroots_runtime_paths::ServiceId; pub const RUNTIME_DISTRIBUTION_SCHEMA: &str = "radroots-runtime-distribution"; @@ -21,6 +22,124 @@ pub struct ResolvedServiceTarget { target: ServiceTier1Target, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ServiceArtifactRequest<'a> { + pub service_id: &'a ServiceId, + pub target_id: &'a str, +} + +/// Exact native release artifact metadata for one hardened service target. +#[derive(Clone, PartialEq, Eq)] +pub struct ResolvedServiceArtifact { + service_id: ServiceId, + target: ServiceTier1Target, + version: String, + package_name: String, + binary_name: String, + channel: String, + binary_archive_name: String, + binary_archive_format: &'static str, + binary_archive_member: String, + artifact_manifest_name: String, + checksums_name: String, + checksum_algorithm: &'static str, + checksum_format: &'static str, + release_contract_sha256: ServiceArtifactSha256, + source_lock_sha256: ServiceArtifactSha256, + output_inventory: Vec<String>, +} + +impl core::fmt::Debug for ResolvedServiceArtifact { + fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + formatter + .debug_struct("ResolvedServiceArtifact") + .field("service_id", &self.service_id) + .field("target", &self.target) + .field("version", &self.version) + .field("package_name", &self.package_name) + .field("binary_name", &self.binary_name) + .field("channel", &self.channel) + .field("binary_archive_name", &self.binary_archive_name) + .field("binary_archive_format", &self.binary_archive_format) + .field("binary_archive_member", &self.binary_archive_member) + .field("artifact_manifest_name", &self.artifact_manifest_name) + .field("checksums_name", &self.checksums_name) + .field("checksum_algorithm", &self.checksum_algorithm) + .field("checksum_format", &self.checksum_format) + .field("release_contract_sha256", &"<redacted>") + .field("source_lock_sha256", &"<redacted>") + .field("output_inventory", &self.output_inventory) + .finish() + } +} + +impl ResolvedServiceArtifact { + #[must_use] + pub fn service_id(&self) -> &ServiceId { + &self.service_id + } + #[must_use] + pub const fn target(&self) -> ServiceTier1Target { + self.target + } + #[must_use] + pub fn version(&self) -> &str { + &self.version + } + #[must_use] + pub fn package_name(&self) -> &str { + &self.package_name + } + #[must_use] + pub fn binary_name(&self) -> &str { + &self.binary_name + } + #[must_use] + pub fn channel(&self) -> &str { + &self.channel + } + #[must_use] + pub fn binary_archive_name(&self) -> &str { + &self.binary_archive_name + } + #[must_use] + pub const fn binary_archive_format(&self) -> &'static str { + self.binary_archive_format + } + #[must_use] + pub fn binary_archive_member(&self) -> &str { + &self.binary_archive_member + } + #[must_use] + pub fn artifact_manifest_name(&self) -> &str { + &self.artifact_manifest_name + } + #[must_use] + pub fn checksums_name(&self) -> &str { + &self.checksums_name + } + #[must_use] + pub const fn checksum_algorithm(&self) -> &'static str { + self.checksum_algorithm + } + #[must_use] + pub const fn checksum_format(&self) -> &'static str { + self.checksum_format + } + #[must_use] + pub const fn release_contract_sha256(&self) -> ServiceArtifactSha256 { + self.release_contract_sha256 + } + #[must_use] + pub const fn source_lock_sha256(&self) -> ServiceArtifactSha256 { + self.source_lock_sha256 + } + #[must_use] + pub fn output_inventory(&self) -> &[String] { + &self.output_inventory + } +} + impl ResolvedServiceTarget { #[must_use] pub fn service_id(&self) -> &ServiceId { @@ -90,7 +209,31 @@ impl RadrootsRuntimeDistributionResolver { .iter() .any(|(_, service)| runtime.id == service.service_id().as_str()) }) { - return Err(RadrootsRuntimeDistributionError::HardenedServiceArtifactDeferred); + return Err(RadrootsRuntimeDistributionError::HardenedServiceLegacyArtifactRow); + } + if !contract + .channels + .active + .iter() + .any(|channel| channel == "stable") + || !contract + .channels + .defined + .iter() + .any(|channel| channel == "stable") + { + return Err(RadrootsRuntimeDistributionError::InvalidServiceArtifactContract); + } + for (service_id, target) in contract.service_targets.iter() { + let artifact = contract + .service_artifacts + .get(target.service_id()) + .ok_or(RadrootsRuntimeDistributionError::InvalidServiceArtifactContract)?; + if service_id != artifact.service_id().as_str() + || target.tier_1_targets() != artifact.tier_1_targets() + { + return Err(RadrootsRuntimeDistributionError::InvalidServiceArtifactContract); + } } Ok(Self { contract }) } @@ -123,6 +266,47 @@ impl RadrootsRuntimeDistributionResolver { }) } + pub fn service_artifact( + &self, + service_id: &ServiceId, + ) -> Result<&HardenedServiceArtifact, RadrootsRuntimeDistributionError> { + self.contract + .service_artifacts + .get(service_id) + .ok_or(RadrootsRuntimeDistributionError::UnsupportedService) + } + + pub fn resolve_service_artifact( + &self, + request: &ServiceArtifactRequest<'_>, + ) -> Result<ResolvedServiceArtifact, RadrootsRuntimeDistributionError> { + let artifact = self.service_artifact(request.service_id)?; + let target = ServiceTier1Target::parse(request.target_id) + .filter(|target| artifact.tier_1_targets().contains(target)) + .ok_or(RadrootsRuntimeDistributionError::UnsupportedServiceTarget)?; + let service = artifact.service_id().as_str(); + let version = artifact.version(); + let binary = artifact.binary_name(); + Ok(ResolvedServiceArtifact { + service_id: request.service_id.clone(), + target, + version: version.to_owned(), + package_name: artifact.package_name().to_owned(), + binary_name: binary.to_owned(), + channel: artifact.channel().as_str().to_owned(), + binary_archive_name: artifact.binary_archive_name().to_owned(), + binary_archive_format: "tar.gz", + binary_archive_member: format!("{service}-{version}-{}/{binary}", target.as_str()), + artifact_manifest_name: artifact.artifact_manifest_name().to_owned(), + checksums_name: artifact.checksums_name().to_owned(), + checksum_algorithm: "sha256", + checksum_format: "sha256_lower_hex_two_spaces_path_lf_sorted_by_path", + release_contract_sha256: artifact.release_contract_sha256(), + source_lock_sha256: artifact.source_lock_sha256(), + output_inventory: artifact.output_inventory().to_vec(), + }) + } + pub fn resolve_artifact( &self, request: &RuntimeArtifactRequest<'_>, diff --git a/crates/runtime_distribution/src/service_artifact.rs b/crates/runtime_distribution/src/service_artifact.rs @@ -0,0 +1,311 @@ +use std::collections::BTreeMap; + +use radroots_runtime_paths::ServiceId; +use serde::{Deserialize, Deserializer}; + +use crate::service::ServiceTier1Target; + +const OUTPUT_INVENTORY: [&str; 12] = [ + "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", +]; + +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum ServiceArtifactChannel { + Stable, +} + +impl ServiceArtifactChannel { + #[must_use] + pub const fn as_str(self) -> &'static str { + match self { + Self::Stable => "stable", + } + } +} + +/// A validated lowercase SHA-256 value. +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct ServiceArtifactSha256([u8; 32]); + +impl ServiceArtifactSha256 { + #[must_use] + pub const fn from_bytes(bytes: [u8; 32]) -> Self { + Self(bytes) + } + #[must_use] + pub const fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } +} + +impl core::fmt::Debug for ServiceArtifactSha256 { + fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + formatter.write_str("ServiceArtifactSha256(<redacted>)") + } +} + +impl<'de> Deserialize<'de> for ServiceArtifactSha256 { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + let value = String::deserialize(deserializer)?; + if value.len() != 64 + || !value + .bytes() + .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) + { + return Err(serde::de::Error::custom("invalid sha256")); + } + let mut bytes = [0_u8; 32]; + for (index, pair) in value.as_bytes().chunks_exact(2).enumerate() { + bytes[index] = (hex_nibble(pair[0]) + .ok_or_else(|| serde::de::Error::custom("invalid sha256"))? + << 4) + | hex_nibble(pair[1]).ok_or_else(|| serde::de::Error::custom("invalid sha256"))?; + } + Ok(Self(bytes)) + } +} + +fn hex_nibble(byte: u8) -> Option<u8> { + match byte { + b'0'..=b'9' => Some(byte - b'0'), + b'a'..=b'f' => Some(byte - b'a' + 10), + _ => None, + } +} + +#[derive(Clone, Deserialize, PartialEq, Eq)] +#[serde(try_from = "HardenedServiceArtifactWire")] +pub struct HardenedServiceArtifact { + service_id: ServiceId, + service_revision: String, + release_contract: String, + release_contract_sha256: ServiceArtifactSha256, + source_lock_sha256: ServiceArtifactSha256, + package_name: String, + binary_name: String, + version: String, + channel: ServiceArtifactChannel, + binary_archive_name: String, + artifact_manifest_name: String, + checksums_name: String, + output_inventory: Vec<String>, + tier_1_targets: Vec<ServiceTier1Target>, +} + +impl core::fmt::Debug for HardenedServiceArtifact { + fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + formatter.write_str("HardenedServiceArtifact(<redacted>)") + } +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct HardenedServiceArtifactWire { + service_id: ServiceId, + service_revision: String, + release_contract: String, + release_contract_sha256: ServiceArtifactSha256, + source_lock_sha256: ServiceArtifactSha256, + package_name: String, + binary_name: String, + version: String, + channel: ServiceArtifactChannel, + binary_archive_name: String, + artifact_manifest_name: String, + checksums_name: String, + output_inventory: Vec<String>, + tier_1_targets: Vec<ServiceTier1Target>, +} + +impl HardenedServiceArtifact { + #[must_use] + pub fn service_id(&self) -> &ServiceId { + &self.service_id + } + #[must_use] + pub fn service_revision(&self) -> &str { + &self.service_revision + } + #[must_use] + pub fn release_contract(&self) -> &str { + &self.release_contract + } + #[must_use] + pub const fn release_contract_sha256(&self) -> ServiceArtifactSha256 { + self.release_contract_sha256 + } + #[must_use] + pub const fn source_lock_sha256(&self) -> ServiceArtifactSha256 { + self.source_lock_sha256 + } + #[must_use] + pub fn package_name(&self) -> &str { + &self.package_name + } + #[must_use] + pub fn binary_name(&self) -> &str { + &self.binary_name + } + #[must_use] + pub fn version(&self) -> &str { + &self.version + } + #[must_use] + pub const fn channel(&self) -> ServiceArtifactChannel { + self.channel + } + #[must_use] + pub fn binary_archive_name(&self) -> &str { + &self.binary_archive_name + } + #[must_use] + pub fn artifact_manifest_name(&self) -> &str { + &self.artifact_manifest_name + } + #[must_use] + pub fn checksums_name(&self) -> &str { + &self.checksums_name + } + #[must_use] + pub fn output_inventory(&self) -> &[String] { + &self.output_inventory + } + #[must_use] + pub fn tier_1_targets(&self) -> &[ServiceTier1Target] { + &self.tier_1_targets + } + + fn has_exact_contract(&self) -> bool { + let (revision, release_contract, release_hash, source_lock_hash) = + match self.service_id.as_str() { + "myc" => ( + "77b381648ed1e586efb696888beb05b9215c69cf", + "contracts/services_hardening/native_release.v2.json", + "4b3ba5789fac6aa219e84e1e5c002cf8230b72f95fd6d95a6419d2fdf2915f83", + "f5ebb390a480830d51d502facc623bd1b10eda27b12dad9f3dbb6a1f1f949217", + ), + "rhi" => ( + "07aa6ea988da5372654bb3d1ee183ac099a77cae", + "contracts/services_hardening/native_release.v1.json", + "06a973176b4b8c11dad13000604576527df829dd0bbe2f501158662f75e70b94", + "3cc8bfac0d98730937754abae2ccfe20e40d0a9bbdefe02ebd94264c20f0d0ff", + ), + _ => return false, + }; + self.service_revision == revision + && self.release_contract == release_contract + && self.release_contract_sha256 == sha256_literal(release_hash) + && self.source_lock_sha256 == sha256_literal(source_lock_hash) + && self.package_name == self.service_id.as_str() + && self.binary_name == self.service_id.as_str() + && self.version == "0.1.0" + && self.channel == ServiceArtifactChannel::Stable + && self.binary_archive_name == "binary.tar.gz" + && self.artifact_manifest_name == "artifact-manifest.v1.json" + && self.checksums_name == "SHA256SUMS" + && self + .output_inventory + .iter() + .map(String::as_str) + .eq(OUTPUT_INVENTORY) + && self.tier_1_targets == ServiceTier1Target::ALL + } +} + +fn sha256_literal(value: &str) -> ServiceArtifactSha256 { + let mut bytes = [0_u8; 32]; + for (index, pair) in value.as_bytes().chunks_exact(2).enumerate() { + bytes[index] = (hex_nibble(pair[0]).expect("literal sha256") << 4) + | hex_nibble(pair[1]).expect("literal sha256"); + } + ServiceArtifactSha256(bytes) +} + +impl TryFrom<HardenedServiceArtifactWire> for HardenedServiceArtifact { + type Error = &'static str; + + fn try_from(wire: HardenedServiceArtifactWire) -> Result<Self, Self::Error> { + let artifact = Self { + service_id: wire.service_id, + service_revision: wire.service_revision, + release_contract: wire.release_contract, + release_contract_sha256: wire.release_contract_sha256, + source_lock_sha256: wire.source_lock_sha256, + package_name: wire.package_name, + binary_name: wire.binary_name, + version: wire.version, + channel: wire.channel, + binary_archive_name: wire.binary_archive_name, + artifact_manifest_name: wire.artifact_manifest_name, + checksums_name: wire.checksums_name, + output_inventory: wire.output_inventory, + tier_1_targets: wire.tier_1_targets, + }; + artifact + .has_exact_contract() + .then_some(artifact) + .ok_or("invalid hardened service artifact") + } +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq)] +#[serde(try_from = "BTreeMap<String, HardenedServiceArtifact>")] +pub struct HardenedServiceArtifacts(BTreeMap<String, HardenedServiceArtifact>); + +impl HardenedServiceArtifacts { + #[must_use] + pub fn get(&self, service_id: &ServiceId) -> Option<&HardenedServiceArtifact> { + self.0.get(service_id.as_str()) + } + pub fn iter(&self) -> impl ExactSizeIterator<Item = (&str, &HardenedServiceArtifact)> { + self.0.iter().map(|(key, value)| (key.as_str(), value)) + } + #[must_use] + pub fn len(&self) -> usize { + self.0.len() + } + #[must_use] + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +impl TryFrom<BTreeMap<String, HardenedServiceArtifact>> for HardenedServiceArtifacts { + type Error = &'static str; + + fn try_from(artifacts: BTreeMap<String, HardenedServiceArtifact>) -> Result<Self, Self::Error> { + if artifacts.len() != 2 { + return Err("service artifact inventory must contain exactly Myc and RHI"); + } + for service in ["myc", "rhi"] { + let artifact = artifacts + .get(service) + .ok_or("service artifact inventory is incomplete")?; + if artifact.service_id.as_str() != service || !artifact.has_exact_contract() { + return Err("service artifact inventory is invalid"); + } + } + if artifacts + .iter() + .any(|(key, artifact)| key != artifact.service_id.as_str()) + { + return Err("service artifact key mismatch"); + } + Ok(Self(artifacts)) + } +} diff --git a/crates/runtime_distribution/tests/fixtures/hardened_service_targets.v1.toml b/crates/runtime_distribution/tests/fixtures/hardened_service_targets.v1.toml @@ -13,8 +13,8 @@ version_resolution = "runtime_scoped_channel_latest" artifact_verification_required = true [channels] -active = [] -defined = [] +active = ["stable"] +defined = ["stable"] [service_targets.myc] service_id = "myc" @@ -43,3 +43,35 @@ status_surface = "local_admin_service_status_v1" operations_surface = "cached_livez_readyz_metrics" support_posture = "target" tier_1_targets = ["x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu"] + +[service_artifacts.myc] +service_id = "myc" +service_revision = "77b381648ed1e586efb696888beb05b9215c69cf" +release_contract = "contracts/services_hardening/native_release.v2.json" +release_contract_sha256 = "4b3ba5789fac6aa219e84e1e5c002cf8230b72f95fd6d95a6419d2fdf2915f83" +source_lock_sha256 = "f5ebb390a480830d51d502facc623bd1b10eda27b12dad9f3dbb6a1f1f949217" +package_name = "myc" +binary_name = "myc" +version = "0.1.0" +channel = "stable" +binary_archive_name = "binary.tar.gz" +artifact_manifest_name = "artifact-manifest.v1.json" +checksums_name = "SHA256SUMS" +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"] +tier_1_targets = ["x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu"] + +[service_artifacts.rhi] +service_id = "rhi" +service_revision = "07aa6ea988da5372654bb3d1ee183ac099a77cae" +release_contract = "contracts/services_hardening/native_release.v1.json" +release_contract_sha256 = "06a973176b4b8c11dad13000604576527df829dd0bbe2f501158662f75e70b94" +source_lock_sha256 = "3cc8bfac0d98730937754abae2ccfe20e40d0a9bbdefe02ebd94264c20f0d0ff" +package_name = "rhi" +binary_name = "rhi" +version = "0.1.0" +channel = "stable" +binary_archive_name = "binary.tar.gz" +artifact_manifest_name = "artifact-manifest.v1.json" +checksums_name = "SHA256SUMS" +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"] +tier_1_targets = ["x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu"] diff --git a/crates/runtime_distribution/tests/package_boundary.rs b/crates/runtime_distribution/tests/package_boundary.rs @@ -1,21 +1,20 @@ const SERVICE_SOURCE: &str = include_str!("../src/service.rs"); +const SERVICE_ARTIFACT_SOURCE: &str = include_str!("../src/service_artifact.rs"); const RESOLVER_SOURCE: &str = include_str!("../src/resolve.rs"); const ROOT_SOURCE: &str = include_str!("../src/lib.rs"); const SERVICE_FIXTURE: &str = include_str!("fixtures/hardened_service_targets.v1.toml"); #[test] -fn hardened_service_metadata_has_no_artifact_or_runtime_authority() { +fn hardened_service_artifacts_are_closed_metadata_without_runtime_authority() { for forbidden in [ - "binary_name", - "package_name", "artifact_adapter", "default_channel", "[[runtime]]", "qualified", ] { assert!( - !SERVICE_SOURCE.contains(forbidden) && !SERVICE_FIXTURE.contains(forbidden), - "hardened service metadata contains deferred authority `{forbidden}`" + !SERVICE_ARTIFACT_SOURCE.contains(forbidden) && !SERVICE_FIXTURE.contains(forbidden), + "hardened service artifact metadata contains forbidden authority `{forbidden}`" ); } @@ -29,10 +28,25 @@ fn hardened_service_metadata_has_no_artifact_or_runtime_authority() { "TcpListener", ] { assert!( - !SERVICE_SOURCE.contains(forbidden), + !SERVICE_SOURCE.contains(forbidden) && !SERVICE_ARTIFACT_SOURCE.contains(forbidden), "service metadata owns forbidden runtime behavior `{forbidden}`" ); } + + for required in [ + "service_artifacts.myc", + "service_artifacts.rhi", + "binary.tar.gz", + "artifact-manifest.v1.json", + "SHA256SUMS", + "service-source.tar.gz", + "sbom.cdx.json", + "provenance-input.v1.json", + ] { + assert!(SERVICE_FIXTURE.contains(required), "missing `{required}`"); + } + assert!(ROOT_SOURCE.contains("mod service_artifact;")); + assert!(!ROOT_SOURCE.contains("pub mod service_artifact;")); } #[test]