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:
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]