lib

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

service_release_artifacts.rs (191247B)


      1 use std::{
      2     collections::{BTreeMap, BTreeSet},
      3     ffi::OsStr,
      4     fmt, fs,
      5     io::{Read as _, Write as _},
      6     path::{Component, Path, PathBuf},
      7     process::{Command, Stdio},
      8 };
      9 
     10 use flate2::{Compression, GzBuilder};
     11 use serde::{Deserialize, Serialize};
     12 use sha2::{Digest as _, Sha256};
     13 use tar::{Builder as TarBuilder, Header as TarHeader};
     14 use tempfile::TempDir;
     15 
     16 use crate::safe_artifact_io::{TarGzipLimits, TraversalLimits};
     17 use crate::service_source_lock::{LIB_REPOSITORY, validate_deferred_nix_material};
     18 use crate::service_source_lock_v3::{
     19     LOCK_FILENAME, PREDECESSOR_LOCK_FILENAME, ServiceSourceLockV3,
     20 };
     21 use crate::{artifact_admission, exact_tree_archive, safe_artifact_io};
     22 
     23 const CONTRACT_RELATIVE: &str =
     24     "contracts/architecture/decisions/services_hardening_release_artifacts.v4.json";
     25 const INPUT_NAMES: [&str; 6] = [
     26     "config.example.toml",
     27     "config.schema.json",
     28     "nixos-module.nix",
     29     "oci-image.tar.gz",
     30     "service-binary",
     31     "systemd.service",
     32 ];
     33 const OUTPUT_NAMES: [&str; 20] = [
     34     "LICENSE-APACHE",
     35     "LICENSE-MIT",
     36     "SHA256SUMS",
     37     "THIRD-PARTY-LICENSES.txt",
     38     "THIRD-PARTY-NOTICES.txt",
     39     "artifact-manifest.v2.json",
     40     "artifact-scan.v1.json",
     41     "binary.tar.gz",
     42     "config.example.toml",
     43     "config.schema.json",
     44     "lib-source.tar",
     45     "nixos-module.nix",
     46     "oci-image.tar.gz",
     47     "oci-image.v1.json",
     48     "provenance.intoto.jsonl",
     49     "radroots.service.source-lock.v3.toml",
     50     "sbom.cdx.json",
     51     "service-source.tar",
     52     "source-archives.v3.json",
     53     "systemd.service",
     54 ];
     55 const SUPPORTED_TARGETS: [&str; 2] = ["aarch64-apple-darwin", "x86_64-unknown-linux-gnu"];
     56 const SECRET_PATTERNS: [&[u8]; 7] = [
     57     b"-----BEGIN PRIVATE KEY-----",
     58     b"-----BEGIN RSA PRIVATE KEY-----",
     59     b"-----BEGIN EC PRIVATE KEY-----",
     60     b"-----BEGIN OPENSSH PRIVATE KEY-----",
     61     b"github_pat_",
     62     b"ghp_",
     63     b"xoxb-",
     64 ];
     65 const MAX_CONTRACT_BYTES: usize = 65_536;
     66 const MAX_MANIFEST_BYTES: usize = 1_048_576;
     67 const MAX_TEXT_INPUT_BYTES: u64 = 1_048_576;
     68 const MAX_GENERATED_DOCUMENT_BYTES: u64 = 16_777_216;
     69 const MAX_SOURCE_LOCK_BYTES: u64 = 4_096;
     70 const MAX_SERVICE_CARGO_LOCK_BYTES: u64 = 16_777_216;
     71 const MAX_SERVICE_FLAKE_LOCK_BYTES: u64 = 4_194_304;
     72 const MAX_BINARY_BYTES: u64 = 536_870_912;
     73 const MAX_SOURCE_ARCHIVE_BYTES: u64 = 1_073_741_824;
     74 const MAX_SOURCE_ARCHIVE_MEMBER_BYTES: u64 = 67_108_864;
     75 const MAX_SOURCE_ARCHIVE_MEMBERS: u64 = 65_536;
     76 const MAX_OCI_BYTES: u64 = 2_147_483_648;
     77 const MAX_METADATA_BYTES: usize = 33_554_432;
     78 const MAX_GIT_OUTPUT_BYTES: usize = 65_536;
     79 const MAX_PACKAGES: usize = 8_192;
     80 const MAX_WORKSPACE_PACKAGES: usize = 64;
     81 const MAX_TEXT_FIELD_BYTES: usize = 512;
     82 const MAX_ARCHIVE_EXPANDED_BYTES: u64 = 17_179_869_184;
     83 const MAX_ARCHIVE_PATH_BYTES: usize = 4_096;
     84 const MAX_RELEASE_TREE_BYTES: u64 = 68_719_476_736;
     85 const FILE_MODE: u32 = 0o644;
     86 const DIRECTORY_MODE: u32 = 0o755;
     87 const CANDIDATE_DIGEST_DOMAIN: &str = "sha256:";
     88 const SECRET_SCAN_OVERLAP_BYTES: usize = 131_072;
     89 
     90 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
     91 pub(crate) enum CommandMode {
     92     Check,
     93     Write,
     94 }
     95 
     96 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
     97 enum ReleaseArtifactError {
     98     InvalidContract,
     99     InvalidServiceRoot,
    100     DirtyServiceSource,
    101     InvalidServiceMetadata,
    102     InvalidInputRoot,
    103     InvalidInputArtifact,
    104     InvalidSourceLock,
    105     InvalidSourceBundle,
    106     InvalidPackageInventory,
    107     ProtectedMaterialDetected,
    108     InvalidOutputRoot,
    109     StaleOutput,
    110     GenerationFailure,
    111 }
    112 
    113 impl ReleaseArtifactError {
    114     const fn code(self) -> &'static str {
    115         match self {
    116             Self::InvalidContract => "invalid_contract",
    117             Self::InvalidServiceRoot => "invalid_service_root",
    118             Self::DirtyServiceSource => "dirty_service_source",
    119             Self::InvalidServiceMetadata => "invalid_service_metadata",
    120             Self::InvalidInputRoot => "invalid_input_root",
    121             Self::InvalidInputArtifact => "invalid_input_artifact",
    122             Self::InvalidSourceLock => "invalid_source_lock",
    123             Self::InvalidSourceBundle => "invalid_source_bundle",
    124             Self::InvalidPackageInventory => "invalid_package_inventory",
    125             Self::ProtectedMaterialDetected => "protected_material_detected",
    126             Self::InvalidOutputRoot => "invalid_output_root",
    127             Self::StaleOutput => "stale_output",
    128             Self::GenerationFailure => "generation_failure",
    129         }
    130     }
    131 }
    132 
    133 impl fmt::Display for ReleaseArtifactError {
    134     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    135         formatter.write_str(match self {
    136             Self::InvalidContract => "service release artifact contract is invalid",
    137             Self::InvalidServiceRoot => "service release source root is invalid",
    138             Self::DirtyServiceSource => "service release source contains an ungoverned change",
    139             Self::InvalidServiceMetadata => "service release metadata is invalid",
    140             Self::InvalidInputRoot => "service release input root is invalid",
    141             Self::InvalidInputArtifact => "service release input artifact is invalid",
    142             Self::InvalidSourceLock => "service release source lock is invalid",
    143             Self::InvalidSourceBundle => "service release source bundle is invalid",
    144             Self::InvalidPackageInventory => "service release package inventory is invalid",
    145             Self::ProtectedMaterialDetected => "service release input contains protected material",
    146             Self::InvalidOutputRoot => "service release output root is invalid",
    147             Self::StaleOutput => "service release artifact set is stale",
    148             Self::GenerationFailure => "service release artifacts could not be generated",
    149         })
    150     }
    151 }
    152 
    153 impl std::error::Error for ReleaseArtifactError {}
    154 
    155 #[derive(Debug, Deserialize)]
    156 #[serde(deny_unknown_fields)]
    157 struct ReleaseMetadata {
    158     service: String,
    159     service_package: String,
    160     binary_name: String,
    161     version: String,
    162     #[serde(skip)]
    163     license: String,
    164 }
    165 
    166 #[derive(Debug, Deserialize)]
    167 struct CargoMetadata {
    168     packages: Vec<CargoPackage>,
    169     workspace_members: Vec<String>,
    170     resolve: Option<CargoResolve>,
    171 }
    172 
    173 #[derive(Debug, Deserialize)]
    174 struct CargoPackage {
    175     id: String,
    176     name: String,
    177     version: String,
    178     source: Option<String>,
    179     checksum: Option<String>,
    180     license: Option<String>,
    181     manifest_path: String,
    182     license_file: Option<String>,
    183     targets: Vec<CargoTarget>,
    184     #[serde(skip)]
    185     license_texts: Vec<DependencyLicenseText>,
    186 }
    187 
    188 #[derive(Clone, Debug)]
    189 struct DependencyLicenseText {
    190     filename: String,
    191     sha256: String,
    192     text: String,
    193 }
    194 
    195 #[derive(Debug, Deserialize)]
    196 struct CargoTarget {
    197     name: String,
    198     kind: Vec<String>,
    199 }
    200 
    201 #[derive(Debug, Deserialize)]
    202 struct CargoResolve {
    203     nodes: Vec<CargoNode>,
    204 }
    205 
    206 #[derive(Debug, Deserialize)]
    207 struct CargoNode {
    208     id: String,
    209     dependencies: Vec<String>,
    210 }
    211 
    212 #[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
    213 struct DigestValue {
    214     alg: &'static str,
    215     content: String,
    216 }
    217 
    218 #[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
    219 struct LicenseChoice {
    220     expression: String,
    221 }
    222 
    223 #[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
    224 struct SbomComponent {
    225     #[serde(rename = "type")]
    226     component_type: &'static str,
    227     #[serde(rename = "bom-ref")]
    228     bom_ref: String,
    229     name: String,
    230     version: String,
    231     #[serde(skip_serializing_if = "Option::is_none")]
    232     purl: Option<String>,
    233     licenses: Vec<LicenseChoice>,
    234     #[serde(skip_serializing_if = "Vec::is_empty")]
    235     hashes: Vec<DigestValue>,
    236     #[serde(skip_serializing_if = "Vec::is_empty")]
    237     properties: Vec<SbomProperty>,
    238 }
    239 
    240 #[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
    241 struct SbomProperty {
    242     name: String,
    243     value: String,
    244 }
    245 
    246 #[derive(Clone, Debug, Eq, PartialEq, Serialize)]
    247 struct SbomDependency {
    248     #[serde(rename = "ref")]
    249     reference: String,
    250     depends_on: Vec<String>,
    251 }
    252 
    253 #[derive(Debug, Serialize)]
    254 struct SbomMetadata {
    255     component: SbomComponent,
    256     properties: Vec<SbomProperty>,
    257 }
    258 
    259 #[derive(Debug, Serialize)]
    260 struct SbomComposition {
    261     aggregate: &'static str,
    262     assemblies: Vec<String>,
    263     dependencies: Vec<String>,
    264 }
    265 
    266 #[derive(Debug, Serialize)]
    267 struct CycloneDxSbom {
    268     #[serde(rename = "$schema")]
    269     json_schema: &'static str,
    270     #[serde(rename = "bomFormat")]
    271     bom_format: &'static str,
    272     #[serde(rename = "specVersion")]
    273     spec_version: &'static str,
    274     version: u32,
    275     metadata: SbomMetadata,
    276     components: Vec<SbomComponent>,
    277     dependencies: Vec<SbomDependency>,
    278     compositions: Vec<SbomComposition>,
    279 }
    280 
    281 #[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
    282 struct ArtifactRecord {
    283     path: String,
    284     byte_length: u64,
    285     sha256: String,
    286 }
    287 
    288 #[derive(Debug, Serialize)]
    289 struct ContractVersionsDocument {
    290     config: u32,
    291     state: u32,
    292     admin: u32,
    293     status: u32,
    294     provider: u32,
    295 }
    296 
    297 #[derive(Debug, Serialize)]
    298 struct SourceArchiveDocument {
    299     schema: &'static str,
    300     contract_version: u32,
    301     candidate_digest: String,
    302     service: String,
    303     service_revision: String,
    304     lib_repository: &'static str,
    305     lib_revision: String,
    306     service_source: ArtifactRecord,
    307     lib_source: ArtifactRecord,
    308     source_lock_sha256: String,
    309     workspace_catalog_sha256: String,
    310     cargo_lock_sha256: String,
    311     flake_lock_sha256: String,
    312     format: &'static str,
    313     compression: &'static str,
    314     git_history: &'static str,
    315 }
    316 
    317 #[derive(Debug, Serialize)]
    318 struct OciImageDocument {
    319     schema: &'static str,
    320     contract_version: u32,
    321     service: String,
    322     version: String,
    323     target: String,
    324     image: ArtifactRecord,
    325 }
    326 
    327 #[derive(Debug, Serialize)]
    328 struct ArtifactManifestDocument {
    329     schema: &'static str,
    330     contract_version: u32,
    331     candidate_digest: String,
    332     service: String,
    333     version: String,
    334     target: String,
    335     source_date_epoch: u32,
    336     service_revision: String,
    337     lib_revision: String,
    338     rust_version: &'static str,
    339     host_feature_profile: &'static str,
    340     contract_versions: ContractVersionsDocument,
    341     confidentiality: ConfidentialityDocument,
    342     artifacts: Vec<ArtifactRecord>,
    343 }
    344 
    345 #[derive(Debug, Serialize)]
    346 struct ConfidentialityDocument {
    347     state: &'static str,
    348     derived_from: ArtifactRecord,
    349     protected_material_included: bool,
    350 }
    351 
    352 #[derive(Debug, Serialize)]
    353 struct ScanDocument {
    354     schema: &'static str,
    355     contract_version: u32,
    356     candidate_digest: String,
    357     state: &'static str,
    358     ruleset_sha256: String,
    359     scanned_artifacts: Vec<ArtifactRecord>,
    360     nested_members_scanned: u64,
    361     expanded_bytes_scanned: u64,
    362 }
    363 
    364 #[derive(Debug, Serialize)]
    365 struct InTotoStatement {
    366     #[serde(rename = "_type")]
    367     statement_type: &'static str,
    368     subject: Vec<InTotoSubject>,
    369     #[serde(rename = "predicateType")]
    370     predicate_type: &'static str,
    371     predicate: SlsaPredicate,
    372 }
    373 
    374 #[derive(Debug, Serialize)]
    375 struct InTotoSubject {
    376     name: String,
    377     digest: BTreeMap<&'static str, String>,
    378 }
    379 
    380 #[derive(Debug, Serialize)]
    381 struct SlsaPredicate {
    382     #[serde(rename = "buildDefinition")]
    383     build_definition: SlsaBuildDefinition,
    384     #[serde(rename = "runDetails")]
    385     run_details: SlsaRunDetails,
    386 }
    387 
    388 #[derive(Debug, Serialize)]
    389 struct SlsaBuildDefinition {
    390     #[serde(rename = "buildType")]
    391     build_type: &'static str,
    392     #[serde(rename = "externalParameters")]
    393     external_parameters: SlsaExternalParameters,
    394     #[serde(rename = "internalParameters")]
    395     internal_parameters: BTreeMap<String, String>,
    396     #[serde(rename = "resolvedDependencies")]
    397     resolved_dependencies: Vec<SlsaResolvedDependency>,
    398 }
    399 
    400 #[derive(Debug, Serialize)]
    401 struct SlsaExternalParameters {
    402     candidate_digest: String,
    403     service: String,
    404     target: String,
    405     source_date_epoch: u32,
    406 }
    407 
    408 #[derive(Debug, Serialize)]
    409 struct SlsaResolvedDependency {
    410     uri: String,
    411     digest: BTreeMap<&'static str, String>,
    412 }
    413 
    414 #[derive(Debug, Serialize)]
    415 struct SlsaRunDetails {
    416     builder: SlsaBuilder,
    417     metadata: SlsaRunMetadata,
    418 }
    419 
    420 #[derive(Debug, Serialize)]
    421 struct SlsaBuilder {
    422     id: &'static str,
    423 }
    424 
    425 #[derive(Debug, Serialize)]
    426 struct SlsaRunMetadata {
    427     #[serde(rename = "invocationId")]
    428     invocation_id: String,
    429 }
    430 
    431 #[derive(Clone, Debug)]
    432 struct FileEvidence {
    433     byte_length: u64,
    434     sha256: String,
    435 }
    436 
    437 #[derive(Debug, Deserialize)]
    438 #[serde(deny_unknown_fields)]
    439 struct ReleaseDecision {
    440     schema: String,
    441     contract_version: u32,
    442     decision_state: String,
    443     owner_step: u32,
    444     predecessor: ReleasePredecessor,
    445     command: String,
    446     modes: Vec<String>,
    447     required_arguments: Vec<String>,
    448     service_metadata_path: String,
    449     service_metadata_fields: Vec<String>,
    450     service_license_path: String,
    451     source_lock_schema: String,
    452     source_lock_definition: String,
    453     artifact_contract_binding: String,
    454     artifact_admission_contract: String,
    455     supported_targets: Vec<String>,
    456     candidate_binding: String,
    457     lib_root_binding: String,
    458     binary_admission: String,
    459     oci_admission: String,
    460     input_inventory: Vec<String>,
    461     excluded_parent_owned_inputs: Vec<String>,
    462     service_root_inventory: Vec<String>,
    463     output_inventory: Vec<String>,
    464     canonical_json: String,
    465     checksum_format: String,
    466     source_archive_format: String,
    467     sbom_format: String,
    468     license_evidence: String,
    469     provenance_posture: String,
    470     protected_material_scan_scope: String,
    471     confidentiality_state: String,
    472     source_cleanliness: String,
    473     revision_stability: String,
    474     no_protected_material: bool,
    475     maximums: ReleaseMaximums,
    476     required_negative_vectors: Vec<String>,
    477     negative_error_codes: Vec<String>,
    478 }
    479 
    480 #[derive(Debug, Deserialize)]
    481 #[serde(deny_unknown_fields)]
    482 struct ReleasePredecessor {
    483     schema: String,
    484     filename: String,
    485     transition: String,
    486 }
    487 
    488 #[derive(Debug, Deserialize)]
    489 #[serde(deny_unknown_fields)]
    490 struct ReleaseMaximums {
    491     text_input_bytes: u64,
    492     generated_document_bytes: u64,
    493     service_cargo_lock_bytes: u64,
    494     service_flake_lock_bytes: u64,
    495     binary_bytes: u64,
    496     source_archive_bytes: u64,
    497     source_archive_member_bytes: u64,
    498     source_archive_members: u64,
    499     oci_bytes: u64,
    500     artifact_scan_expanded_bytes: u64,
    501     cargo_metadata_bytes: usize,
    502     packages: usize,
    503     workspace_packages: usize,
    504 }
    505 
    506 #[derive(Clone, Copy)]
    507 pub(crate) struct Arguments<'a> {
    508     pub(crate) mode: CommandMode,
    509     pub(crate) service_root: &'a Path,
    510     pub(crate) lib_root: &'a Path,
    511     pub(crate) input_root: &'a Path,
    512     pub(crate) output_root: &'a Path,
    513     pub(crate) target: &'a str,
    514     pub(crate) source_date_epoch: u32,
    515     pub(crate) candidate_digest: &'a str,
    516 }
    517 
    518 pub(crate) fn run(arguments: Arguments<'_>) -> Result<(), String> {
    519     run_inner(arguments).map_err(|error| error.to_string())
    520 }
    521 
    522 fn run_inner(arguments: Arguments<'_>) -> Result<(), ReleaseArtifactError> {
    523     let Arguments {
    524         mode,
    525         service_root,
    526         lib_root,
    527         input_root,
    528         output_root,
    529         target,
    530         source_date_epoch,
    531         candidate_digest,
    532     } = arguments;
    533     if !SUPPORTED_TARGETS.contains(&target)
    534         || source_date_epoch == 0
    535         || !valid_lower_hex(candidate_digest, 64)
    536     {
    537         return Err(ReleaseArtifactError::InvalidServiceMetadata);
    538     }
    539     let service_root = validate_git_root(service_root)?;
    540     let lib_root = validate_git_root(lib_root)?;
    541     if git_remote(&lib_root)? != "ssh://git@github.com/radrootslabs/lib.git"
    542         && git_remote(&lib_root)? != LIB_REPOSITORY
    543     {
    544         return Err(ReleaseArtifactError::InvalidServiceRoot);
    545     }
    546     let (input_root, input_snapshot) = validate_exact_input_root(input_root)?;
    547     let (output_parent, output_root) =
    548         validate_output_parent(output_root, &service_root, &input_root)?;
    549     let initial_head = git_head(&service_root)?;
    550     validate_clean_git(&service_root)?;
    551 
    552     let metadata = read_release_metadata(&service_root)?;
    553     let source_lock_bytes = read_bounded_regular(
    554         &service_root.join(LOCK_FILENAME),
    555         MAX_SOURCE_LOCK_BYTES,
    556         ReleaseArtifactError::InvalidSourceLock,
    557     )?;
    558     let source_lock = ServiceSourceLockV3::from_canonical_bytes(&source_lock_bytes)
    559         .map_err(|_| ReleaseArtifactError::InvalidSourceLock)?;
    560     if source_lock.service() != metadata.service {
    561         return Err(ReleaseArtifactError::InvalidSourceLock);
    562     }
    563     validate_source_lock_files(&service_root, &source_lock)?;
    564     let cargo_metadata = cargo_metadata(&service_root)?;
    565     let (sbom, notices, license_texts) = build_supply_chain_documents(&metadata, cargo_metadata)?;
    566 
    567     let staging = tempfile::Builder::new()
    568         .prefix(".radroots-service-release-")
    569         .tempdir_in(&output_parent)
    570         .map_err(|_| ReleaseArtifactError::GenerationFailure)?;
    571     set_directory_mode(staging.path())?;
    572 
    573     let static_inputs = [
    574         ("config.example.toml", "config.example.toml"),
    575         ("config.schema.json", "config.schema.json"),
    576         ("systemd.service", "systemd.service"),
    577         ("nixos-module.nix", "nixos-module.nix"),
    578     ];
    579     for (input, output) in static_inputs {
    580         copy_snapshot_file(
    581             &input_snapshot,
    582             input,
    583             &staging.path().join(output),
    584             MAX_TEXT_INPUT_BYTES,
    585         )?;
    586         validate_text_artifact(&staging.path().join(output), output)?;
    587     }
    588     copy_bounded(
    589         &service_root.join("LICENSE-APACHE"),
    590         &staging.path().join("LICENSE-APACHE"),
    591         MAX_TEXT_INPUT_BYTES,
    592     )?;
    593     validate_text_artifact(&staging.path().join("LICENSE-APACHE"), "LICENSE-APACHE")?;
    594     copy_bounded(
    595         &service_root.join("LICENSE-MIT"),
    596         &staging.path().join("LICENSE-MIT"),
    597         MAX_TEXT_INPUT_BYTES,
    598     )?;
    599     validate_text_artifact(&staging.path().join("LICENSE-MIT"), "LICENSE-MIT")?;
    600     write_generated(&staging.path().join(LOCK_FILENAME), &source_lock_bytes)?;
    601     create_binary_archive_from_snapshot(
    602         &input_snapshot,
    603         "service-binary",
    604         &staging.path().join("binary.tar.gz"),
    605         &metadata.binary_name,
    606         target,
    607         source_date_epoch,
    608     )?;
    609     let oci = copy_snapshot_file(
    610         &input_snapshot,
    611         "oci-image.tar.gz",
    612         &staging.path().join("oci-image.tar.gz"),
    613         MAX_OCI_BYTES,
    614     )?;
    615     let versions = source_lock.contract_versions();
    616     artifact_admission::admit_oci(
    617         &staging.path().join("oci-image.tar.gz"),
    618         staging.path(),
    619         &artifact_admission::OciExpectation {
    620             service: &metadata.service,
    621             binary_name: &metadata.binary_name,
    622             version: &metadata.version,
    623             service_revision: &initial_head,
    624             lib_revision: source_lock.revision(),
    625             license: &metadata.license,
    626             contract_versions: artifact_admission::ContractVersions {
    627                 admin: versions.admin(),
    628                 config: versions.config(),
    629                 provider: versions.provider(),
    630                 state: versions.state(),
    631                 status: versions.status(),
    632             },
    633         },
    634     )
    635     .map_err(|_| ReleaseArtifactError::InvalidInputArtifact)?;
    636     input_snapshot
    637         .revalidate()
    638         .map_err(|_| ReleaseArtifactError::InvalidInputRoot)?;
    639     let service_source = exact_tree_archive::create(
    640         &service_root,
    641         &initial_head,
    642         &staging.path().join("service-source.tar"),
    643         u64::from(source_date_epoch),
    644     )
    645     .map_err(|_| ReleaseArtifactError::InvalidSourceBundle)?;
    646     let lib_mtime = exact_tree_archive::commit_timestamp(&lib_root, source_lock.revision())
    647         .map_err(|_| ReleaseArtifactError::InvalidSourceBundle)?;
    648     let lib_source = exact_tree_archive::create(
    649         &lib_root,
    650         source_lock.revision(),
    651         &staging.path().join("lib-source.tar"),
    652         lib_mtime,
    653     )
    654     .map_err(|_| ReleaseArtifactError::InvalidSourceBundle)?;
    655     if lib_source.sha256 != source_lock.source_archive_sha256() {
    656         return Err(ReleaseArtifactError::InvalidSourceBundle);
    657     }
    658     let catalog = exact_tree_archive::read_blob(
    659         &lib_root,
    660         source_lock.revision(),
    661         "contracts/crates/catalog.v2.toml",
    662         MAX_TEXT_INPUT_BYTES as usize,
    663     )
    664     .map_err(|_| ReleaseArtifactError::InvalidSourceBundle)?;
    665     if sha256_bytes(&catalog) != source_lock.workspace_catalog_sha256() {
    666         return Err(ReleaseArtifactError::InvalidSourceBundle);
    667     }
    668 
    669     let oci_document = OciImageDocument {
    670         schema: "radroots.service.oci-image.v1",
    671         contract_version: 1,
    672         service: metadata.service.clone(),
    673         version: metadata.version.clone(),
    674         target: target.to_owned(),
    675         image: artifact_record("oci-image.tar.gz", &oci),
    676     };
    677     write_json(&staging.path().join("oci-image.v1.json"), &oci_document)?;
    678     let source_lock_sha256 = sha256_bytes(&source_lock_bytes);
    679     let source_document = SourceArchiveDocument {
    680         schema: "radroots.service.source-archives.v3",
    681         contract_version: 3,
    682         candidate_digest: candidate_digest.to_owned(),
    683         service: metadata.service.clone(),
    684         service_revision: initial_head.clone(),
    685         lib_repository: LIB_REPOSITORY,
    686         lib_revision: source_lock.revision().to_owned(),
    687         service_source: artifact_record_from_exact_tree("service-source.tar", &service_source),
    688         lib_source: artifact_record_from_exact_tree("lib-source.tar", &lib_source),
    689         source_lock_sha256: source_lock_sha256.clone(),
    690         workspace_catalog_sha256: source_lock.workspace_catalog_sha256().to_owned(),
    691         cargo_lock_sha256: source_lock.cargo_lock_sha256().to_owned(),
    692         flake_lock_sha256: source_lock.flake_lock_sha256().to_owned(),
    693         format: "ustar",
    694         compression: "none",
    695         git_history: "forbidden",
    696     };
    697     write_json(
    698         &staging.path().join("source-archives.v3.json"),
    699         &source_document,
    700     )?;
    701     write_generated(
    702         &staging.path().join("THIRD-PARTY-NOTICES.txt"),
    703         notices.as_bytes(),
    704     )?;
    705     write_generated(
    706         &staging.path().join("THIRD-PARTY-LICENSES.txt"),
    707         license_texts.as_bytes(),
    708     )?;
    709 
    710     let mut sbom = sbom;
    711     let sbom_artifacts = inventory_records(staging.path())?;
    712     reconcile_sbom_artifacts(&mut sbom, &sbom_artifacts);
    713     validate_cyclonedx_profile(&sbom, &sbom_artifacts)?;
    714     write_json(&staging.path().join("sbom.cdx.json"), &sbom)?;
    715     let scanned_artifacts = inventory_records(staging.path())?;
    716     let (nested_members_scanned, expanded_bytes_scanned) =
    717         scan_artifact_inventory(staging.path(), &scanned_artifacts)?;
    718     let scan = ScanDocument {
    719         schema: "radroots.service.artifact-scan.v1",
    720         contract_version: 1,
    721         candidate_digest: candidate_digest.to_owned(),
    722         state: "no_protected_material_detected",
    723         ruleset_sha256: scanner_ruleset_sha256(),
    724         scanned_artifacts,
    725         nested_members_scanned,
    726         expanded_bytes_scanned,
    727     };
    728     write_json(&staging.path().join("artifact-scan.v1.json"), &scan)?;
    729     let scan_evidence = hash_regular(
    730         &staging.path().join("artifact-scan.v1.json"),
    731         MAX_GENERATED_DOCUMENT_BYTES,
    732     )?;
    733 
    734     let payload = inventory_records(staging.path())?;
    735     let versions = source_lock.contract_versions();
    736     let manifest = ArtifactManifestDocument {
    737         schema: "radroots.service.release-artifacts.v2",
    738         contract_version: 2,
    739         candidate_digest: candidate_digest.to_owned(),
    740         service: metadata.service.clone(),
    741         version: metadata.version.clone(),
    742         target: target.to_owned(),
    743         source_date_epoch,
    744         service_revision: initial_head.clone(),
    745         lib_revision: source_lock.revision().to_owned(),
    746         rust_version: "1.97.1",
    747         host_feature_profile: "service-host",
    748         contract_versions: ContractVersionsDocument {
    749             config: versions.config(),
    750             state: versions.state(),
    751             admin: versions.admin(),
    752             status: versions.status(),
    753             provider: versions.provider(),
    754         },
    755         confidentiality: ConfidentialityDocument {
    756             state: scan.state,
    757             derived_from: artifact_record("artifact-scan.v1.json", &scan_evidence),
    758             protected_material_included: false,
    759         },
    760         artifacts: payload.clone(),
    761     };
    762     validate_confidentiality_binding(&manifest, &scan, &scan_evidence, &payload)?;
    763     write_json(&staging.path().join("artifact-manifest.v2.json"), &manifest)?;
    764     let manifest_evidence = hash_regular(
    765         &staging.path().join("artifact-manifest.v2.json"),
    766         MAX_TEXT_INPUT_BYTES,
    767     )?;
    768     let service_repository = git_remote(&service_root)?;
    769     let provenance = build_provenance(
    770         ProvenanceInput {
    771             candidate_digest,
    772             service: &metadata.service,
    773             target,
    774             source_date_epoch,
    775             service_repository: &service_repository,
    776             service_revision: &initial_head,
    777             lib_revision: source_lock.revision(),
    778             source_lock_sha256: &source_lock_sha256,
    779             manifest_sha256: &manifest_evidence.sha256,
    780         },
    781         &payload,
    782     );
    783     validate_provenance_subjects(&provenance, candidate_digest, &payload)?;
    784     write_json(&staging.path().join("provenance.intoto.jsonl"), &provenance)?;
    785     write_checksums(staging.path())?;
    786     validate_exact_output_inventory(staging.path())?;
    787     sync_directory(staging.path())?;
    788     let expected_output = inventory_records(staging.path())?;
    789 
    790     validate_clean_git(&service_root)?;
    791     if git_head(&service_root)? != initial_head {
    792         return Err(ReleaseArtifactError::DirtyServiceSource);
    793     }
    794 
    795     if output_root.exists() {
    796         compare_output(staging.path(), &output_root)?;
    797         return Ok(());
    798     }
    799     if mode == CommandMode::Check {
    800         return Err(ReleaseArtifactError::StaleOutput);
    801     }
    802     let staging_path = staging.keep();
    803     fs::rename(&staging_path, &output_root).map_err(|_| ReleaseArtifactError::GenerationFailure)?;
    804     sync_directory(&output_parent)?;
    805     validate_output_records(&output_root, &expected_output)
    806 }
    807 
    808 fn validate_confidentiality_binding(
    809     manifest: &ArtifactManifestDocument,
    810     scan: &ScanDocument,
    811     scan_evidence: &FileEvidence,
    812     payload: &[ArtifactRecord],
    813 ) -> Result<(), ReleaseArtifactError> {
    814     if manifest.candidate_digest != scan.candidate_digest
    815         || manifest.confidentiality.state != scan.state
    816         || manifest.confidentiality.protected_material_included
    817         || manifest.confidentiality.derived_from.path != "artifact-scan.v1.json"
    818         || manifest.confidentiality.derived_from.sha256 != scan_evidence.sha256
    819         || manifest.artifacts != payload
    820     {
    821         return Err(ReleaseArtifactError::GenerationFailure);
    822     }
    823     Ok(())
    824 }
    825 
    826 fn read_release_metadata(root: &Path) -> Result<ReleaseMetadata, ReleaseArtifactError> {
    827     let bytes = read_bounded_regular(
    828         &root.join("Cargo.toml"),
    829         MAX_MANIFEST_BYTES as u64,
    830         ReleaseArtifactError::InvalidServiceMetadata,
    831     )?;
    832     let text =
    833         std::str::from_utf8(&bytes).map_err(|_| ReleaseArtifactError::InvalidServiceMetadata)?;
    834     let value = toml::from_str::<toml::Value>(text)
    835         .map_err(|_| ReleaseArtifactError::InvalidServiceMetadata)?;
    836     let release = value
    837         .get("workspace")
    838         .and_then(|value| value.get("metadata"))
    839         .and_then(|value| value.get("radroots"))
    840         .and_then(|value| value.get("service_release"))
    841         .cloned()
    842         .ok_or(ReleaseArtifactError::InvalidServiceMetadata)?;
    843     let mut metadata = release
    844         .try_into::<ReleaseMetadata>()
    845         .map_err(|_| ReleaseArtifactError::InvalidServiceMetadata)?;
    846     metadata.license = value
    847         .get("workspace")
    848         .and_then(|workspace| workspace.get("package"))
    849         .or_else(|| value.get("package"))
    850         .and_then(|package| package.get("license"))
    851         .and_then(toml::Value::as_str)
    852         .ok_or(ReleaseArtifactError::InvalidServiceMetadata)?
    853         .to_owned();
    854     if !valid_snake_identifier(&metadata.service)
    855         || !valid_kebab_identifier(&metadata.service_package)
    856         || !valid_kebab_identifier(&metadata.binary_name)
    857         || metadata.version.len() > 128
    858         || metadata.license != "AGPL-3.0-or-later"
    859         || !matches!(
    860             semver::Version::parse(&metadata.version),
    861             Ok(version) if version.to_string() == metadata.version
    862         )
    863     {
    864         return Err(ReleaseArtifactError::InvalidServiceMetadata);
    865     }
    866     Ok(metadata)
    867 }
    868 
    869 fn validate_source_lock_files(
    870     root: &Path,
    871     source_lock: &ServiceSourceLockV3,
    872 ) -> Result<(), ReleaseArtifactError> {
    873     for name in [
    874         PREDECESSOR_LOCK_FILENAME,
    875         "radroots.service.source-lock.v1.toml",
    876     ] {
    877         match fs::symlink_metadata(root.join(name)) {
    878             Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
    879             _ => return Err(ReleaseArtifactError::InvalidSourceLock),
    880         }
    881     }
    882     let cargo_lock = hash_regular(&root.join("Cargo.lock"), MAX_SERVICE_CARGO_LOCK_BYTES)
    883         .map_err(|_| ReleaseArtifactError::InvalidSourceLock)?;
    884     if cargo_lock.sha256 != source_lock.cargo_lock_sha256() {
    885         return Err(ReleaseArtifactError::InvalidSourceLock);
    886     }
    887     let flake_nix = read_bounded_regular(
    888         &root.join("flake.nix"),
    889         MAX_TEXT_INPUT_BYTES,
    890         ReleaseArtifactError::InvalidSourceLock,
    891     )?;
    892     let flake_lock = read_bounded_regular(
    893         &root.join("flake.lock"),
    894         MAX_SERVICE_FLAKE_LOCK_BYTES,
    895         ReleaseArtifactError::InvalidSourceLock,
    896     )?;
    897     let evidence = validate_deferred_nix_material(&flake_nix, &flake_lock)
    898         .map_err(|_| ReleaseArtifactError::InvalidSourceLock)?;
    899     let artifact_contract = hash_regular(
    900         &root.join(source_lock.artifact_contract_path()),
    901         MAX_TEXT_INPUT_BYTES,
    902     )
    903     .map_err(|_| ReleaseArtifactError::InvalidSourceLock)?;
    904     if evidence.lib_revision() == source_lock.revision()
    905         && evidence.flake_lock_sha256() == source_lock.flake_lock_sha256()
    906         && artifact_contract.sha256 == source_lock.artifact_contract_sha256()
    907     {
    908         Ok(())
    909     } else {
    910         Err(ReleaseArtifactError::InvalidSourceLock)
    911     }
    912 }
    913 
    914 fn cargo_metadata(root: &Path) -> Result<CargoMetadata, ReleaseArtifactError> {
    915     let mut command = Command::new("cargo");
    916     command
    917         .args(["metadata", "--format-version", "1", "--locked", "--offline"])
    918         .current_dir(root);
    919     let bytes = command_stdout(&mut command, MAX_METADATA_BYTES)
    920         .map_err(|_| ReleaseArtifactError::InvalidPackageInventory)?;
    921     let mut metadata = serde_json::from_slice::<CargoMetadata>(&bytes)
    922         .map_err(|_| ReleaseArtifactError::InvalidPackageInventory)?;
    923     for package in &mut metadata.packages {
    924         if package.source.is_some() {
    925             package.license_texts = dependency_license_texts(package)?;
    926         }
    927     }
    928     Ok(metadata)
    929 }
    930 
    931 fn dependency_license_texts(
    932     package: &CargoPackage,
    933 ) -> Result<Vec<DependencyLicenseText>, ReleaseArtifactError> {
    934     let manifest = Path::new(&package.manifest_path);
    935     if !manifest.is_absolute() || manifest.file_name() != Some(OsStr::new("Cargo.toml")) {
    936         return Err(ReleaseArtifactError::InvalidPackageInventory);
    937     }
    938     let package_root = manifest
    939         .parent()
    940         .ok_or(ReleaseArtifactError::InvalidPackageInventory)?
    941         .canonicalize()
    942         .map_err(|_| ReleaseArtifactError::InvalidPackageInventory)?;
    943     let mut candidates = BTreeSet::new();
    944     if let Some(license_file) = package.license_file.as_deref() {
    945         let relative = Path::new(license_file);
    946         if relative.is_absolute()
    947             || relative
    948                 .components()
    949                 .any(|component| !matches!(component, Component::Normal(_)))
    950         {
    951             return Err(ReleaseArtifactError::InvalidPackageInventory);
    952         }
    953         candidates.insert(relative.to_path_buf());
    954     } else {
    955         let entries = fs::read_dir(&package_root)
    956             .map_err(|_| ReleaseArtifactError::InvalidPackageInventory)?;
    957         for (index, entry) in entries.enumerate() {
    958             if index >= 256 {
    959                 return Err(ReleaseArtifactError::InvalidPackageInventory);
    960             }
    961             let entry = entry.map_err(|_| ReleaseArtifactError::InvalidPackageInventory)?;
    962             let filename = entry
    963                 .file_name()
    964                 .into_string()
    965                 .map_err(|_| ReleaseArtifactError::InvalidPackageInventory)?;
    966             let uppercase = filename.to_ascii_uppercase();
    967             if ["LICENSE", "COPYING", "COPYRIGHT", "NOTICE"]
    968                 .iter()
    969                 .any(|prefix| {
    970                     uppercase == *prefix
    971                         || uppercase.starts_with(&format!("{prefix}-"))
    972                         || uppercase.starts_with(&format!("{prefix}."))
    973                 })
    974             {
    975                 candidates.insert(PathBuf::from(filename));
    976             }
    977         }
    978     }
    979     if candidates.is_empty() || candidates.len() > 16 {
    980         return Err(ReleaseArtifactError::InvalidPackageInventory);
    981     }
    982     let mut texts = Vec::with_capacity(candidates.len());
    983     let mut total = 0_u64;
    984     for relative in candidates {
    985         let path = package_root.join(&relative);
    986         let canonical = path
    987             .canonicalize()
    988             .map_err(|_| ReleaseArtifactError::InvalidPackageInventory)?;
    989         if canonical.parent() != Some(package_root.as_path()) {
    990             return Err(ReleaseArtifactError::InvalidPackageInventory);
    991         }
    992         let bytes = read_bounded_regular(
    993             &canonical,
    994             MAX_TEXT_INPUT_BYTES,
    995             ReleaseArtifactError::InvalidPackageInventory,
    996         )?;
    997         total = total
    998             .checked_add(bytes.len() as u64)
    999             .filter(|total| *total <= MAX_GENERATED_DOCUMENT_BYTES)
   1000             .ok_or(ReleaseArtifactError::InvalidPackageInventory)?;
   1001         scan_bytes(&bytes)?;
   1002         let text =
   1003             String::from_utf8(bytes).map_err(|_| ReleaseArtifactError::InvalidPackageInventory)?;
   1004         if text.trim().is_empty() {
   1005             return Err(ReleaseArtifactError::InvalidPackageInventory);
   1006         }
   1007         texts.push(DependencyLicenseText {
   1008             filename: relative
   1009                 .to_str()
   1010                 .ok_or(ReleaseArtifactError::InvalidPackageInventory)?
   1011                 .to_owned(),
   1012             sha256: sha256_bytes(text.as_bytes()),
   1013             text,
   1014         });
   1015     }
   1016     Ok(texts)
   1017 }
   1018 
   1019 fn build_supply_chain_documents(
   1020     metadata: &ReleaseMetadata,
   1021     cargo: CargoMetadata,
   1022 ) -> Result<(CycloneDxSbom, String, String), ReleaseArtifactError> {
   1023     if cargo.packages.is_empty()
   1024         || cargo.packages.len() > MAX_PACKAGES
   1025         || cargo.workspace_members.is_empty()
   1026         || cargo.workspace_members.len() > MAX_WORKSPACE_PACKAGES
   1027     {
   1028         return Err(ReleaseArtifactError::InvalidPackageInventory);
   1029     }
   1030     let workspace = cargo.workspace_members.into_iter().collect::<BTreeSet<_>>();
   1031     let mut by_id = BTreeMap::new();
   1032     let mut root_id = None;
   1033     for package in cargo.packages {
   1034         validate_metadata_package(&package, workspace.contains(&package.id))?;
   1035         if package.name == metadata.service_package
   1036             && (root_id.replace(package.id.clone()).is_some()
   1037                 || package.version != metadata.version
   1038                 || !package.targets.iter().any(|target| {
   1039                     target.name == metadata.binary_name
   1040                         && target.kind.iter().any(|kind| kind == "bin")
   1041                 })
   1042                 || !workspace.contains(&package.id))
   1043         {
   1044             return Err(ReleaseArtifactError::InvalidPackageInventory);
   1045         }
   1046         if by_id.insert(package.id.clone(), package).is_some() {
   1047             return Err(ReleaseArtifactError::InvalidPackageInventory);
   1048         }
   1049     }
   1050     let root_id = root_id.ok_or(ReleaseArtifactError::InvalidPackageInventory)?;
   1051     if by_id.len() > MAX_PACKAGES || !workspace.iter().all(|id| by_id.contains_key(id)) {
   1052         return Err(ReleaseArtifactError::InvalidPackageInventory);
   1053     }
   1054     let references = by_id
   1055         .values()
   1056         .map(|package| (package.id.clone(), package_reference(package)))
   1057         .collect::<BTreeMap<_, _>>();
   1058     let root_package = by_id
   1059         .get(&root_id)
   1060         .ok_or(ReleaseArtifactError::InvalidPackageInventory)?;
   1061     let root_component = sbom_component(root_package, "application")?;
   1062     let mut components = by_id
   1063         .values()
   1064         .filter(|package| package.id != root_id)
   1065         .map(|package| sbom_component(package, "library"))
   1066         .collect::<Result<Vec<_>, _>>()?;
   1067     components.sort();
   1068     let resolve = cargo
   1069         .resolve
   1070         .ok_or(ReleaseArtifactError::InvalidPackageInventory)?;
   1071     if resolve.nodes.len() != by_id.len() || resolve.nodes.len() > MAX_PACKAGES {
   1072         return Err(ReleaseArtifactError::InvalidPackageInventory);
   1073     }
   1074     let mut seen_nodes = BTreeSet::new();
   1075     let mut dependencies = Vec::with_capacity(resolve.nodes.len());
   1076     for node in resolve.nodes {
   1077         if !seen_nodes.insert(node.id.clone()) {
   1078             return Err(ReleaseArtifactError::InvalidPackageInventory);
   1079         }
   1080         let reference = references
   1081             .get(&node.id)
   1082             .ok_or(ReleaseArtifactError::InvalidPackageInventory)?
   1083             .clone();
   1084         let mut depends_on = node
   1085             .dependencies
   1086             .iter()
   1087             .map(|dependency| {
   1088                 references
   1089                     .get(dependency)
   1090                     .cloned()
   1091                     .ok_or(ReleaseArtifactError::InvalidPackageInventory)
   1092             })
   1093             .collect::<Result<Vec<_>, _>>()?;
   1094         depends_on.sort();
   1095         depends_on.dedup();
   1096         dependencies.push(SbomDependency {
   1097             reference,
   1098             depends_on,
   1099         });
   1100     }
   1101     dependencies.sort_by(|left, right| left.reference.cmp(&right.reference));
   1102     let sbom = CycloneDxSbom {
   1103         json_schema: "https://cyclonedx.org/schema/bom-1.6.schema.json",
   1104         bom_format: "CycloneDX",
   1105         spec_version: "1.6",
   1106         version: 1,
   1107         metadata: SbomMetadata {
   1108             component: root_component,
   1109             properties: vec![SbomProperty {
   1110                 name: "radroots:evidence:dependency-closure".to_owned(),
   1111                 value: "complete".to_owned(),
   1112             }],
   1113         },
   1114         components,
   1115         dependencies,
   1116         compositions: Vec::new(),
   1117     };
   1118 
   1119     let mut third_party = by_id
   1120         .values()
   1121         .filter(|package| !workspace.contains(&package.id))
   1122         .collect::<Vec<_>>();
   1123     third_party.sort_by(|left, right| {
   1124         (&left.name, &left.version, &left.source).cmp(&(&right.name, &right.version, &right.source))
   1125     });
   1126     let mut notices = String::from(
   1127         "Radroots service third-party notices v1\n\nThis inventory is generated from the locked Cargo dependency graph.\n",
   1128     );
   1129     let mut licenses = String::from(
   1130         "Radroots service third-party license texts v1\n\nThis file contains the exact bounded license texts admitted for each locked third-party Cargo package.\n",
   1131     );
   1132     if third_party.is_empty() {
   1133         notices.push_str("\nNo third-party Cargo packages are present.\n");
   1134         licenses.push_str("\nNo third-party Cargo packages are present.\n");
   1135     } else {
   1136         for package in third_party {
   1137             let license = package
   1138                 .license
   1139                 .as_deref()
   1140                 .ok_or(ReleaseArtifactError::InvalidPackageInventory)?;
   1141             let source = package
   1142                 .source
   1143                 .as_deref()
   1144                 .ok_or(ReleaseArtifactError::InvalidPackageInventory)?;
   1145             use fmt::Write as _;
   1146             writeln!(notices).map_err(|_| ReleaseArtifactError::GenerationFailure)?;
   1147             writeln!(notices, "Package: {} {}", package.name, package.version)
   1148                 .map_err(|_| ReleaseArtifactError::GenerationFailure)?;
   1149             writeln!(notices, "License: {license}")
   1150                 .map_err(|_| ReleaseArtifactError::GenerationFailure)?;
   1151             writeln!(notices, "Source: {source}")
   1152                 .map_err(|_| ReleaseArtifactError::GenerationFailure)?;
   1153             if package.license_texts.is_empty() {
   1154                 return Err(ReleaseArtifactError::InvalidPackageInventory);
   1155             }
   1156             for license_text in &package.license_texts {
   1157                 if !valid_output_component(&license_text.filename)
   1158                     || license_text.text.trim().is_empty()
   1159                     || license_text.sha256 != sha256_bytes(license_text.text.as_bytes())
   1160                 {
   1161                     return Err(ReleaseArtifactError::InvalidPackageInventory);
   1162                 }
   1163                 writeln!(
   1164                     notices,
   1165                     "License-Text: {} sha256:{}",
   1166                     license_text.filename, license_text.sha256
   1167                 )
   1168                 .map_err(|_| ReleaseArtifactError::GenerationFailure)?;
   1169                 writeln!(licenses).map_err(|_| ReleaseArtifactError::GenerationFailure)?;
   1170                 writeln!(
   1171                     licenses,
   1172                     "===== {} {} / {} / sha256:{} =====",
   1173                     package.name, package.version, license_text.filename, license_text.sha256
   1174                 )
   1175                 .map_err(|_| ReleaseArtifactError::GenerationFailure)?;
   1176                 licenses.push_str(&license_text.text);
   1177                 if !license_text.text.ends_with('\n') {
   1178                     licenses.push('\n');
   1179                 }
   1180             }
   1181         }
   1182     }
   1183     scan_bytes(notices.as_bytes())?;
   1184     scan_bytes(licenses.as_bytes())?;
   1185     if licenses.len() as u64 > MAX_GENERATED_DOCUMENT_BYTES {
   1186         return Err(ReleaseArtifactError::InvalidPackageInventory);
   1187     }
   1188     Ok((sbom, notices, licenses))
   1189 }
   1190 
   1191 fn reconcile_sbom_artifacts(sbom: &mut CycloneDxSbom, artifacts: &[ArtifactRecord]) {
   1192     let root_reference = sbom.metadata.component.bom_ref.clone();
   1193     let mut assemblies = Vec::with_capacity(artifacts.len());
   1194     for artifact in artifacts {
   1195         let reference = format!("artifact:{}#{}", artifact.path, artifact.sha256);
   1196         assemblies.push(reference.clone());
   1197         sbom.components.push(SbomComponent {
   1198             component_type: "file",
   1199             bom_ref: reference.clone(),
   1200             name: artifact.path.clone(),
   1201             version: artifact.sha256.clone(),
   1202             purl: None,
   1203             licenses: sbom.metadata.component.licenses.clone(),
   1204             hashes: vec![DigestValue {
   1205                 alg: "SHA-256",
   1206                 content: artifact.sha256.clone(),
   1207             }],
   1208             properties: vec![
   1209                 SbomProperty {
   1210                     name: "radroots:artifact:byte-length".to_owned(),
   1211                     value: artifact.byte_length.to_string(),
   1212                 },
   1213                 SbomProperty {
   1214                     name: "radroots:ecosystem".to_owned(),
   1215                     value: artifact_ecosystem(&artifact.path).to_owned(),
   1216                 },
   1217             ],
   1218         });
   1219         sbom.dependencies.push(SbomDependency {
   1220             reference,
   1221             depends_on: vec![root_reference.clone()],
   1222         });
   1223     }
   1224     sbom.components.sort();
   1225     sbom.dependencies
   1226         .sort_by(|left, right| left.reference.cmp(&right.reference));
   1227     assemblies.sort();
   1228     let mut dependencies =
   1229         sbom.components
   1230             .iter()
   1231             .filter(|component| {
   1232                 component.properties.iter().any(|property| {
   1233                     property.name == "radroots:ecosystem" && property.value == "cargo"
   1234                 })
   1235             })
   1236             .map(|component| component.bom_ref.clone())
   1237             .collect::<Vec<_>>();
   1238     dependencies.push(root_reference);
   1239     dependencies.sort();
   1240     dependencies.dedup();
   1241     sbom.compositions = vec![SbomComposition {
   1242         aggregate: "complete",
   1243         assemblies,
   1244         dependencies,
   1245     }];
   1246 }
   1247 
   1248 fn validate_cyclonedx_profile(
   1249     sbom: &CycloneDxSbom,
   1250     artifacts: &[ArtifactRecord],
   1251 ) -> Result<(), ReleaseArtifactError> {
   1252     let schema = serde_json::json!({
   1253         "$schema": "https://json-schema.org/draft/2020-12/schema",
   1254         "type": "object",
   1255         "additionalProperties": false,
   1256         "required": ["$schema", "bomFormat", "specVersion", "version", "metadata", "components", "dependencies", "compositions"],
   1257         "properties": {
   1258             "$schema": {"const": "https://cyclonedx.org/schema/bom-1.6.schema.json"},
   1259             "bomFormat": {"const": "CycloneDX"},
   1260             "specVersion": {"const": "1.6"},
   1261             "version": {"const": 1},
   1262             "metadata": {"type": "object"},
   1263             "components": {"type": "array", "minItems": 1},
   1264             "dependencies": {"type": "array", "minItems": 1},
   1265             "compositions": {"type": "array", "minItems": 1, "maxItems": 1}
   1266         }
   1267     });
   1268     let validator =
   1269         jsonschema::validator_for(&schema).map_err(|_| ReleaseArtifactError::GenerationFailure)?;
   1270     let value = serde_json::to_value(sbom).map_err(|_| ReleaseArtifactError::GenerationFailure)?;
   1271     validator
   1272         .validate(&value)
   1273         .map_err(|_| ReleaseArtifactError::GenerationFailure)?;
   1274 
   1275     let component_references = sbom
   1276         .components
   1277         .iter()
   1278         .map(|component| component.bom_ref.as_str())
   1279         .chain(std::iter::once(sbom.metadata.component.bom_ref.as_str()))
   1280         .collect::<BTreeSet<_>>();
   1281     if component_references.len() != sbom.components.len() + 1
   1282         || sbom.dependencies.len() != component_references.len()
   1283         || sbom.dependencies.iter().any(|dependency| {
   1284             !component_references.contains(dependency.reference.as_str())
   1285                 || dependency
   1286                     .depends_on
   1287                     .iter()
   1288                     .any(|reference| !component_references.contains(reference.as_str()))
   1289         })
   1290     {
   1291         return Err(ReleaseArtifactError::GenerationFailure);
   1292     }
   1293     let composition = sbom
   1294         .compositions
   1295         .first()
   1296         .ok_or(ReleaseArtifactError::GenerationFailure)?;
   1297     let expected_artifacts = artifacts
   1298         .iter()
   1299         .map(|artifact| format!("artifact:{}#{}", artifact.path, artifact.sha256))
   1300         .collect::<BTreeSet<_>>();
   1301     if composition.aggregate != "complete"
   1302         || composition.assemblies.iter().collect::<BTreeSet<_>>()
   1303             != expected_artifacts.iter().collect::<BTreeSet<_>>()
   1304         || artifacts.iter().any(|artifact| {
   1305             sbom.components
   1306                 .iter()
   1307                 .filter(|component| {
   1308                     component.bom_ref == format!("artifact:{}#{}", artifact.path, artifact.sha256)
   1309                         && component.hashes
   1310                             == [DigestValue {
   1311                                 alg: "SHA-256",
   1312                                 content: artifact.sha256.clone(),
   1313                             }]
   1314                 })
   1315                 .count()
   1316                 != 1
   1317         })
   1318     {
   1319         return Err(ReleaseArtifactError::GenerationFailure);
   1320     }
   1321     Ok(())
   1322 }
   1323 
   1324 fn validate_provenance_subjects(
   1325     provenance: &InTotoStatement,
   1326     candidate_digest: &str,
   1327     artifacts: &[ArtifactRecord],
   1328 ) -> Result<(), ReleaseArtifactError> {
   1329     let expected = artifacts
   1330         .iter()
   1331         .map(|artifact| (artifact.path.as_str(), artifact.sha256.as_str()))
   1332         .collect::<BTreeSet<_>>();
   1333     let observed = provenance
   1334         .subject
   1335         .iter()
   1336         .filter_map(|subject| {
   1337             subject
   1338                 .digest
   1339                 .get("sha256")
   1340                 .map(|digest| (subject.name.as_str(), digest.as_str()))
   1341         })
   1342         .collect::<BTreeSet<_>>();
   1343     if provenance.statement_type != "https://in-toto.io/Statement/v1"
   1344         || provenance.predicate_type != "https://slsa.dev/provenance/v1"
   1345         || provenance
   1346             .predicate
   1347             .build_definition
   1348             .external_parameters
   1349             .candidate_digest
   1350             != format!("{CANDIDATE_DIGEST_DOMAIN}{candidate_digest}")
   1351         || observed != expected
   1352         || provenance.subject.len() != expected.len()
   1353     {
   1354         return Err(ReleaseArtifactError::GenerationFailure);
   1355     }
   1356     Ok(())
   1357 }
   1358 
   1359 fn artifact_ecosystem(path: &str) -> &'static str {
   1360     if path == "oci-image.tar.gz" || path == "nixos-module.nix" {
   1361         "nix"
   1362     } else if path.ends_with("-source.tar") {
   1363         "git"
   1364     } else if path == "binary.tar.gz" {
   1365         "native"
   1366     } else {
   1367         "release"
   1368     }
   1369 }
   1370 
   1371 struct ProvenanceInput<'a> {
   1372     candidate_digest: &'a str,
   1373     service: &'a str,
   1374     target: &'a str,
   1375     source_date_epoch: u32,
   1376     service_repository: &'a str,
   1377     service_revision: &'a str,
   1378     lib_revision: &'a str,
   1379     source_lock_sha256: &'a str,
   1380     manifest_sha256: &'a str,
   1381 }
   1382 
   1383 fn build_provenance(input: ProvenanceInput<'_>, artifacts: &[ArtifactRecord]) -> InTotoStatement {
   1384     let ProvenanceInput {
   1385         candidate_digest,
   1386         service,
   1387         target,
   1388         source_date_epoch,
   1389         service_repository,
   1390         service_revision,
   1391         lib_revision,
   1392         source_lock_sha256,
   1393         manifest_sha256,
   1394     } = input;
   1395     let mut subject = artifacts
   1396         .iter()
   1397         .map(|artifact| InTotoSubject {
   1398             name: artifact.path.clone(),
   1399             digest: BTreeMap::from([("sha256", artifact.sha256.clone())]),
   1400         })
   1401         .collect::<Vec<_>>();
   1402     subject.sort_by(|left, right| left.name.cmp(&right.name));
   1403     let mut resolved_dependencies = vec![
   1404         SlsaResolvedDependency {
   1405             uri: format!("git+{service_repository}@{service_revision}"),
   1406             digest: BTreeMap::from([("gitCommit", service_revision.to_owned())]),
   1407         },
   1408         SlsaResolvedDependency {
   1409             uri: format!("git+{LIB_REPOSITORY}@{lib_revision}"),
   1410             digest: BTreeMap::from([("gitCommit", lib_revision.to_owned())]),
   1411         },
   1412         SlsaResolvedDependency {
   1413             uri: format!("file:{LOCK_FILENAME}"),
   1414             digest: BTreeMap::from([("sha256", source_lock_sha256.to_owned())]),
   1415         },
   1416         SlsaResolvedDependency {
   1417             uri: "file:artifact-manifest.v2.json".to_owned(),
   1418             digest: BTreeMap::from([("sha256", manifest_sha256.to_owned())]),
   1419         },
   1420     ];
   1421     resolved_dependencies.sort_by(|left, right| left.uri.cmp(&right.uri));
   1422     let invocation_id = sha256_bytes(
   1423         format!(
   1424             "radroots.service.slsa.invocation.v1\0{candidate_digest}\0{service}\0{target}\0{manifest_sha256}"
   1425         )
   1426         .as_bytes(),
   1427     );
   1428     InTotoStatement {
   1429         statement_type: "https://in-toto.io/Statement/v1",
   1430         subject,
   1431         predicate_type: "https://slsa.dev/provenance/v1",
   1432         predicate: SlsaPredicate {
   1433             build_definition: SlsaBuildDefinition {
   1434                 build_type: "https://radroots.dev/contracts/service-release-artifacts/v2",
   1435                 external_parameters: SlsaExternalParameters {
   1436                     candidate_digest: format!("{CANDIDATE_DIGEST_DOMAIN}{candidate_digest}"),
   1437                     service: service.to_owned(),
   1438                     target: target.to_owned(),
   1439                     source_date_epoch,
   1440                 },
   1441                 internal_parameters: BTreeMap::new(),
   1442                 resolved_dependencies,
   1443             },
   1444             run_details: SlsaRunDetails {
   1445                 builder: SlsaBuilder {
   1446                     id: "https://radroots.dev/builders/service-release-artifacts/v2",
   1447                 },
   1448                 metadata: SlsaRunMetadata { invocation_id },
   1449             },
   1450         },
   1451     }
   1452 }
   1453 
   1454 fn scanner_ruleset_sha256() -> String {
   1455     let mut bytes = b"radroots.service.artifact-scan.rules.v1\0".to_vec();
   1456     for pattern in SECRET_PATTERNS {
   1457         bytes.extend_from_slice(pattern);
   1458         bytes.push(0);
   1459     }
   1460     sha256_bytes(&bytes)
   1461 }
   1462 
   1463 fn scan_artifact_inventory(
   1464     root: &Path,
   1465     artifacts: &[ArtifactRecord],
   1466 ) -> Result<(u64, u64), ReleaseArtifactError> {
   1467     let mut nested_members = 0_u64;
   1468     let mut expanded_bytes = 0_u64;
   1469     for artifact in artifacts {
   1470         let path = root.join(&artifact.path);
   1471         expanded_bytes = expanded_bytes
   1472             .checked_add(scan_regular_file(&path, artifact.byte_length)?)
   1473             .ok_or(ReleaseArtifactError::InvalidInputArtifact)?;
   1474         if artifact.path == "service-source.tar" || artifact.path == "lib-source.tar" {
   1475             let (members, bytes) = scan_tar_members(&path, false)?;
   1476             nested_members = nested_members
   1477                 .checked_add(members)
   1478                 .ok_or(ReleaseArtifactError::InvalidInputArtifact)?;
   1479             expanded_bytes = expanded_bytes
   1480                 .checked_add(bytes)
   1481                 .ok_or(ReleaseArtifactError::InvalidInputArtifact)?;
   1482         } else if artifact.path == "binary.tar.gz" {
   1483             let (members, bytes) = scan_tar_gzip_members(&path)?;
   1484             nested_members = nested_members
   1485                 .checked_add(members)
   1486                 .ok_or(ReleaseArtifactError::InvalidInputArtifact)?;
   1487             expanded_bytes = expanded_bytes
   1488                 .checked_add(bytes)
   1489                 .ok_or(ReleaseArtifactError::InvalidInputArtifact)?;
   1490         } else if artifact.path == "oci-image.tar.gz" {
   1491             let limits = TarGzipLimits {
   1492                 max_compressed_bytes: MAX_OCI_BYTES,
   1493                 max_expanded_bytes: MAX_ARCHIVE_EXPANDED_BYTES,
   1494                 max_members: 65_536,
   1495                 max_member_bytes: MAX_ARCHIVE_EXPANDED_BYTES,
   1496                 max_payload_bytes: MAX_ARCHIVE_EXPANDED_BYTES,
   1497                 max_depth: 64,
   1498                 max_path_bytes: MAX_ARCHIVE_PATH_BYTES,
   1499             };
   1500             let materialized =
   1501                 safe_artifact_io::materialize_tar_gzip_path(path.as_path(), root, limits)
   1502                     .map_err(|_| ReleaseArtifactError::InvalidInputArtifact)?;
   1503             for file in materialized.snapshot().files() {
   1504                 let relative = file
   1505                     .relative_path()
   1506                     .to_str()
   1507                     .ok_or(ReleaseArtifactError::InvalidInputArtifact)?;
   1508                 validate_scanned_path(relative)?;
   1509                 let evidence = materialized
   1510                     .snapshot()
   1511                     .hash(file, MAX_ARCHIVE_EXPANDED_BYTES)
   1512                     .map_err(|_| ReleaseArtifactError::InvalidInputArtifact)?;
   1513                 expanded_bytes = expanded_bytes
   1514                     .checked_add(scan_regular_file(
   1515                         &materialized.root().join(file.relative_path()),
   1516                         evidence.byte_length,
   1517                     )?)
   1518                     .ok_or(ReleaseArtifactError::InvalidInputArtifact)?;
   1519                 nested_members = nested_members
   1520                     .checked_add(1)
   1521                     .ok_or(ReleaseArtifactError::InvalidInputArtifact)?;
   1522                 if relative.ends_with("/layer.tar") {
   1523                     let (members, bytes) =
   1524                         scan_tar_members(&materialized.root().join(file.relative_path()), true)?;
   1525                     nested_members = nested_members
   1526                         .checked_add(members)
   1527                         .ok_or(ReleaseArtifactError::InvalidInputArtifact)?;
   1528                     expanded_bytes = expanded_bytes
   1529                         .checked_add(bytes)
   1530                         .ok_or(ReleaseArtifactError::InvalidInputArtifact)?;
   1531                 }
   1532             }
   1533             materialized
   1534                 .revalidate()
   1535                 .map_err(|_| ReleaseArtifactError::InvalidInputArtifact)?;
   1536         }
   1537     }
   1538     Ok((nested_members, expanded_bytes))
   1539 }
   1540 
   1541 fn scan_tar_gzip_members(path: &Path) -> Result<(u64, u64), ReleaseArtifactError> {
   1542     let file = fs::File::open(path).map_err(|_| ReleaseArtifactError::InvalidInputArtifact)?;
   1543     let decoder = flate2::read::GzDecoder::new(file);
   1544     let mut archive = tar::Archive::new(decoder);
   1545     let mut members = 0_u64;
   1546     let mut bytes = 0_u64;
   1547     for entry in archive
   1548         .entries()
   1549         .map_err(|_| ReleaseArtifactError::InvalidInputArtifact)?
   1550     {
   1551         let mut entry = entry.map_err(|_| ReleaseArtifactError::InvalidInputArtifact)?;
   1552         members = members
   1553             .checked_add(1)
   1554             .filter(|count| *count <= 4)
   1555             .ok_or(ReleaseArtifactError::InvalidInputArtifact)?;
   1556         let path = entry
   1557             .path()
   1558             .map_err(|_| ReleaseArtifactError::InvalidInputArtifact)?;
   1559         let path = path
   1560             .to_str()
   1561             .ok_or(ReleaseArtifactError::InvalidInputArtifact)?;
   1562         validate_scanned_path(path)?;
   1563         if entry.header().entry_type().is_file() {
   1564             let size = entry.size();
   1565             if size > MAX_BINARY_BYTES {
   1566                 return Err(ReleaseArtifactError::InvalidInputArtifact);
   1567             }
   1568             bytes = bytes
   1569                 .checked_add(scan_reader(&mut entry, size)?)
   1570                 .filter(|count| *count <= MAX_BINARY_BYTES)
   1571                 .ok_or(ReleaseArtifactError::InvalidInputArtifact)?;
   1572         } else if !entry.header().entry_type().is_dir() {
   1573             return Err(ReleaseArtifactError::InvalidInputArtifact);
   1574         }
   1575     }
   1576     if members == 0 {
   1577         Err(ReleaseArtifactError::InvalidInputArtifact)
   1578     } else {
   1579         Ok((members, bytes))
   1580     }
   1581 }
   1582 
   1583 fn scan_tar_members(path: &Path, links_allowed: bool) -> Result<(u64, u64), ReleaseArtifactError> {
   1584     let file = fs::File::open(path).map_err(|_| ReleaseArtifactError::InvalidInputArtifact)?;
   1585     let mut archive = tar::Archive::new(file);
   1586     let mut members = 0_u64;
   1587     let mut bytes = 0_u64;
   1588     for entry in archive
   1589         .entries()
   1590         .map_err(|_| ReleaseArtifactError::InvalidInputArtifact)?
   1591     {
   1592         let mut entry = entry.map_err(|_| ReleaseArtifactError::InvalidInputArtifact)?;
   1593         let path = entry
   1594             .path()
   1595             .map_err(|_| ReleaseArtifactError::InvalidInputArtifact)?;
   1596         let path = path
   1597             .to_str()
   1598             .ok_or(ReleaseArtifactError::InvalidInputArtifact)?;
   1599         validate_scanned_path(path)?;
   1600         members = members
   1601             .checked_add(1)
   1602             .filter(|count| *count <= MAX_SOURCE_ARCHIVE_MEMBERS)
   1603             .ok_or(ReleaseArtifactError::InvalidInputArtifact)?;
   1604         if entry.header().entry_type().is_file() {
   1605             let size = entry.size();
   1606             if !links_allowed && size > MAX_SOURCE_ARCHIVE_MEMBER_BYTES {
   1607                 return Err(ReleaseArtifactError::InvalidInputArtifact);
   1608             }
   1609             let scanned = scan_reader(&mut entry, size)?;
   1610             bytes = bytes
   1611                 .checked_add(scanned)
   1612                 .filter(|count| *count <= MAX_ARCHIVE_EXPANDED_BYTES)
   1613                 .ok_or(ReleaseArtifactError::InvalidInputArtifact)?;
   1614         } else if !links_allowed
   1615             || !(entry.header().entry_type().is_dir()
   1616                 || entry.header().entry_type().is_symlink()
   1617                 || entry.header().entry_type().is_hard_link())
   1618         {
   1619             return Err(ReleaseArtifactError::InvalidInputArtifact);
   1620         }
   1621     }
   1622     if members == 0 {
   1623         Err(ReleaseArtifactError::InvalidInputArtifact)
   1624     } else {
   1625         Ok((members, bytes))
   1626     }
   1627 }
   1628 
   1629 fn scan_regular_file(path: &Path, expected_length: u64) -> Result<u64, ReleaseArtifactError> {
   1630     let mut file = fs::File::open(path).map_err(|_| ReleaseArtifactError::InvalidInputArtifact)?;
   1631     scan_reader(&mut file, expected_length)
   1632 }
   1633 
   1634 fn scan_reader(
   1635     reader: &mut impl std::io::Read,
   1636     expected_length: u64,
   1637 ) -> Result<u64, ReleaseArtifactError> {
   1638     let mut scanner = SecretScanner::default();
   1639     let mut total = 0_u64;
   1640     let mut buffer = [0_u8; 64 * 1024];
   1641     loop {
   1642         let read = reader
   1643             .read(&mut buffer)
   1644             .map_err(|_| ReleaseArtifactError::InvalidInputArtifact)?;
   1645         if read == 0 {
   1646             break;
   1647         }
   1648         total = total
   1649             .checked_add(read as u64)
   1650             .filter(|value| *value <= expected_length)
   1651             .ok_or(ReleaseArtifactError::InvalidInputArtifact)?;
   1652         scanner.scan(&buffer[..read])?;
   1653     }
   1654     if total == expected_length {
   1655         Ok(total)
   1656     } else {
   1657         Err(ReleaseArtifactError::InvalidInputArtifact)
   1658     }
   1659 }
   1660 
   1661 fn validate_scanned_path(path: &str) -> Result<(), ReleaseArtifactError> {
   1662     let sensitive = Path::new(path).components().any(|component| {
   1663         let Component::Normal(value) = component else {
   1664             return true;
   1665         };
   1666         let value = value.to_string_lossy().to_ascii_lowercase();
   1667         matches!(
   1668             value.as_str(),
   1669             ".git"
   1670                 | ".ssh"
   1671                 | ".aws"
   1672                 | ".env"
   1673                 | "credentials"
   1674                 | "secrets"
   1675                 | "id_rsa"
   1676                 | "id_ed25519"
   1677                 | "private_key"
   1678         )
   1679     });
   1680     if sensitive || path.len() > MAX_ARCHIVE_PATH_BYTES {
   1681         Err(ReleaseArtifactError::ProtectedMaterialDetected)
   1682     } else {
   1683         Ok(())
   1684     }
   1685 }
   1686 
   1687 fn validate_metadata_package(
   1688     package: &CargoPackage,
   1689     workspace_member: bool,
   1690 ) -> Result<(), ReleaseArtifactError> {
   1691     for value in [&package.name, &package.version, &package.id] {
   1692         if value.is_empty() || value.len() > MAX_TEXT_FIELD_BYTES || value.contains(['\n', '\r']) {
   1693             return Err(ReleaseArtifactError::InvalidPackageInventory);
   1694         }
   1695     }
   1696     if package
   1697         .license
   1698         .as_ref()
   1699         .is_some_and(|value| !valid_metadata_text(value))
   1700         || package
   1701             .source
   1702             .as_ref()
   1703             .is_some_and(|value| !valid_public_source(value))
   1704         || (!workspace_member && (package.source.is_none() || package.license.is_none()))
   1705         || package
   1706             .checksum
   1707             .as_ref()
   1708             .is_some_and(|value| !valid_lower_hex(value, 64))
   1709     {
   1710         return Err(ReleaseArtifactError::InvalidPackageInventory);
   1711     }
   1712     Ok(())
   1713 }
   1714 
   1715 fn sbom_component(
   1716     package: &CargoPackage,
   1717     component_type: &'static str,
   1718 ) -> Result<SbomComponent, ReleaseArtifactError> {
   1719     let license = package
   1720         .license
   1721         .clone()
   1722         .unwrap_or_else(|| "NOASSERTION".to_owned());
   1723     let hashes = package
   1724         .checksum
   1725         .iter()
   1726         .map(|checksum| DigestValue {
   1727             alg: "SHA-256",
   1728             content: checksum.clone(),
   1729         })
   1730         .collect();
   1731     Ok(SbomComponent {
   1732         component_type,
   1733         bom_ref: package_reference(package),
   1734         name: package.name.clone(),
   1735         version: package.version.clone(),
   1736         purl: Some(format!("pkg:cargo/{}@{}", package.name, package.version)),
   1737         licenses: vec![LicenseChoice {
   1738             expression: license,
   1739         }],
   1740         hashes,
   1741         properties: vec![SbomProperty {
   1742             name: "radroots:ecosystem".to_owned(),
   1743             value: "cargo".to_owned(),
   1744         }],
   1745     })
   1746 }
   1747 
   1748 fn package_reference(package: &CargoPackage) -> String {
   1749     let source = package.source.as_deref().unwrap_or("workspace");
   1750     let source_digest = sha256_bytes(source.as_bytes());
   1751     format!(
   1752         "cargo:{}@{}#{}",
   1753         package.name,
   1754         package.version,
   1755         &source_digest[..16]
   1756     )
   1757 }
   1758 
   1759 fn validate_exact_input_root(
   1760     path: &Path,
   1761 ) -> Result<(PathBuf, safe_artifact_io::TraversalSnapshot), ReleaseArtifactError> {
   1762     let root = validate_absolute_directory(path, ReleaseArtifactError::InvalidInputRoot)?;
   1763     let snapshot = flat_directory_snapshot(&root, ReleaseArtifactError::InvalidInputRoot)?;
   1764     let inventory = snapshot_inventory(&snapshot, ReleaseArtifactError::InvalidInputRoot)?;
   1765     if inventory == INPUT_NAMES.into_iter().map(str::to_owned).collect() {
   1766         Ok((root, snapshot))
   1767     } else {
   1768         Err(ReleaseArtifactError::InvalidInputRoot)
   1769     }
   1770 }
   1771 
   1772 fn validate_output_parent(
   1773     output: &Path,
   1774     service_root: &Path,
   1775     input_root: &Path,
   1776 ) -> Result<(PathBuf, PathBuf), ReleaseArtifactError> {
   1777     if !output.is_absolute()
   1778         || output
   1779             .file_name()
   1780             .and_then(OsStr::to_str)
   1781             .is_none_or(|name| !valid_output_component(name))
   1782     {
   1783         return Err(ReleaseArtifactError::InvalidOutputRoot);
   1784     }
   1785     let parent = output
   1786         .parent()
   1787         .ok_or(ReleaseArtifactError::InvalidOutputRoot)?;
   1788     let parent = validate_absolute_directory(parent, ReleaseArtifactError::InvalidOutputRoot)?;
   1789     let output = parent.join(
   1790         output
   1791             .file_name()
   1792             .ok_or(ReleaseArtifactError::InvalidOutputRoot)?,
   1793     );
   1794     if output.starts_with(service_root)
   1795         || output.starts_with(input_root)
   1796         || service_root.starts_with(&output)
   1797         || input_root.starts_with(&output)
   1798     {
   1799         return Err(ReleaseArtifactError::InvalidOutputRoot);
   1800     }
   1801     if let Ok(metadata) = fs::symlink_metadata(&output)
   1802         && (metadata.file_type().is_symlink() || !metadata.is_dir())
   1803     {
   1804         return Err(ReleaseArtifactError::InvalidOutputRoot);
   1805     }
   1806     Ok((parent, output))
   1807 }
   1808 
   1809 fn validate_git_root(path: &Path) -> Result<PathBuf, ReleaseArtifactError> {
   1810     let root = validate_absolute_directory(path, ReleaseArtifactError::InvalidServiceRoot)?;
   1811     let top = git_stdout(
   1812         &root,
   1813         ["rev-parse", "--show-toplevel"],
   1814         MAX_GIT_OUTPUT_BYTES,
   1815     )
   1816     .map_err(|_| ReleaseArtifactError::InvalidServiceRoot)?;
   1817     let top = std::str::from_utf8(&top)
   1818         .map_err(|_| ReleaseArtifactError::InvalidServiceRoot)?
   1819         .trim();
   1820     let top = fs::canonicalize(top).map_err(|_| ReleaseArtifactError::InvalidServiceRoot)?;
   1821     if top == root {
   1822         Ok(root)
   1823     } else {
   1824         Err(ReleaseArtifactError::InvalidServiceRoot)
   1825     }
   1826 }
   1827 
   1828 fn validate_absolute_directory(
   1829     path: &Path,
   1830     error: ReleaseArtifactError,
   1831 ) -> Result<PathBuf, ReleaseArtifactError> {
   1832     if !path.is_absolute() {
   1833         return Err(error);
   1834     }
   1835     let metadata = fs::symlink_metadata(path).map_err(|_| error)?;
   1836     if metadata.file_type().is_symlink() || !metadata.is_dir() {
   1837         return Err(error);
   1838     }
   1839     fs::canonicalize(path).map_err(|_| error)
   1840 }
   1841 
   1842 fn validate_clean_git(root: &Path) -> Result<(), ReleaseArtifactError> {
   1843     let status = git_stdout(
   1844         root,
   1845         ["status", "--porcelain=v1", "-z", "--untracked-files=all"],
   1846         MAX_GIT_OUTPUT_BYTES,
   1847     )
   1848     .map_err(|_| ReleaseArtifactError::DirtyServiceSource)?;
   1849     if status.is_empty() {
   1850         Ok(())
   1851     } else {
   1852         Err(ReleaseArtifactError::DirtyServiceSource)
   1853     }
   1854 }
   1855 
   1856 fn git_head(root: &Path) -> Result<String, ReleaseArtifactError> {
   1857     let bytes = git_stdout(root, ["rev-parse", "HEAD"], 128)
   1858         .map_err(|_| ReleaseArtifactError::InvalidServiceRoot)?;
   1859     let value = std::str::from_utf8(&bytes)
   1860         .map_err(|_| ReleaseArtifactError::InvalidServiceRoot)?
   1861         .trim();
   1862     if valid_lower_hex(value, 40) {
   1863         Ok(value.to_owned())
   1864     } else {
   1865         Err(ReleaseArtifactError::InvalidServiceRoot)
   1866     }
   1867 }
   1868 
   1869 fn git_remote(root: &Path) -> Result<String, ReleaseArtifactError> {
   1870     let bytes = git_stdout(root, ["remote", "get-url", "origin"], MAX_GIT_OUTPUT_BYTES)
   1871         .map_err(|_| ReleaseArtifactError::InvalidServiceRoot)?;
   1872     let value = std::str::from_utf8(&bytes)
   1873         .map_err(|_| ReleaseArtifactError::InvalidServiceRoot)?
   1874         .trim();
   1875     if value.len() > MAX_TEXT_FIELD_BYTES
   1876         || !(value.starts_with("https://github.com/") || value.starts_with("ssh://git@github.com/"))
   1877         || value.contains(['\n', '\r'])
   1878     {
   1879         return Err(ReleaseArtifactError::InvalidServiceRoot);
   1880     }
   1881     Ok(value.to_owned())
   1882 }
   1883 
   1884 #[cfg(test)]
   1885 fn create_binary_archive(
   1886     source: &Path,
   1887     output: &Path,
   1888     binary_name: &str,
   1889     source_date_epoch: u32,
   1890 ) -> Result<FileEvidence, ReleaseArtifactError> {
   1891     let (_stable, stable_source) = binary_staging(output)?;
   1892     let source_evidence =
   1893         safe_artifact_io::copy_regular_to_new_path(source, &stable_source, MAX_BINARY_BYTES)
   1894             .map_err(|_| ReleaseArtifactError::InvalidInputArtifact)?;
   1895     if source_evidence.byte_length == 0 {
   1896         return Err(ReleaseArtifactError::InvalidInputArtifact);
   1897     }
   1898     write_binary_archive(
   1899         &stable_source,
   1900         output,
   1901         binary_name,
   1902         source_date_epoch,
   1903         source_evidence,
   1904     )
   1905 }
   1906 
   1907 fn create_binary_archive_from_snapshot(
   1908     snapshot: &safe_artifact_io::TraversalSnapshot,
   1909     source_name: &str,
   1910     output: &Path,
   1911     binary_name: &str,
   1912     target: &str,
   1913     source_date_epoch: u32,
   1914 ) -> Result<FileEvidence, ReleaseArtifactError> {
   1915     let source = snapshot_file(snapshot, source_name)?;
   1916     let (_stable, stable_source) = binary_staging(output)?;
   1917     let source_evidence = snapshot
   1918         .copy_to_new_path(source, &stable_source, MAX_BINARY_BYTES)
   1919         .map_err(|_| ReleaseArtifactError::InvalidInputArtifact)?;
   1920     if source_evidence.byte_length == 0 {
   1921         return Err(ReleaseArtifactError::InvalidInputArtifact);
   1922     }
   1923     artifact_admission::admit_binary(&stable_source, target, true)
   1924         .map_err(|_| ReleaseArtifactError::InvalidInputArtifact)?;
   1925     write_binary_archive(
   1926         &stable_source,
   1927         output,
   1928         binary_name,
   1929         source_date_epoch,
   1930         source_evidence,
   1931     )
   1932 }
   1933 
   1934 fn binary_staging(output: &Path) -> Result<(TempDir, PathBuf), ReleaseArtifactError> {
   1935     let parent = output
   1936         .parent()
   1937         .ok_or(ReleaseArtifactError::GenerationFailure)?;
   1938     let stable = tempfile::Builder::new()
   1939         .prefix(".radroots-binary-input-")
   1940         .tempdir_in(parent)
   1941         .map_err(|_| ReleaseArtifactError::GenerationFailure)?;
   1942     let stable_root = stable
   1943         .path()
   1944         .canonicalize()
   1945         .map_err(|_| ReleaseArtifactError::GenerationFailure)?;
   1946     Ok((stable, stable_root.join("admitted-binary")))
   1947 }
   1948 
   1949 fn write_binary_archive(
   1950     stable_source: &Path,
   1951     output: &Path,
   1952     binary_name: &str,
   1953     source_date_epoch: u32,
   1954     source_evidence: safe_artifact_io::FileEvidence,
   1955 ) -> Result<FileEvidence, ReleaseArtifactError> {
   1956     let mut source_file =
   1957         fs::File::open(stable_source).map_err(|_| ReleaseArtifactError::GenerationFailure)?;
   1958     let output_file = fs::OpenOptions::new()
   1959         .create_new(true)
   1960         .write(true)
   1961         .open(output)
   1962         .map_err(|_| ReleaseArtifactError::GenerationFailure)?;
   1963     let encoder = GzBuilder::new()
   1964         .mtime(source_date_epoch)
   1965         .operating_system(255)
   1966         .write(output_file, Compression::best());
   1967     let mut archive = TarBuilder::new(encoder);
   1968     let mut header = TarHeader::new_gnu();
   1969     header.set_size(source_evidence.byte_length);
   1970     header.set_mode(0o755);
   1971     header.set_uid(0);
   1972     header.set_gid(0);
   1973     header.set_mtime(u64::from(source_date_epoch));
   1974     header.set_cksum();
   1975     archive
   1976         .append_data(&mut header, format!("bin/{binary_name}"), &mut source_file)
   1977         .map_err(|_| ReleaseArtifactError::GenerationFailure)?;
   1978     let encoder = archive
   1979         .into_inner()
   1980         .map_err(|_| ReleaseArtifactError::GenerationFailure)?;
   1981     let output_file = encoder
   1982         .finish()
   1983         .map_err(|_| ReleaseArtifactError::GenerationFailure)?;
   1984     output_file
   1985         .sync_all()
   1986         .map_err(|_| ReleaseArtifactError::GenerationFailure)?;
   1987     set_file_mode(output)?;
   1988     admit_binary_archive(output)?;
   1989     hash_regular(output, MAX_BINARY_BYTES + MAX_TEXT_INPUT_BYTES)
   1990 }
   1991 
   1992 fn snapshot_file<'a>(
   1993     snapshot: &'a safe_artifact_io::TraversalSnapshot,
   1994     name: &str,
   1995 ) -> Result<&'a safe_artifact_io::TraversedFile, ReleaseArtifactError> {
   1996     snapshot
   1997         .files()
   1998         .iter()
   1999         .find(|file| file.relative_path() == Path::new(name))
   2000         .ok_or(ReleaseArtifactError::InvalidInputRoot)
   2001 }
   2002 
   2003 fn copy_snapshot_file(
   2004     snapshot: &safe_artifact_io::TraversalSnapshot,
   2005     source_name: &str,
   2006     output: &Path,
   2007     maximum: u64,
   2008 ) -> Result<FileEvidence, ReleaseArtifactError> {
   2009     let evidence = snapshot
   2010         .copy_to_new_path(snapshot_file(snapshot, source_name)?, output, maximum)
   2011         .map_err(|_| ReleaseArtifactError::InvalidInputArtifact)?;
   2012     set_file_mode(output)?;
   2013     Ok(FileEvidence {
   2014         byte_length: evidence.byte_length,
   2015         sha256: evidence.sha256,
   2016     })
   2017 }
   2018 
   2019 fn copy_bounded(
   2020     source: &Path,
   2021     output: &Path,
   2022     maximum: u64,
   2023 ) -> Result<FileEvidence, ReleaseArtifactError> {
   2024     let evidence = safe_artifact_io::copy_regular_to_new_path(source, output, maximum)
   2025         .map_err(|_| ReleaseArtifactError::InvalidInputArtifact)?;
   2026     set_file_mode(output)?;
   2027     Ok(FileEvidence {
   2028         byte_length: evidence.byte_length,
   2029         sha256: evidence.sha256,
   2030     })
   2031 }
   2032 
   2033 fn admit_binary_archive(path: &Path) -> Result<(), ReleaseArtifactError> {
   2034     let limits = TarGzipLimits {
   2035         max_compressed_bytes: MAX_BINARY_BYTES + MAX_TEXT_INPUT_BYTES,
   2036         max_expanded_bytes: MAX_BINARY_BYTES + MAX_TEXT_INPUT_BYTES,
   2037         max_members: 4,
   2038         max_member_bytes: MAX_BINARY_BYTES,
   2039         max_payload_bytes: MAX_BINARY_BYTES,
   2040         max_depth: 4,
   2041         max_path_bytes: MAX_ARCHIVE_PATH_BYTES,
   2042     };
   2043     safe_artifact_io::admit_tar_gzip_path(path, limits)
   2044         .map(|_| ())
   2045         .map_err(|_| ReleaseArtifactError::InvalidInputArtifact)
   2046 }
   2047 
   2048 #[derive(Default)]
   2049 struct SecretScanner {
   2050     tail: Vec<u8>,
   2051 }
   2052 
   2053 impl SecretScanner {
   2054     fn scan(&mut self, bytes: &[u8]) -> Result<(), ReleaseArtifactError> {
   2055         let mut combined = Vec::with_capacity(self.tail.len() + bytes.len());
   2056         combined.extend_from_slice(&self.tail);
   2057         combined.extend_from_slice(bytes);
   2058         if contains_secret(&combined) {
   2059             return Err(ReleaseArtifactError::ProtectedMaterialDetected);
   2060         }
   2061         let retained = SECRET_SCAN_OVERLAP_BYTES.min(combined.len());
   2062         self.tail.clear();
   2063         self.tail
   2064             .extend_from_slice(&combined[combined.len() - retained..]);
   2065         Ok(())
   2066     }
   2067 }
   2068 
   2069 fn contains_secret(bytes: &[u8]) -> bool {
   2070     let pem = [
   2071         (
   2072             b"-----BEGIN PRIVATE KEY-----".as_slice(),
   2073             b"-----END PRIVATE KEY-----".as_slice(),
   2074         ),
   2075         (
   2076             b"-----BEGIN RSA PRIVATE KEY-----".as_slice(),
   2077             b"-----END RSA PRIVATE KEY-----".as_slice(),
   2078         ),
   2079         (
   2080             b"-----BEGIN EC PRIVATE KEY-----".as_slice(),
   2081             b"-----END EC PRIVATE KEY-----".as_slice(),
   2082         ),
   2083         (
   2084             b"-----BEGIN OPENSSH PRIVATE KEY-----".as_slice(),
   2085             b"-----END OPENSSH PRIVATE KEY-----".as_slice(),
   2086         ),
   2087     ]
   2088     .iter()
   2089     .any(|(begin, end)| contains_pem_secret(bytes, begin, end));
   2090     let github_pat = contains_prefixed_secret(bytes, b"github_pat_", 50, 128, is_token_byte);
   2091     let ghp = contains_prefixed_secret(bytes, b"ghp_", 36, 36, |byte| byte.is_ascii_alphanumeric());
   2092     let slack = contains_prefixed_secret(bytes, b"xoxb-", 20, 128, |byte| {
   2093         byte.is_ascii_digit() || byte == b'-'
   2094     });
   2095     pem || github_pat || ghp || slack
   2096 }
   2097 
   2098 fn contains_pem_secret(bytes: &[u8], begin: &[u8], end: &[u8]) -> bool {
   2099     let Some(begin_at) = bytes
   2100         .windows(begin.len())
   2101         .position(|window| window == begin)
   2102     else {
   2103         return false;
   2104     };
   2105     let body = &bytes[begin_at + begin.len()..];
   2106     if !body.starts_with(b"\n") && !body.starts_with(b"\r\n") {
   2107         return false;
   2108     }
   2109     let Some(end_at) = body.windows(end.len()).position(|window| window == end) else {
   2110         return false;
   2111     };
   2112     let encoded = &body[..end_at];
   2113     encoded.iter().all(|byte| {
   2114         byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'/' | b'=' | b'\n' | b'\r')
   2115     }) && encoded
   2116         .iter()
   2117         .filter(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'/' | b'='))
   2118         .count()
   2119         >= 32
   2120 }
   2121 
   2122 fn contains_prefixed_secret(
   2123     bytes: &[u8],
   2124     prefix: &[u8],
   2125     minimum_suffix: usize,
   2126     maximum_suffix: usize,
   2127     valid_suffix: impl Fn(u8) -> bool,
   2128 ) -> bool {
   2129     bytes
   2130         .windows(prefix.len())
   2131         .enumerate()
   2132         .any(|(index, window)| {
   2133             if window != prefix
   2134                 || index
   2135                     .checked_sub(1)
   2136                     .is_some_and(|prior| is_token_byte(bytes[prior]))
   2137             {
   2138                 return false;
   2139             }
   2140             let start = index + prefix.len();
   2141             let suffix = bytes[start..]
   2142                 .iter()
   2143                 .take_while(|byte| valid_suffix(**byte))
   2144                 .count();
   2145             (minimum_suffix..=maximum_suffix).contains(&suffix)
   2146                 && bytes
   2147                     .get(start + suffix)
   2148                     .is_none_or(|byte| !is_token_byte(*byte))
   2149         })
   2150 }
   2151 
   2152 fn is_token_byte(byte: u8) -> bool {
   2153     byte.is_ascii_alphanumeric() || byte == b'_'
   2154 }
   2155 
   2156 fn scan_bytes(bytes: &[u8]) -> Result<(), ReleaseArtifactError> {
   2157     let mut scanner = SecretScanner::default();
   2158     scanner.scan(bytes)
   2159 }
   2160 
   2161 fn validate_text_artifact(path: &Path, name: &str) -> Result<(), ReleaseArtifactError> {
   2162     let bytes = read_bounded_regular(
   2163         path,
   2164         MAX_TEXT_INPUT_BYTES,
   2165         ReleaseArtifactError::InvalidInputArtifact,
   2166     )?;
   2167     std::str::from_utf8(&bytes).map_err(|_| ReleaseArtifactError::InvalidInputArtifact)?;
   2168     if bytes.contains(&0) {
   2169         return Err(ReleaseArtifactError::InvalidInputArtifact);
   2170     }
   2171     scan_bytes(&bytes)?;
   2172     if name == "config.schema.json" {
   2173         serde_json::from_slice::<serde_json::Value>(&bytes)
   2174             .map_err(|_| ReleaseArtifactError::InvalidInputArtifact)?;
   2175     } else if name == "config.example.toml" {
   2176         toml::from_str::<toml::Value>(
   2177             std::str::from_utf8(&bytes).map_err(|_| ReleaseArtifactError::InvalidInputArtifact)?,
   2178         )
   2179         .map_err(|_| ReleaseArtifactError::InvalidInputArtifact)?;
   2180     }
   2181     Ok(())
   2182 }
   2183 
   2184 fn write_json<T: Serialize>(path: &Path, value: &T) -> Result<(), ReleaseArtifactError> {
   2185     let mut bytes =
   2186         serde_json::to_vec(value).map_err(|_| ReleaseArtifactError::GenerationFailure)?;
   2187     bytes.push(b'\n');
   2188     write_generated(path, &bytes)
   2189 }
   2190 
   2191 fn write_generated(path: &Path, bytes: &[u8]) -> Result<(), ReleaseArtifactError> {
   2192     if bytes.is_empty() || bytes.len() as u64 > MAX_GENERATED_DOCUMENT_BYTES {
   2193         return Err(ReleaseArtifactError::GenerationFailure);
   2194     }
   2195     scan_bytes(bytes)?;
   2196     let mut file = fs::OpenOptions::new()
   2197         .create_new(true)
   2198         .write(true)
   2199         .open(path)
   2200         .map_err(|_| ReleaseArtifactError::GenerationFailure)?;
   2201     file.write_all(bytes)
   2202         .and_then(|()| file.sync_all())
   2203         .map_err(|_| ReleaseArtifactError::GenerationFailure)?;
   2204     set_file_mode(path)
   2205 }
   2206 
   2207 fn write_checksums(root: &Path) -> Result<(), ReleaseArtifactError> {
   2208     let mut records = inventory_records(root)?;
   2209     records.sort();
   2210     let mut checksums = String::new();
   2211     use fmt::Write as _;
   2212     for record in records {
   2213         writeln!(checksums, "{}  {}", record.sha256, record.path)
   2214             .map_err(|_| ReleaseArtifactError::GenerationFailure)?;
   2215     }
   2216     write_generated(&root.join("SHA256SUMS"), checksums.as_bytes())
   2217 }
   2218 
   2219 fn inventory_records(root: &Path) -> Result<Vec<ArtifactRecord>, ReleaseArtifactError> {
   2220     inventory_records_impl(root, || {})
   2221 }
   2222 
   2223 fn inventory_records_impl<F>(
   2224     root: &Path,
   2225     after_snapshot: F,
   2226 ) -> Result<Vec<ArtifactRecord>, ReleaseArtifactError>
   2227 where
   2228     F: FnOnce(),
   2229 {
   2230     let snapshot = flat_directory_snapshot(root, ReleaseArtifactError::GenerationFailure)?;
   2231     let names = snapshot_inventory(&snapshot, ReleaseArtifactError::GenerationFailure)?;
   2232     after_snapshot();
   2233     if snapshot.root_permission_mode() != DIRECTORY_MODE {
   2234         return Err(ReleaseArtifactError::GenerationFailure);
   2235     }
   2236     let mut records = Vec::with_capacity(names.len());
   2237     for name in names {
   2238         let maximum = output_maximum(&name)?;
   2239         let file = snapshot
   2240             .files()
   2241             .iter()
   2242             .find(|file| file.relative_path() == Path::new(&name))
   2243             .ok_or(ReleaseArtifactError::GenerationFailure)?;
   2244         if file.permission_mode() != FILE_MODE {
   2245             return Err(ReleaseArtifactError::GenerationFailure);
   2246         }
   2247         let evidence = snapshot
   2248             .hash(file, maximum)
   2249             .map_err(|_| ReleaseArtifactError::GenerationFailure)?;
   2250         records.push(artifact_record(
   2251             &name,
   2252             &FileEvidence {
   2253                 byte_length: evidence.byte_length,
   2254                 sha256: evidence.sha256,
   2255             },
   2256         ));
   2257     }
   2258     snapshot
   2259         .revalidate()
   2260         .map_err(|_| ReleaseArtifactError::GenerationFailure)?;
   2261     Ok(records)
   2262 }
   2263 
   2264 fn artifact_record(path: &str, evidence: &FileEvidence) -> ArtifactRecord {
   2265     ArtifactRecord {
   2266         path: path.to_owned(),
   2267         byte_length: evidence.byte_length,
   2268         sha256: evidence.sha256.clone(),
   2269     }
   2270 }
   2271 
   2272 fn artifact_record_from_exact_tree(
   2273     path: &str,
   2274     evidence: &exact_tree_archive::ExactTreeArchiveEvidence,
   2275 ) -> ArtifactRecord {
   2276     ArtifactRecord {
   2277         path: path.to_owned(),
   2278         byte_length: evidence.byte_length,
   2279         sha256: evidence.sha256.clone(),
   2280     }
   2281 }
   2282 
   2283 fn output_maximum(name: &str) -> Result<u64, ReleaseArtifactError> {
   2284     match name {
   2285         "binary.tar.gz" => Ok(MAX_BINARY_BYTES + MAX_TEXT_INPUT_BYTES),
   2286         "oci-image.tar.gz" => Ok(MAX_OCI_BYTES),
   2287         "service-source.tar" | "lib-source.tar" => Ok(MAX_SOURCE_ARCHIVE_BYTES),
   2288         "LICENSE-APACHE"
   2289         | "LICENSE-MIT"
   2290         | "config.example.toml"
   2291         | "config.schema.json"
   2292         | "nixos-module.nix"
   2293         | "radroots.service.source-lock.v3.toml"
   2294         | "systemd.service" => Ok(MAX_TEXT_INPUT_BYTES),
   2295         "SHA256SUMS"
   2296         | "THIRD-PARTY-LICENSES.txt"
   2297         | "THIRD-PARTY-NOTICES.txt"
   2298         | "artifact-manifest.v2.json"
   2299         | "artifact-scan.v1.json"
   2300         | "oci-image.v1.json"
   2301         | "provenance.intoto.jsonl"
   2302         | "sbom.cdx.json"
   2303         | "source-archives.v3.json" => Ok(MAX_GENERATED_DOCUMENT_BYTES),
   2304         _ => Err(ReleaseArtifactError::GenerationFailure),
   2305     }
   2306 }
   2307 
   2308 fn hash_regular(path: &Path, maximum: u64) -> Result<FileEvidence, ReleaseArtifactError> {
   2309     let evidence = safe_artifact_io::hash_regular_path(path, maximum)
   2310         .map_err(|_| ReleaseArtifactError::InvalidInputArtifact)?;
   2311     Ok(FileEvidence {
   2312         byte_length: evidence.byte_length,
   2313         sha256: evidence.sha256,
   2314     })
   2315 }
   2316 
   2317 fn read_bounded_regular(
   2318     path: &Path,
   2319     maximum: u64,
   2320     error: ReleaseArtifactError,
   2321 ) -> Result<Vec<u8>, ReleaseArtifactError> {
   2322     safe_artifact_io::read_regular_path(path, maximum).map_err(|_| error)
   2323 }
   2324 
   2325 #[cfg(test)]
   2326 fn validate_regular_input(path: &Path, maximum: u64) -> Result<fs::Metadata, ReleaseArtifactError> {
   2327     let metadata =
   2328         fs::symlink_metadata(path).map_err(|_| ReleaseArtifactError::InvalidInputArtifact)?;
   2329     if metadata.file_type().is_symlink()
   2330         || !metadata.is_file()
   2331         || metadata.len() == 0
   2332         || metadata.len() > maximum
   2333     {
   2334         Err(ReleaseArtifactError::InvalidInputArtifact)
   2335     } else {
   2336         Ok(metadata)
   2337     }
   2338 }
   2339 
   2340 #[cfg(all(test, unix))]
   2341 fn validate_unchanged_input(
   2342     path: &Path,
   2343     expected: &fs::Metadata,
   2344 ) -> Result<(), ReleaseArtifactError> {
   2345     use std::os::unix::fs::MetadataExt as _;
   2346     let current =
   2347         fs::symlink_metadata(path).map_err(|_| ReleaseArtifactError::InvalidInputArtifact)?;
   2348     if current.file_type().is_symlink()
   2349         || !current.is_file()
   2350         || current.dev() != expected.dev()
   2351         || current.ino() != expected.ino()
   2352         || current.len() != expected.len()
   2353     {
   2354         Err(ReleaseArtifactError::InvalidInputArtifact)
   2355     } else {
   2356         Ok(())
   2357     }
   2358 }
   2359 
   2360 #[cfg(all(test, not(unix)))]
   2361 fn validate_unchanged_input(
   2362     path: &Path,
   2363     expected: &fs::Metadata,
   2364 ) -> Result<(), ReleaseArtifactError> {
   2365     let current =
   2366         fs::symlink_metadata(path).map_err(|_| ReleaseArtifactError::InvalidInputArtifact)?;
   2367     if current.file_type().is_symlink() || !current.is_file() || current.len() != expected.len() {
   2368         Err(ReleaseArtifactError::InvalidInputArtifact)
   2369     } else {
   2370         Ok(())
   2371     }
   2372 }
   2373 
   2374 #[cfg(test)]
   2375 fn directory_inventory(
   2376     root: &Path,
   2377     error: ReleaseArtifactError,
   2378 ) -> Result<BTreeSet<String>, ReleaseArtifactError> {
   2379     let snapshot = flat_directory_snapshot(root, error)?;
   2380     snapshot_inventory(&snapshot, error)
   2381 }
   2382 
   2383 fn flat_directory_snapshot(
   2384     root: &Path,
   2385     error: ReleaseArtifactError,
   2386 ) -> Result<safe_artifact_io::TraversalSnapshot, ReleaseArtifactError> {
   2387     let limits = TraversalLimits {
   2388         max_entries: (OUTPUT_NAMES.len() + 1) as u64,
   2389         max_files: (OUTPUT_NAMES.len() + 1) as u64,
   2390         max_total_bytes: MAX_RELEASE_TREE_BYTES,
   2391         max_file_bytes: MAX_ARCHIVE_EXPANDED_BYTES,
   2392         max_depth: 1,
   2393         max_path_bytes: MAX_ARCHIVE_PATH_BYTES,
   2394     };
   2395     safe_artifact_io::traverse_regular_files(root, limits, &[]).map_err(|_| error)
   2396 }
   2397 
   2398 fn snapshot_inventory(
   2399     snapshot: &safe_artifact_io::TraversalSnapshot,
   2400     error: ReleaseArtifactError,
   2401 ) -> Result<BTreeSet<String>, ReleaseArtifactError> {
   2402     if snapshot.entry_count() != snapshot.files().len() as u64 {
   2403         return Err(error);
   2404     }
   2405     let mut names = BTreeSet::new();
   2406     for entry in snapshot.files() {
   2407         let path = entry.relative_path();
   2408         if path.components().count() != 1 {
   2409             return Err(error);
   2410         }
   2411         let name = path
   2412             .file_name()
   2413             .and_then(OsStr::to_str)
   2414             .ok_or(error)?
   2415             .to_owned();
   2416         if !names.insert(name) || names.len() > OUTPUT_NAMES.len() {
   2417             return Err(error);
   2418         }
   2419     }
   2420     snapshot.revalidate().map_err(|_| error)?;
   2421     Ok(names)
   2422 }
   2423 
   2424 fn validate_exact_output_inventory(root: &Path) -> Result<(), ReleaseArtifactError> {
   2425     let expected = OUTPUT_NAMES.into_iter().map(str::to_owned).collect();
   2426     let actual = inventory_records(root)?
   2427         .into_iter()
   2428         .map(|record| record.path)
   2429         .collect::<BTreeSet<_>>();
   2430     if actual != expected {
   2431         return Err(ReleaseArtifactError::GenerationFailure);
   2432     }
   2433     Ok(())
   2434 }
   2435 
   2436 fn compare_output(expected: &Path, actual: &Path) -> Result<(), ReleaseArtifactError> {
   2437     let actual = validate_absolute_directory(actual, ReleaseArtifactError::StaleOutput)?;
   2438     let expected_records =
   2439         inventory_records(expected).map_err(|_| ReleaseArtifactError::StaleOutput)?;
   2440     let actual_records =
   2441         inventory_records(&actual).map_err(|_| ReleaseArtifactError::StaleOutput)?;
   2442     if expected_records != actual_records {
   2443         return Err(ReleaseArtifactError::StaleOutput);
   2444     }
   2445     Ok(())
   2446 }
   2447 
   2448 fn validate_output_records(
   2449     actual: &Path,
   2450     expected: &[ArtifactRecord],
   2451 ) -> Result<(), ReleaseArtifactError> {
   2452     let actual = validate_absolute_directory(actual, ReleaseArtifactError::StaleOutput)?;
   2453     validate_exact_output_inventory(&actual).map_err(|_| ReleaseArtifactError::StaleOutput)?;
   2454     let actual_records =
   2455         inventory_records(&actual).map_err(|_| ReleaseArtifactError::StaleOutput)?;
   2456     if actual_records == expected {
   2457         Ok(())
   2458     } else {
   2459         Err(ReleaseArtifactError::StaleOutput)
   2460     }
   2461 }
   2462 
   2463 #[cfg(unix)]
   2464 fn set_file_mode(path: &Path) -> Result<(), ReleaseArtifactError> {
   2465     use std::os::unix::fs::PermissionsExt as _;
   2466     fs::set_permissions(path, fs::Permissions::from_mode(FILE_MODE))
   2467         .map_err(|_| ReleaseArtifactError::GenerationFailure)
   2468 }
   2469 
   2470 #[cfg(not(unix))]
   2471 fn set_file_mode(_path: &Path) -> Result<(), ReleaseArtifactError> {
   2472     Ok(())
   2473 }
   2474 
   2475 #[cfg(unix)]
   2476 fn set_directory_mode(path: &Path) -> Result<(), ReleaseArtifactError> {
   2477     use std::os::unix::fs::PermissionsExt as _;
   2478     fs::set_permissions(path, fs::Permissions::from_mode(DIRECTORY_MODE))
   2479         .map_err(|_| ReleaseArtifactError::GenerationFailure)
   2480 }
   2481 
   2482 #[cfg(not(unix))]
   2483 fn set_directory_mode(_path: &Path) -> Result<(), ReleaseArtifactError> {
   2484     Ok(())
   2485 }
   2486 
   2487 #[cfg(all(test, unix))]
   2488 fn validate_file_mode(path: &Path) -> Result<(), ReleaseArtifactError> {
   2489     use std::os::unix::fs::PermissionsExt as _;
   2490     let mode = fs::symlink_metadata(path)
   2491         .map_err(|_| ReleaseArtifactError::StaleOutput)?
   2492         .permissions()
   2493         .mode()
   2494         & 0o7777;
   2495     if mode == FILE_MODE {
   2496         Ok(())
   2497     } else {
   2498         Err(ReleaseArtifactError::StaleOutput)
   2499     }
   2500 }
   2501 
   2502 #[cfg(all(test, not(unix)))]
   2503 fn validate_file_mode(_path: &Path) -> Result<(), ReleaseArtifactError> {
   2504     Ok(())
   2505 }
   2506 
   2507 #[cfg(all(test, unix))]
   2508 fn validate_directory_mode(path: &Path) -> Result<(), ReleaseArtifactError> {
   2509     use std::os::unix::fs::PermissionsExt as _;
   2510     let mode = fs::symlink_metadata(path)
   2511         .map_err(|_| ReleaseArtifactError::StaleOutput)?
   2512         .permissions()
   2513         .mode()
   2514         & 0o7777;
   2515     if mode == DIRECTORY_MODE {
   2516         Ok(())
   2517     } else {
   2518         Err(ReleaseArtifactError::StaleOutput)
   2519     }
   2520 }
   2521 
   2522 #[cfg(all(test, not(unix)))]
   2523 fn validate_directory_mode(_path: &Path) -> Result<(), ReleaseArtifactError> {
   2524     Ok(())
   2525 }
   2526 
   2527 fn sync_directory(path: &Path) -> Result<(), ReleaseArtifactError> {
   2528     fs::File::open(path)
   2529         .and_then(|directory| directory.sync_all())
   2530         .map_err(|_| ReleaseArtifactError::GenerationFailure)
   2531 }
   2532 
   2533 fn command_stdout(command: &mut Command, maximum: usize) -> Result<Vec<u8>, ()> {
   2534     let mut child = command
   2535         .stdin(Stdio::null())
   2536         .stdout(Stdio::piped())
   2537         .stderr(Stdio::null())
   2538         .spawn()
   2539         .map_err(|_| ())?;
   2540     let mut stdout = child.stdout.take().ok_or(())?;
   2541     let mut bytes = Vec::new();
   2542     if stdout
   2543         .by_ref()
   2544         .take(maximum as u64 + 1)
   2545         .read_to_end(&mut bytes)
   2546         .is_err()
   2547         || bytes.len() > maximum
   2548     {
   2549         let _ = child.kill();
   2550         let _ = child.wait();
   2551         return Err(());
   2552     }
   2553     let status = child.wait().map_err(|_| ())?;
   2554     if status.success() { Ok(bytes) } else { Err(()) }
   2555 }
   2556 
   2557 fn git_stdout<const N: usize>(root: &Path, args: [&str; N], maximum: usize) -> Result<Vec<u8>, ()> {
   2558     let mut command = Command::new("git");
   2559     command.args(args).current_dir(root);
   2560     command_stdout(&mut command, maximum)
   2561 }
   2562 
   2563 fn valid_metadata_text(value: &str) -> bool {
   2564     !value.is_empty()
   2565         && value.len() <= MAX_TEXT_FIELD_BYTES
   2566         && !value.contains(['\n', '\r'])
   2567         && !SECRET_PATTERNS.iter().any(|pattern| {
   2568             value
   2569                 .as_bytes()
   2570                 .windows(pattern.len())
   2571                 .any(|window| window == *pattern)
   2572         })
   2573         && scan_bytes(value.as_bytes()).is_ok()
   2574 }
   2575 
   2576 fn valid_public_source(value: &str) -> bool {
   2577     valid_metadata_text(value)
   2578         && (value.starts_with("registry+https://") || value.starts_with("git+https://"))
   2579         && !value.contains("@github.com")
   2580 }
   2581 
   2582 fn valid_snake_identifier(value: &str) -> bool {
   2583     value.len() <= 128
   2584         && value
   2585             .split('_')
   2586             .all(|segment| !segment.is_empty() && valid_lower_alphanumeric(segment))
   2587         && value.as_bytes().first().is_some_and(u8::is_ascii_lowercase)
   2588         && value
   2589             .as_bytes()
   2590             .last()
   2591             .is_some_and(u8::is_ascii_alphanumeric)
   2592 }
   2593 
   2594 fn valid_kebab_identifier(value: &str) -> bool {
   2595     value.len() <= 128
   2596         && value
   2597             .split('-')
   2598             .all(|segment| !segment.is_empty() && valid_lower_alphanumeric(segment))
   2599         && value.as_bytes().first().is_some_and(u8::is_ascii_lowercase)
   2600         && value
   2601             .as_bytes()
   2602             .last()
   2603             .is_some_and(u8::is_ascii_alphanumeric)
   2604 }
   2605 
   2606 fn valid_lower_alphanumeric(value: &str) -> bool {
   2607     value
   2608         .bytes()
   2609         .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit())
   2610 }
   2611 
   2612 fn valid_output_component(value: &str) -> bool {
   2613     let mut components = Path::new(value).components();
   2614     !value.is_empty()
   2615         && value.len() <= 128
   2616         && !value.contains(['/', '\\'])
   2617         && matches!(components.next(), Some(Component::Normal(_)))
   2618         && components.next().is_none()
   2619         && value != "."
   2620         && value != ".."
   2621 }
   2622 
   2623 fn valid_lower_hex(value: &str, length: usize) -> bool {
   2624     value.len() == length
   2625         && value
   2626             .bytes()
   2627             .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
   2628 }
   2629 
   2630 fn sha256_bytes(bytes: &[u8]) -> String {
   2631     hex::encode(Sha256::digest(bytes))
   2632 }
   2633 
   2634 pub(crate) fn validate_contract(workspace_root: &Path) -> Result<(), String> {
   2635     validate_contract_inner(workspace_root).map_err(|error| error.to_string())
   2636 }
   2637 
   2638 fn validate_contract_inner(workspace_root: &Path) -> Result<(), ReleaseArtifactError> {
   2639     let bytes = read_bounded_regular(
   2640         &workspace_root.join(CONTRACT_RELATIVE),
   2641         MAX_CONTRACT_BYTES as u64,
   2642         ReleaseArtifactError::InvalidContract,
   2643     )?;
   2644     let decision = serde_json::from_slice::<ReleaseDecision>(&bytes)
   2645         .map_err(|_| ReleaseArtifactError::InvalidContract)?;
   2646     validate_decision(&decision)
   2647 }
   2648 
   2649 fn validate_decision(decision: &ReleaseDecision) -> Result<(), ReleaseArtifactError> {
   2650     let errors = [
   2651         ReleaseArtifactError::InvalidContract,
   2652         ReleaseArtifactError::InvalidServiceRoot,
   2653         ReleaseArtifactError::DirtyServiceSource,
   2654         ReleaseArtifactError::InvalidServiceMetadata,
   2655         ReleaseArtifactError::InvalidInputRoot,
   2656         ReleaseArtifactError::InvalidInputArtifact,
   2657         ReleaseArtifactError::InvalidSourceLock,
   2658         ReleaseArtifactError::InvalidSourceBundle,
   2659         ReleaseArtifactError::InvalidPackageInventory,
   2660         ReleaseArtifactError::ProtectedMaterialDetected,
   2661         ReleaseArtifactError::InvalidOutputRoot,
   2662         ReleaseArtifactError::StaleOutput,
   2663         ReleaseArtifactError::GenerationFailure,
   2664     ];
   2665     if decision.schema != "radroots.services-hardening.release-artifacts-decisions.v4"
   2666         || decision.contract_version != 4
   2667         || decision.decision_state != "active"
   2668         || decision.owner_step != 305
   2669         || decision.predecessor.schema
   2670             != "radroots.services-hardening.release-artifacts-decisions.v3"
   2671         || decision.predecessor.filename != "services_hardening_release_artifacts.v3.json"
   2672         || decision.predecessor.transition != "forward_only_replace"
   2673         || decision.command != "cargo xtask service-release-artifacts"
   2674         || decision.modes != ["check", "write"]
   2675         || decision.required_arguments
   2676             != [
   2677                 "mode",
   2678                 "service_root",
   2679                 "lib_root",
   2680                 "input_root",
   2681                 "output_root",
   2682                 "target",
   2683                 "source_date_epoch",
   2684                 "candidate_digest",
   2685             ]
   2686         || decision.service_metadata_path
   2687             != "Cargo.toml.workspace.metadata.radroots.service_release"
   2688         || decision.service_metadata_fields
   2689             != ["service", "service_package", "binary_name", "version"]
   2690         || decision.service_license_path
   2691             != "Cargo.toml.workspace.package.license_or_package.license"
   2692         || decision.source_lock_schema != "radroots.service.source-lock.v3"
   2693         || decision.source_lock_definition
   2694             != "contracts/architecture/decisions/services_hardening_source_lock.v3.json"
   2695         || decision.artifact_contract_binding
   2696             != "source_lock_exact_regular_file_bytes_in_same_service_revision"
   2697         || decision.artifact_admission_contract
   2698             != "contracts/architecture/decisions/services_hardening_artifact_admission.v1.json"
   2699         || decision.supported_targets != SUPPORTED_TARGETS
   2700         || decision.candidate_binding != "explicit_sha256_candidate_identity_digest"
   2701         || decision.lib_root_binding
   2702             != "canonical_public_lib_git_root_containing_the_locked_revision"
   2703         || decision.binary_admission
   2704             != "exact_format_architecture_linkage_structural_and_native_bounded_help_smoke"
   2705         || decision.oci_admission
   2706             != "safe_materialization_exact_manifest_config_AGPL_labels_layers_and_entrypoint"
   2707         || decision.input_inventory != INPUT_NAMES
   2708         || decision.excluded_parent_owned_inputs != ["backup_restore_runbook", "operator_runbook"]
   2709         || decision.service_root_inventory != ["LICENSE-APACHE", "LICENSE-MIT", LOCK_FILENAME]
   2710         || decision.output_inventory != OUTPUT_NAMES
   2711         || decision.canonical_json != "compact_utf8_json_with_one_final_lf"
   2712         || decision.checksum_format != "sha256_lower_hex_two_spaces_path_lf_sorted_by_path"
   2713         || decision.source_archive_format
   2714             != "canonical_uncompressed_ustar_exact_git_revision_tree_without_history"
   2715         || decision.sbom_format != "cyclonedx_json_1_6_complete_cargo_nix_and_artifact_closure"
   2716         || decision.license_evidence != "exact_dependency_attribution_with_bounded_license_texts"
   2717         || decision.provenance_posture
   2718             != "candidate_derived_unsigned_intoto_statement_slsa_v1_exact_manifest_subjects"
   2719         || decision.protected_material_scan_scope
   2720             != "all_artifact_bytes_and_bounded_nested_binary_oci_layer_and_source_archive_payloads"
   2721         || decision.confidentiality_state != "derived_only_from_the_exact_artifact_scan_record"
   2722         || decision.source_cleanliness != "no_tracked_staged_or_untracked_changes"
   2723         || decision.revision_stability != "same_service_head_before_and_after_generation"
   2724         || !decision.no_protected_material
   2725         || decision.maximums.text_input_bytes != MAX_TEXT_INPUT_BYTES
   2726         || decision.maximums.generated_document_bytes != MAX_GENERATED_DOCUMENT_BYTES
   2727         || decision.maximums.service_cargo_lock_bytes != MAX_SERVICE_CARGO_LOCK_BYTES
   2728         || decision.maximums.service_flake_lock_bytes != MAX_SERVICE_FLAKE_LOCK_BYTES
   2729         || decision.maximums.binary_bytes != MAX_BINARY_BYTES
   2730         || decision.maximums.source_archive_bytes != MAX_SOURCE_ARCHIVE_BYTES
   2731         || decision.maximums.source_archive_member_bytes != MAX_SOURCE_ARCHIVE_MEMBER_BYTES
   2732         || decision.maximums.source_archive_members != MAX_SOURCE_ARCHIVE_MEMBERS
   2733         || decision.maximums.oci_bytes != MAX_OCI_BYTES
   2734         || decision.maximums.artifact_scan_expanded_bytes != MAX_ARCHIVE_EXPANDED_BYTES
   2735         || decision.maximums.cargo_metadata_bytes != MAX_METADATA_BYTES
   2736         || decision.maximums.packages != MAX_PACKAGES
   2737         || decision.maximums.workspace_packages != MAX_WORKSPACE_PACKAGES
   2738         || decision.required_negative_vectors
   2739             != [
   2740                 "cyclonedx_schema_drift",
   2741                 "missing_dependency_component",
   2742                 "unreconciled_artifact_subject",
   2743                 "missing_or_mismatched_license_text",
   2744                 "invented_candidate_digest",
   2745                 "git_history_bundle",
   2746                 "secret_in_binary",
   2747                 "secret_in_oci_layer",
   2748                 "secret_in_source_archive",
   2749                 "sensitive_archive_path",
   2750                 "scan_confidentiality_mismatch",
   2751             ]
   2752         || decision.negative_error_codes != errors.map(ReleaseArtifactError::code)
   2753     {
   2754         return Err(ReleaseArtifactError::InvalidContract);
   2755     }
   2756     Ok(())
   2757 }
   2758 
   2759 #[cfg(test)]
   2760 mod tests {
   2761     use serde_json::{Value, json};
   2762     use std::process::Command;
   2763 
   2764     use crate::service_source_lock::ContractVersions;
   2765     use crate::service_source_lock_v3::FixtureParts;
   2766 
   2767     use super::*;
   2768 
   2769     struct ReleaseFixture {
   2770         _root: TempDir,
   2771         service: PathBuf,
   2772         lib: PathBuf,
   2773         input: PathBuf,
   2774         output_a: PathBuf,
   2775         output_b: PathBuf,
   2776     }
   2777 
   2778     impl ReleaseFixture {
   2779         fn new() -> Self {
   2780             let root = TempDir::new().expect("fixture root");
   2781             let canonical_root = root.path().canonicalize().expect("canonical fixture root");
   2782             let service = canonical_root.join("service");
   2783             let lib = canonical_root.join("lib");
   2784             let input = canonical_root.join("input");
   2785             fs::create_dir_all(service.join("src")).expect("service source");
   2786             fs::create_dir_all(lib.join("contracts/crates")).expect("Lib contracts");
   2787             fs::create_dir(&input).expect("input root");
   2788 
   2789             write_file(
   2790                 &lib.join("contracts/crates/catalog.v2.toml"),
   2791                 b"schema_version = 2\n",
   2792             );
   2793             write_file(&lib.join("README.md"), b"fixture Lib source\n");
   2794             initialize_git(&lib, "https://github.com/radrootslabs/lib");
   2795             let lib_revision = git_output(&lib, &["rev-parse", "HEAD"]);
   2796             let lib_archive_path = canonical_root.join("lib-source.tar");
   2797             let lib_archive = exact_tree_archive::create(
   2798                 &lib,
   2799                 &lib_revision,
   2800                 &lib_archive_path,
   2801                 exact_tree_archive::commit_timestamp(&lib, &lib_revision)
   2802                     .expect("Lib commit timestamp"),
   2803             )
   2804             .expect("Lib source archive");
   2805             let catalog =
   2806                 fs::read(lib.join("contracts/crates/catalog.v2.toml")).expect("workspace catalog");
   2807 
   2808             write_file(
   2809                 &service.join("Cargo.toml"),
   2810                 br#"[package]
   2811 name = "fixture-service"
   2812 version = "0.1.0-alpha"
   2813 edition = "2024"
   2814 license = "AGPL-3.0-or-later"
   2815 
   2816 [[bin]]
   2817 name = "fixture-service"
   2818 path = "src/main.rs"
   2819 
   2820 [workspace]
   2821 resolver = "3"
   2822 
   2823 [workspace.metadata.radroots.service_release]
   2824 service = "myc"
   2825 service_package = "fixture-service"
   2826 binary_name = "fixture-service"
   2827 version = "0.1.0-alpha"
   2828 "#,
   2829             );
   2830             write_file(&service.join("src/main.rs"), b"fn main() {}\n");
   2831             write_file(
   2832                 &service.join("Cargo.lock"),
   2833                 br#"# This file is automatically @generated by Cargo.
   2834 # It is not intended for manual editing.
   2835 version = 4
   2836 
   2837 [[package]]
   2838 name = "fixture-service"
   2839 version = "0.1.0-alpha"
   2840 "#,
   2841             );
   2842             write_file(
   2843                 &service.join("flake.nix"),
   2844                 format!(
   2845                     "{{\n  inputs.lib = {{\n    url = \"github:radrootslabs/lib/{lib_revision}\";\n    flake = false;\n  }};\n  outputs = {{ ... }}: {{ }};\n}}\n"
   2846                 )
   2847                 .as_bytes(),
   2848             );
   2849             write_file(
   2850                 &service.join("flake.lock"),
   2851                 format!(
   2852                     "{{\"nodes\":{{\"root\":{{\"inputs\":{{\"lib\":\"lib\"}}}},\"lib\":{{\"locked\":{{\"lastModified\":1,\"narHash\":\"sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\",\"owner\":\"radrootslabs\",\"repo\":\"lib\",\"rev\":\"{lib_revision}\",\"type\":\"github\"}},\"original\":{{\"owner\":\"radrootslabs\",\"repo\":\"lib\",\"rev\":\"{lib_revision}\",\"type\":\"github\"}}}}}},\"root\":\"root\",\"version\":7}}\n"
   2853                 )
   2854                 .as_bytes(),
   2855             );
   2856             write_file(
   2857                 &service.join("LICENSE-APACHE"),
   2858                 b"Apache-2.0 fixture license\n",
   2859             );
   2860             write_file(&service.join("LICENSE-MIT"), b"MIT fixture license\n");
   2861             let artifact_contract_path =
   2862                 service.join("contracts/release/myc-artifact-contract.v3.json");
   2863             fs::create_dir_all(
   2864                 artifact_contract_path
   2865                     .parent()
   2866                     .expect("artifact contract parent"),
   2867             )
   2868             .expect("artifact contract directory");
   2869             write_file(&artifact_contract_path, b"{}\n");
   2870             let cargo_lock = fs::read(service.join("Cargo.lock")).expect("Cargo lock");
   2871             let flake_lock = fs::read(service.join("flake.lock")).expect("flake lock");
   2872             let artifact_contract = fs::read(&artifact_contract_path).expect("artifact contract");
   2873             let source_lock = ServiceSourceLockV3::fixture(FixtureParts {
   2874                 service: "myc",
   2875                 revision: &lib_revision,
   2876                 workspace_catalog_sha256: &sha256_bytes(&catalog),
   2877                 source_archive_sha256: &lib_archive.sha256,
   2878                 cargo_lock_sha256: &sha256_bytes(&cargo_lock),
   2879                 flake_lock_sha256: &sha256_bytes(&flake_lock),
   2880                 artifact_contract_sha256: &sha256_bytes(&artifact_contract),
   2881                 contract_versions: ContractVersions::new(1, 1, 1, 1, 1),
   2882             })
   2883             .expect("source lock");
   2884             write_file(&service.join(LOCK_FILENAME), source_lock.canonical_bytes());
   2885             initialize_git(&service, "https://github.com/radrootslabs/fixture-service");
   2886             let service_revision = git_output(&service, &["rev-parse", "HEAD"]);
   2887             for (name, bytes) in [
   2888                 ("config.example.toml", b"enabled = true\n".as_slice()),
   2889                 ("config.schema.json", b"{\"type\":\"object\"}\n".as_slice()),
   2890                 ("nixos-module.nix", b"{ ... }: {}\n".as_slice()),
   2891                 (
   2892                     "systemd.service",
   2893                     b"[Service]\nExecStart=/usr/bin/fixture-service\n".as_slice(),
   2894                 ),
   2895             ] {
   2896                 write_file(&input.join(name), bytes);
   2897             }
   2898             let status = Command::new("rustc")
   2899                 .args(["--edition=2024", "src/main.rs", "-o"])
   2900                 .arg(input.join("service-binary"))
   2901                 .current_dir(&service)
   2902                 .status()
   2903                 .expect("compile fixture service binary");
   2904             assert!(status.success(), "compile fixture service binary");
   2905             create_oci_fixture(
   2906                 &input.join("oci-image.tar.gz"),
   2907                 &service_revision,
   2908                 &lib_revision,
   2909             );
   2910 
   2911             Self {
   2912                 output_a: canonical_root.join("release-a"),
   2913                 output_b: canonical_root.join("release-b"),
   2914                 _root: root,
   2915                 service,
   2916                 lib,
   2917                 input,
   2918             }
   2919         }
   2920 
   2921         fn write(&self, output: &Path) -> Result<(), ReleaseArtifactError> {
   2922             run_inner(Arguments {
   2923                 mode: CommandMode::Write,
   2924                 service_root: &self.service,
   2925                 lib_root: &self.lib,
   2926                 input_root: &self.input,
   2927                 output_root: output,
   2928                 target: native_fixture_target(),
   2929                 source_date_epoch: 1_700_000_000,
   2930                 candidate_digest: &"a".repeat(64),
   2931             })
   2932         }
   2933 
   2934         fn check(&self, output: &Path) -> Result<(), ReleaseArtifactError> {
   2935             run_inner(Arguments {
   2936                 mode: CommandMode::Check,
   2937                 service_root: &self.service,
   2938                 lib_root: &self.lib,
   2939                 input_root: &self.input,
   2940                 output_root: output,
   2941                 target: native_fixture_target(),
   2942                 source_date_epoch: 1_700_000_000,
   2943                 candidate_digest: &"a".repeat(64),
   2944             })
   2945         }
   2946     }
   2947 
   2948     fn write_file(path: &Path, bytes: &[u8]) {
   2949         fs::write(path, bytes).expect("write fixture file");
   2950     }
   2951 
   2952     fn git(root: &Path, args: &[&str]) {
   2953         let status = Command::new("git")
   2954             .args(args)
   2955             .current_dir(root)
   2956             .status()
   2957             .expect("run Git");
   2958         assert!(status.success(), "git {args:?}");
   2959     }
   2960 
   2961     fn git_output(root: &Path, args: &[&str]) -> String {
   2962         let output = Command::new("git")
   2963             .args(args)
   2964             .current_dir(root)
   2965             .output()
   2966             .expect("run Git");
   2967         assert!(output.status.success(), "git {args:?}");
   2968         String::from_utf8(output.stdout)
   2969             .expect("Git UTF-8")
   2970             .trim()
   2971             .to_owned()
   2972     }
   2973 
   2974     fn initialize_git(root: &Path, remote: &str) {
   2975         git(root, &["init", "--quiet"]);
   2976         git(root, &["config", "user.email", "fixture@radroots.test"]);
   2977         git(root, &["config", "user.name", "Radroots Fixture"]);
   2978         git(root, &["remote", "add", "origin", remote]);
   2979         git(root, &["add", "."]);
   2980         git(root, &["commit", "--quiet", "-m", "fixture"]);
   2981         git(root, &["branch", "-M", "archive"]);
   2982     }
   2983 
   2984     fn native_fixture_target() -> &'static str {
   2985         if cfg!(all(target_os = "macos", target_arch = "aarch64")) {
   2986             "aarch64-apple-darwin"
   2987         } else {
   2988             "x86_64-unknown-linux-gnu"
   2989         }
   2990     }
   2991 
   2992     fn create_oci_fixture(output: &Path, service_revision: &str, lib_revision: &str) {
   2993         let entrypoint =
   2994             "/nix/store/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-fixture-service/bin/fixture-service";
   2995         let mut layer = Vec::new();
   2996         {
   2997             let mut tar = TarBuilder::new(&mut layer);
   2998             let bytes = b"fixture executable";
   2999             let mut header = TarHeader::new_gnu();
   3000             header.set_size(bytes.len() as u64);
   3001             header.set_mode(0o755);
   3002             header.set_uid(0);
   3003             header.set_gid(0);
   3004             header.set_mtime(0);
   3005             header.set_cksum();
   3006             tar.append_data(
   3007                 &mut header,
   3008                 entrypoint.trim_start_matches('/'),
   3009                 bytes.as_slice(),
   3010             )
   3011             .expect("write layer entrypoint");
   3012             tar.finish().expect("finish layer");
   3013         }
   3014         let layer_digest = sha256_bytes(&layer);
   3015         let layer_name = format!("{layer_digest}/layer.tar");
   3016         let labels = artifact_admission::OciExpectation {
   3017             service: "myc",
   3018             binary_name: "fixture-service",
   3019             version: "0.1.0-alpha",
   3020             service_revision,
   3021             lib_revision,
   3022             license: "AGPL-3.0-or-later",
   3023             contract_versions: artifact_admission::ContractVersions {
   3024                 admin: 1,
   3025                 config: 1,
   3026                 provider: 1,
   3027                 state: 1,
   3028                 status: 1,
   3029             },
   3030         };
   3031         let labels = artifact_admission::fixture_labels(&labels);
   3032         let config = serde_json::to_vec(&serde_json::json!({
   3033             "architecture": "amd64",
   3034             "config": {
   3035                 "Entrypoint": [entrypoint],
   3036                 "Env": ["SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt"],
   3037                 "Labels": labels,
   3038                 "StopSignal": "SIGTERM",
   3039                 "User": "65532:65532",
   3040                 "WorkingDir": "/"
   3041             },
   3042             "created": "1970-01-01T00:00:01+00:00",
   3043             "os": "linux",
   3044             "rootfs": {"diff_ids": [format!("sha256:{layer_digest}")], "type": "layers"}
   3045         }))
   3046         .expect("serialize config");
   3047         let config_name = format!("{}.json", sha256_bytes(&config));
   3048         let manifest = serde_json::to_vec(&serde_json::json!([{
   3049             "Config": config_name,
   3050             "Layers": [layer_name],
   3051             "RepoTags": ["myc:0.1.0-alpha"]
   3052         }]))
   3053         .expect("serialize manifest");
   3054         let repositories = serde_json::to_vec(&serde_json::json!({
   3055             "myc": {"0.1.0-alpha": layer_digest}
   3056         }))
   3057         .expect("serialize repositories");
   3058         let mut members = vec![
   3059             (config_name, false, config),
   3060             (format!("{layer_digest}/"), true, Vec::new()),
   3061             (format!("{layer_digest}/VERSION"), false, b"1.0".to_vec()),
   3062             (format!("{layer_digest}/json"), false, b"{}".to_vec()),
   3063             (layer_name, false, layer),
   3064             ("manifest.json".to_owned(), false, manifest),
   3065             ("repositories".to_owned(), false, repositories),
   3066         ];
   3067         members.sort_by(|left, right| left.0.as_bytes().cmp(right.0.as_bytes()));
   3068         let output_file = fs::File::create(output).expect("create OCI fixture");
   3069         let encoder = GzBuilder::new()
   3070             .mtime(0)
   3071             .operating_system(255)
   3072             .write(output_file, Compression::best());
   3073         let mut archive = TarBuilder::new(encoder);
   3074         for (name, directory, bytes) in members {
   3075             let mut header = TarHeader::new_gnu();
   3076             header.set_entry_type(if directory {
   3077                 tar::EntryType::Directory
   3078             } else {
   3079                 tar::EntryType::Regular
   3080             });
   3081             header.set_size(bytes.len() as u64);
   3082             header.set_mode(if directory { 0o755 } else { 0o644 });
   3083             header.set_uid(0);
   3084             header.set_gid(0);
   3085             header.set_mtime(0);
   3086             header.set_cksum();
   3087             archive
   3088                 .append_data(&mut header, name, bytes.as_slice())
   3089                 .expect("write OCI member");
   3090         }
   3091         let encoder = archive.into_inner().expect("finish OCI fixture tar");
   3092         let file = encoder.finish().expect("finish OCI fixture gzip");
   3093         file.sync_all().expect("sync OCI fixture");
   3094     }
   3095 
   3096     fn sample_metadata() -> ReleaseMetadata {
   3097         ReleaseMetadata {
   3098             service: "fixture_service".to_owned(),
   3099             service_package: "fixture-service".to_owned(),
   3100             binary_name: "fixture-service".to_owned(),
   3101             version: "0.1.0-alpha".to_owned(),
   3102             license: "AGPL-3.0-or-later".to_owned(),
   3103         }
   3104     }
   3105 
   3106     fn package(
   3107         id: &str,
   3108         name: &str,
   3109         source: Option<&str>,
   3110         checksum: Option<&str>,
   3111         license: Option<&str>,
   3112         binary: bool,
   3113     ) -> CargoPackage {
   3114         CargoPackage {
   3115             id: id.to_owned(),
   3116             name: name.to_owned(),
   3117             version: "0.1.0-alpha".to_owned(),
   3118             source: source.map(str::to_owned),
   3119             checksum: checksum.map(str::to_owned),
   3120             license: license.map(str::to_owned),
   3121             manifest_path: format!("/fixture/{name}/Cargo.toml"),
   3122             license_file: None,
   3123             targets: if binary {
   3124                 vec![CargoTarget {
   3125                     name: name.to_owned(),
   3126                     kind: vec!["bin".to_owned()],
   3127                 }]
   3128             } else {
   3129                 Vec::new()
   3130             },
   3131             license_texts: source
   3132                 .map(|_| {
   3133                     let text = "fixture dependency license text\n".to_owned();
   3134                     vec![DependencyLicenseText {
   3135                         filename: "LICENSE".to_owned(),
   3136                         sha256: sha256_bytes(text.as_bytes()),
   3137                         text,
   3138                     }]
   3139                 })
   3140                 .unwrap_or_default(),
   3141         }
   3142     }
   3143 
   3144     fn sample_cargo_metadata() -> CargoMetadata {
   3145         let root_id = "path+file:///fixture#fixture-service@0.1.0-alpha";
   3146         let dependency_id = "registry+https://example.invalid#index@0.1.0-alpha";
   3147         CargoMetadata {
   3148             packages: vec![
   3149                 package(root_id, "fixture-service", None, None, Some("MIT"), true),
   3150                 package(
   3151                     dependency_id,
   3152                     "dependency",
   3153                     Some("registry+https://github.com/rust-lang/crates.io-index"),
   3154                     Some(&"a".repeat(64)),
   3155                     Some("Apache-2.0"),
   3156                     false,
   3157                 ),
   3158             ],
   3159             workspace_members: vec![root_id.to_owned()],
   3160             resolve: Some(CargoResolve {
   3161                 nodes: vec![
   3162                     CargoNode {
   3163                         id: dependency_id.to_owned(),
   3164                         dependencies: Vec::new(),
   3165                     },
   3166                     CargoNode {
   3167                         id: root_id.to_owned(),
   3168                         dependencies: vec![dependency_id.to_owned()],
   3169                     },
   3170                 ],
   3171             }),
   3172         }
   3173     }
   3174 
   3175     #[test]
   3176     fn oci_admission_reconciles_manifest_config_and_layer_content() {
   3177         let temporary = TempDir::new().unwrap();
   3178         let root = temporary.path().canonicalize().unwrap();
   3179         let original = root.join("original.tar.gz");
   3180         let revision = "a".repeat(40);
   3181         create_oci_fixture(&original, &revision, &revision);
   3182         let expected = artifact_admission::OciExpectation {
   3183             service: "myc",
   3184             binary_name: "fixture-service",
   3185             version: "0.1.0-alpha",
   3186             service_revision: &revision,
   3187             lib_revision: &revision,
   3188             license: "AGPL-3.0-or-later",
   3189             contract_versions: artifact_admission::ContractVersions {
   3190                 admin: 1,
   3191                 config: 1,
   3192                 provider: 1,
   3193                 state: 1,
   3194                 status: 1,
   3195             },
   3196         };
   3197         artifact_admission::admit_oci(&original, &root, &expected).unwrap();
   3198         let decoder = flate2::read::GzDecoder::new(fs::File::open(&original).unwrap());
   3199         let mut archive = tar::Archive::new(decoder);
   3200         let mut original_members = BTreeMap::new();
   3201         let mut directories = BTreeSet::new();
   3202         for entry in archive.entries().unwrap() {
   3203             let mut entry = entry.unwrap();
   3204             let name = entry.path().unwrap().to_str().unwrap().to_owned();
   3205             if entry.header().entry_type().is_dir() {
   3206                 directories.insert(name.clone());
   3207             }
   3208             let mut bytes = Vec::new();
   3209             entry.read_to_end(&mut bytes).unwrap();
   3210             original_members.insert(name, bytes);
   3211         }
   3212         for case in 0..14 {
   3213             let mut members = original_members.clone();
   3214             let mut manifest: Value = serde_json::from_slice(&members["manifest.json"]).unwrap();
   3215             match case {
   3216                 0 => manifest = json!([]),
   3217                 1 => manifest[0]["Config"] = json!("wrong.extension"),
   3218                 2 => manifest[0]["Config"] = json!("invalid.json"),
   3219                 3 => manifest[0]["RepoTags"] = json!(["wrong:tag"]),
   3220                 4 => manifest[0]["Layers"] = json!([]),
   3221                 5 => manifest[0]["Layers"] = json!(["one", "two", "three"]),
   3222                 6 => {
   3223                     manifest[0]["Layers"] = json!([
   3224                         manifest[0]["Layers"][0].clone(),
   3225                         manifest[0]["Layers"][0].clone()
   3226                     ])
   3227                 }
   3228                 7 => {
   3229                     members
   3230                         .get_mut(manifest[0]["Config"].as_str().unwrap())
   3231                         .unwrap()
   3232                         .push(b' ');
   3233                 }
   3234                 8..=10 => {
   3235                     let old_name = manifest[0]["Config"].as_str().unwrap().to_owned();
   3236                     let mut config: Value =
   3237                         serde_json::from_slice(&members.remove(&old_name).unwrap()).unwrap();
   3238                     match case {
   3239                         8 => config["rootfs"]["type"] = json!("unknown"),
   3240                         9 => config["rootfs"]["diff_ids"] = json!([]),
   3241                         _ => {
   3242                             config["rootfs"]["diff_ids"] =
   3243                                 json!([format!("sha256:{}", "0".repeat(64))])
   3244                         }
   3245                     }
   3246                     let bytes = serde_json::to_vec(&config).unwrap();
   3247                     let name = format!("{}.json", sha256_bytes(&bytes));
   3248                     members.insert(name.clone(), bytes);
   3249                     manifest[0]["Config"] = json!(name);
   3250                 }
   3251                 11 => {
   3252                     members.remove(manifest[0]["Layers"][0].as_str().unwrap());
   3253                 }
   3254                 12 => {
   3255                     members.insert("unexpected".to_owned(), b"extra".to_vec());
   3256                 }
   3257                 _ => {}
   3258             }
   3259             members.insert(
   3260                 "manifest.json".to_owned(),
   3261                 serde_json::to_vec(&manifest).unwrap(),
   3262             );
   3263             let path = root.join(format!("invalid-{case}.tar.gz"));
   3264             let encoder = GzBuilder::new()
   3265                 .mtime(0)
   3266                 .operating_system(255)
   3267                 .write(fs::File::create(&path).unwrap(), Compression::best());
   3268             let mut archive = TarBuilder::new(encoder);
   3269             for (name, bytes) in members {
   3270                 let mut header = TarHeader::new_gnu();
   3271                 let directory = directories.contains(&name);
   3272                 header.set_entry_type(if directory {
   3273                     tar::EntryType::Directory
   3274                 } else {
   3275                     tar::EntryType::Regular
   3276                 });
   3277                 header.set_size(bytes.len() as u64);
   3278                 header.set_mode(if directory { 0o755 } else { 0o644 });
   3279                 header.set_uid(0);
   3280                 header.set_gid(0);
   3281                 header.set_mtime(0);
   3282                 header.set_cksum();
   3283                 archive
   3284                     .append_data(&mut header, name, bytes.as_slice())
   3285                     .unwrap();
   3286             }
   3287             archive.into_inner().unwrap().finish().unwrap();
   3288             let admitted = artifact_admission::admit_oci(&path, &root, &expected);
   3289             if case == 13 {
   3290                 admitted.expect("rewritten unmodified OCI fixture remains valid");
   3291             } else {
   3292                 assert!(admitted.is_err(), "OCI case {case}");
   3293             }
   3294         }
   3295         for case in 0..5 {
   3296             let mut changed = artifact_admission::OciExpectation { ..expected };
   3297             match case {
   3298                 0 => changed.license = "MIT",
   3299                 1 => changed.service = "../escape",
   3300                 2 => changed.binary_name = "../escape",
   3301                 3 => changed.service_revision = "invalid",
   3302                 _ => changed.lib_revision = "invalid",
   3303             }
   3304             assert!(artifact_admission::admit_oci(&original, &root, &changed).is_err());
   3305         }
   3306     }
   3307 
   3308     #[test]
   3309     fn dependency_license_admission_preserves_exact_text_and_rejects_missing_or_unsafe_paths() {
   3310         let temporary = TempDir::new().unwrap();
   3311         let root = temporary.path().canonicalize().unwrap();
   3312         let mut dependency = package(
   3313             "fixture",
   3314             "fixture",
   3315             Some("registry+https://github.com/rust-lang/crates.io-index"),
   3316             Some(&"a".repeat(64)),
   3317             Some("MIT"),
   3318             false,
   3319         );
   3320         dependency.manifest_path = root.join("Cargo.toml").to_str().unwrap().to_owned();
   3321         fs::write(root.join("Cargo.toml"), b"fixture").unwrap();
   3322         assert!(dependency_license_texts(&dependency).is_err());
   3323         let text = "Fixture copyright\nPermission is granted.\n";
   3324         fs::write(root.join("LICENSE"), text).unwrap();
   3325         fs::write(root.join("COPYING-MIT"), "Copying terms\n").unwrap();
   3326         fs::write(root.join("NOTICE.txt"), "Notice terms\n").unwrap();
   3327         let texts = dependency_license_texts(&dependency).unwrap();
   3328         assert_eq!(
   3329             texts
   3330                 .iter()
   3331                 .map(|row| row.filename.as_str())
   3332                 .collect::<Vec<_>>(),
   3333             ["COPYING-MIT", "LICENSE", "NOTICE.txt"]
   3334         );
   3335         assert_eq!(texts[1].text, text);
   3336         assert_eq!(texts[1].sha256, sha256_bytes(text.as_bytes()));
   3337         dependency.license_file = Some("LICENSE".to_owned());
   3338         assert_eq!(dependency_license_texts(&dependency).unwrap().len(), 1);
   3339         for path in ["../outside", "/absolute", "./LICENSE", "missing"] {
   3340             dependency.license_file = Some(path.to_owned());
   3341             assert!(dependency_license_texts(&dependency).is_err(), "{path}");
   3342         }
   3343         for path in [
   3344             "relative/Cargo.toml".to_owned(),
   3345             root.join("not-a-manifest").to_str().unwrap().to_owned(),
   3346         ] {
   3347             dependency.manifest_path = path;
   3348             assert!(dependency_license_texts(&dependency).is_err());
   3349         }
   3350     }
   3351 
   3352     #[test]
   3353     fn dependency_license_content_and_inventory_are_bounded() {
   3354         let temporary = TempDir::new().unwrap();
   3355         let root = temporary.path().canonicalize().unwrap();
   3356         let mut dependency = package(
   3357             "fixture",
   3358             "fixture",
   3359             Some("registry+https://github.com/rust-lang/crates.io-index"),
   3360             Some(&"a".repeat(64)),
   3361             Some("MIT"),
   3362             false,
   3363         );
   3364         dependency.manifest_path = root.join("Cargo.toml").to_str().unwrap().to_owned();
   3365         dependency.license_file = Some("LICENSE".to_owned());
   3366         for bytes in [
   3367             b" \n\t".as_slice(),
   3368             &[255, 254],
   3369             b"-----BEGIN PRIVATE KEY-----\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n-----END PRIVATE KEY-----",
   3370         ] {
   3371             fs::write(root.join("LICENSE"), bytes).unwrap();
   3372             assert!(dependency_license_texts(&dependency).is_err());
   3373         }
   3374         let file = fs::File::create(root.join("LICENSE")).unwrap();
   3375         file.set_len(MAX_TEXT_INPUT_BYTES + 1).unwrap();
   3376         assert!(dependency_license_texts(&dependency).is_err());
   3377         drop(file);
   3378         fs::write(root.join("LICENSE"), b"Valid terms\n").unwrap();
   3379         dependency.license_file = None;
   3380         for index in 0..16 {
   3381             fs::write(root.join(format!("LICENSE-{index}")), b"Terms\n").unwrap();
   3382         }
   3383         assert!(dependency_license_texts(&dependency).is_err());
   3384         for index in 0..256 {
   3385             fs::write(root.join(format!("ordinary-{index}")), b"").unwrap();
   3386         }
   3387         assert!(dependency_license_texts(&dependency).is_err());
   3388     }
   3389 
   3390     #[cfg(unix)]
   3391     #[test]
   3392     fn dependency_license_paths_cannot_escape_the_package_root() {
   3393         use std::os::unix::fs::symlink;
   3394         let temporary = TempDir::new().unwrap();
   3395         let root = temporary.path().canonicalize().unwrap();
   3396         let package_root = root.join("package");
   3397         fs::create_dir(&package_root).unwrap();
   3398         let mut dependency = package(
   3399             "fixture",
   3400             "fixture",
   3401             Some("registry+https://github.com/rust-lang/crates.io-index"),
   3402             Some(&"a".repeat(64)),
   3403             Some("MIT"),
   3404             false,
   3405         );
   3406         dependency.manifest_path = package_root.join("Cargo.toml").to_str().unwrap().to_owned();
   3407         fs::write(root.join("outside"), b"Outside terms\n").unwrap();
   3408         symlink(root.join("outside"), package_root.join("LICENSE")).unwrap();
   3409         assert!(dependency_license_texts(&dependency).is_err());
   3410         fs::create_dir(package_root.join("nested")).unwrap();
   3411         fs::write(package_root.join("nested/terms"), b"Nested terms\n").unwrap();
   3412         dependency.license_file = Some("nested/terms".to_owned());
   3413         assert!(dependency_license_texts(&dependency).is_err());
   3414         assert_eq!(fs::read(root.join("outside")).unwrap(), b"Outside terms\n");
   3415     }
   3416 
   3417     #[test]
   3418     fn contract_matches_the_checked_in_decision() {
   3419         let root = Path::new(env!("CARGO_MANIFEST_DIR"))
   3420             .parent()
   3421             .and_then(Path::parent)
   3422             .expect("workspace root");
   3423         validate_contract_inner(root).expect("release decision");
   3424     }
   3425 
   3426     #[test]
   3427     fn contract_rejects_every_independent_governed_field_drift() {
   3428         let root = Path::new(env!("CARGO_MANIFEST_DIR"))
   3429             .parent()
   3430             .and_then(Path::parent)
   3431             .expect("workspace root");
   3432         let bytes = fs::read(root.join(CONTRACT_RELATIVE)).expect("decision");
   3433         let canonical = serde_json::from_slice::<serde_json::Value>(&bytes).expect("decision json");
   3434         for (pointer, replacement) in [
   3435             ("/schema", serde_json::json!("other")),
   3436             ("/contract_version", serde_json::json!(1)),
   3437             ("/decision_state", serde_json::json!("draft")),
   3438             ("/owner_step", serde_json::json!(1)),
   3439             ("/predecessor/schema", serde_json::json!("other")),
   3440             ("/predecessor/filename", serde_json::json!("other")),
   3441             ("/predecessor/transition", serde_json::json!("other")),
   3442             ("/command", serde_json::json!("other")),
   3443             ("/modes", serde_json::json!([])),
   3444             ("/required_arguments", serde_json::json!([])),
   3445             ("/service_metadata_path", serde_json::json!("other")),
   3446             ("/service_metadata_fields", serde_json::json!([])),
   3447             ("/service_license_path", serde_json::json!("other")),
   3448             ("/source_lock_schema", serde_json::json!("other")),
   3449             ("/source_lock_definition", serde_json::json!("other")),
   3450             ("/artifact_contract_binding", serde_json::json!("other")),
   3451             ("/artifact_admission_contract", serde_json::json!("other")),
   3452             ("/supported_targets", serde_json::json!([])),
   3453             ("/candidate_binding", serde_json::json!("other")),
   3454             ("/lib_root_binding", serde_json::json!("other")),
   3455             ("/binary_admission", serde_json::json!("other")),
   3456             ("/oci_admission", serde_json::json!("other")),
   3457             ("/input_inventory", serde_json::json!([])),
   3458             ("/excluded_parent_owned_inputs", serde_json::json!([])),
   3459             ("/service_root_inventory", serde_json::json!([])),
   3460             ("/output_inventory", serde_json::json!([])),
   3461             ("/canonical_json", serde_json::json!("other")),
   3462             ("/checksum_format", serde_json::json!("other")),
   3463             ("/source_archive_format", serde_json::json!("other")),
   3464             ("/sbom_format", serde_json::json!("other")),
   3465             ("/license_evidence", serde_json::json!("other")),
   3466             ("/provenance_posture", serde_json::json!("other")),
   3467             ("/protected_material_scan_scope", serde_json::json!("other")),
   3468             ("/confidentiality_state", serde_json::json!("other")),
   3469             ("/source_cleanliness", serde_json::json!("other")),
   3470             ("/revision_stability", serde_json::json!("other")),
   3471             ("/no_protected_material", serde_json::json!(false)),
   3472             ("/maximums/text_input_bytes", serde_json::json!(1)),
   3473             ("/maximums/generated_document_bytes", serde_json::json!(1)),
   3474             ("/maximums/service_cargo_lock_bytes", serde_json::json!(1)),
   3475             ("/maximums/service_flake_lock_bytes", serde_json::json!(1)),
   3476             ("/maximums/binary_bytes", serde_json::json!(1)),
   3477             ("/maximums/source_archive_bytes", serde_json::json!(1)),
   3478             (
   3479                 "/maximums/source_archive_member_bytes",
   3480                 serde_json::json!(1),
   3481             ),
   3482             ("/maximums/source_archive_members", serde_json::json!(1)),
   3483             ("/maximums/oci_bytes", serde_json::json!(1)),
   3484             (
   3485                 "/maximums/artifact_scan_expanded_bytes",
   3486                 serde_json::json!(1),
   3487             ),
   3488             ("/maximums/cargo_metadata_bytes", serde_json::json!(1)),
   3489             ("/maximums/packages", serde_json::json!(1)),
   3490             ("/maximums/workspace_packages", serde_json::json!(1)),
   3491             ("/required_negative_vectors", serde_json::json!([])),
   3492             ("/negative_error_codes", serde_json::json!([])),
   3493         ] {
   3494             let mut drifted = canonical.clone();
   3495             *drifted.pointer_mut(pointer).expect("governed field") = replacement;
   3496             let decision = serde_json::from_value::<ReleaseDecision>(drifted)
   3497                 .expect("structurally valid drift");
   3498             assert_eq!(
   3499                 validate_decision(&decision),
   3500                 Err(ReleaseArtifactError::InvalidContract),
   3501                 "accepted drift at {pointer}"
   3502             );
   3503         }
   3504     }
   3505 
   3506     #[test]
   3507     fn exact_inventory_and_limits_are_literal() {
   3508         assert_eq!(INPUT_NAMES.len(), 6);
   3509         assert_eq!(OUTPUT_NAMES.len(), 20);
   3510         assert_eq!(MAX_TEXT_INPUT_BYTES, 1_048_576);
   3511         assert_eq!(MAX_GENERATED_DOCUMENT_BYTES, 16_777_216);
   3512         assert_eq!(MAX_SERVICE_CARGO_LOCK_BYTES, 16_777_216);
   3513         assert_eq!(MAX_SERVICE_FLAKE_LOCK_BYTES, 4_194_304);
   3514         assert_eq!(MAX_BINARY_BYTES, 536_870_912);
   3515         assert_eq!(MAX_SOURCE_ARCHIVE_BYTES, 1_073_741_824);
   3516         assert_eq!(MAX_SOURCE_ARCHIVE_MEMBER_BYTES, 67_108_864);
   3517         assert_eq!(MAX_SOURCE_ARCHIVE_MEMBERS, 65_536);
   3518         assert_eq!(MAX_OCI_BYTES, 2_147_483_648);
   3519         assert_eq!(MAX_METADATA_BYTES, 33_554_432);
   3520         assert_eq!(MAX_PACKAGES, 8_192);
   3521         assert_eq!(MAX_WORKSPACE_PACKAGES, 64);
   3522     }
   3523 
   3524     #[test]
   3525     fn supply_chain_documents_are_deterministic_and_complete() {
   3526         let root_id = "path+file:///fixture#fixture-service@0.1.0-alpha";
   3527         let dependency_id = "registry+https://example.invalid#index@0.1.0-alpha";
   3528         let cargo = CargoMetadata {
   3529             packages: vec![
   3530                 package(
   3531                     dependency_id,
   3532                     "dependency",
   3533                     Some("registry+https://github.com/rust-lang/crates.io-index"),
   3534                     Some(&"a".repeat(64)),
   3535                     Some("Apache-2.0"),
   3536                     false,
   3537                 ),
   3538                 package(root_id, "fixture-service", None, None, Some("MIT"), true),
   3539             ],
   3540             workspace_members: vec![root_id.to_owned()],
   3541             resolve: Some(CargoResolve {
   3542                 nodes: vec![
   3543                     CargoNode {
   3544                         id: dependency_id.to_owned(),
   3545                         dependencies: Vec::new(),
   3546                     },
   3547                     CargoNode {
   3548                         id: root_id.to_owned(),
   3549                         dependencies: vec![dependency_id.to_owned()],
   3550                     },
   3551                 ],
   3552             }),
   3553         };
   3554         let (sbom, notices, licenses) =
   3555             build_supply_chain_documents(&sample_metadata(), cargo).expect("documents");
   3556         let bytes = serde_json::to_vec(&sbom).expect("SBOM JSON");
   3557         assert_eq!(serde_json::to_vec(&sbom).expect("SBOM JSON"), bytes);
   3558         assert_eq!(sbom.bom_format, "CycloneDX");
   3559         assert_eq!(sbom.spec_version, "1.6");
   3560         assert_eq!(sbom.metadata.component.name, "fixture-service");
   3561         assert_eq!(sbom.components.len(), 1);
   3562         assert_eq!(sbom.dependencies.len(), 2);
   3563         assert!(notices.contains("Package: dependency 0.1.0-alpha"));
   3564         assert!(notices.contains("License: Apache-2.0"));
   3565         assert!(notices.contains("registry+https://github.com/rust-lang/crates.io-index"));
   3566         assert!(notices.contains("License-Text: LICENSE sha256:"));
   3567         assert!(licenses.contains("fixture dependency license text"));
   3568     }
   3569 
   3570     #[test]
   3571     fn license_documents_require_exact_bounded_nonempty_text_and_safe_filenames() {
   3572         build_supply_chain_documents(&sample_metadata(), sample_cargo_metadata()).unwrap();
   3573         for case in ["missing", "filename", "empty", "oversized"] {
   3574             let mut changed = sample_cargo_metadata();
   3575             let package = &mut changed.packages[1];
   3576             match case {
   3577                 "missing" => package.license_texts.clear(),
   3578                 "filename" => package.license_texts[0].filename = "../LICENSE".into(),
   3579                 "empty" => package.license_texts[0].text = " \n".into(),
   3580                 "oversized" => {
   3581                     package.license_texts[0].text =
   3582                         "x".repeat(MAX_GENERATED_DOCUMENT_BYTES as usize + 1)
   3583                 }
   3584                 _ => unreachable!(),
   3585             }
   3586             if let Some(text) = package.license_texts.first_mut() {
   3587                 text.sha256 = sha256_bytes(text.text.as_bytes());
   3588             }
   3589             assert!(
   3590                 matches!(
   3591                     build_supply_chain_documents(&sample_metadata(), changed),
   3592                     Err(ReleaseArtifactError::InvalidPackageInventory)
   3593                 ),
   3594                 "{case}"
   3595             );
   3596         }
   3597         let mut without_newline = sample_cargo_metadata();
   3598         let text = &mut without_newline.packages[1].license_texts[0];
   3599         text.text = "Exact license terms".into();
   3600         text.sha256 = sha256_bytes(text.text.as_bytes());
   3601         let (_, _, document) =
   3602             build_supply_chain_documents(&sample_metadata(), without_newline).unwrap();
   3603         assert!(document.ends_with("Exact license terms\n"));
   3604     }
   3605 
   3606     #[test]
   3607     fn private_or_incomplete_dependency_evidence_is_rejected() {
   3608         let root_id = "root";
   3609         for dependency in [
   3610             package(
   3611                 "dep",
   3612                 "dep",
   3613                 Some("git+ssh://git@github.com/private/repo"),
   3614                 Some(&"a".repeat(64)),
   3615                 Some("MIT"),
   3616                 false,
   3617             ),
   3618             package(
   3619                 "dep",
   3620                 "dep",
   3621                 Some("registry+https://github.com/rust-lang/crates.io-index"),
   3622                 Some(&"a".repeat(64)),
   3623                 None,
   3624                 false,
   3625             ),
   3626         ] {
   3627             let cargo = CargoMetadata {
   3628                 packages: vec![
   3629                     package(root_id, "fixture-service", None, None, Some("MIT"), true),
   3630                     dependency,
   3631                 ],
   3632                 workspace_members: vec![root_id.to_owned()],
   3633                 resolve: Some(CargoResolve {
   3634                     nodes: vec![
   3635                         CargoNode {
   3636                             id: root_id.to_owned(),
   3637                             dependencies: vec!["dep".to_owned()],
   3638                         },
   3639                         CargoNode {
   3640                             id: "dep".to_owned(),
   3641                             dependencies: Vec::new(),
   3642                         },
   3643                     ],
   3644                 }),
   3645             };
   3646             assert!(matches!(
   3647                 build_supply_chain_documents(&sample_metadata(), cargo),
   3648                 Err(ReleaseArtifactError::InvalidPackageInventory)
   3649             ));
   3650         }
   3651     }
   3652 
   3653     #[test]
   3654     fn package_metadata_rejects_each_independent_field_drift() {
   3655         let invalid = |package: CargoPackage, workspace_member| {
   3656             assert_eq!(
   3657                 validate_metadata_package(&package, workspace_member),
   3658                 Err(ReleaseArtifactError::InvalidPackageInventory)
   3659             );
   3660         };
   3661         for field in ["name", "version", "id"] {
   3662             for value in [
   3663                 String::new(),
   3664                 "x".repeat(MAX_TEXT_FIELD_BYTES + 1),
   3665                 "x\ny".into(),
   3666             ] {
   3667                 let mut candidate =
   3668                     package("root", "fixture-service", None, None, Some("MIT"), true);
   3669                 match field {
   3670                     "name" => candidate.name = value,
   3671                     "version" => candidate.version = value,
   3672                     "id" => candidate.id = value,
   3673                     _ => unreachable!(),
   3674                 }
   3675                 invalid(candidate, true);
   3676             }
   3677         }
   3678 
   3679         for license in [
   3680             Some(""),
   3681             Some("bad\nlicense"),
   3682             Some("-----BEGIN PRIVATE KEY-----"),
   3683         ] {
   3684             invalid(
   3685                 package("root", "fixture-service", None, None, license, true),
   3686                 true,
   3687             );
   3688         }
   3689         for source in [
   3690             Some(""),
   3691             Some("path+file:///private"),
   3692             Some("git+ssh://git@github.com/private/repo"),
   3693         ] {
   3694             invalid(
   3695                 package(
   3696                     "dep",
   3697                     "dep",
   3698                     source,
   3699                     Some(&"a".repeat(64)),
   3700                     Some("MIT"),
   3701                     false,
   3702                 ),
   3703                 false,
   3704             );
   3705         }
   3706         invalid(
   3707             package(
   3708                 "dep",
   3709                 "dep",
   3710                 None,
   3711                 Some(&"a".repeat(64)),
   3712                 Some("MIT"),
   3713                 false,
   3714             ),
   3715             false,
   3716         );
   3717         invalid(
   3718             package(
   3719                 "dep",
   3720                 "dep",
   3721                 Some("registry+https://github.com/rust-lang/crates.io-index"),
   3722                 Some(&"a".repeat(64)),
   3723                 None,
   3724                 false,
   3725             ),
   3726             false,
   3727         );
   3728         for checksum in ["a".repeat(63), "g".repeat(64)] {
   3729             invalid(
   3730                 package(
   3731                     "root",
   3732                     "fixture-service",
   3733                     None,
   3734                     Some(&checksum),
   3735                     Some("MIT"),
   3736                     true,
   3737                 ),
   3738                 true,
   3739             );
   3740         }
   3741     }
   3742 
   3743     #[test]
   3744     fn supply_chain_graph_rejects_each_independent_structural_drift() {
   3745         let invalid = |cargo| {
   3746             assert!(matches!(
   3747                 build_supply_chain_documents(&sample_metadata(), cargo),
   3748                 Err(ReleaseArtifactError::InvalidPackageInventory)
   3749             ));
   3750         };
   3751 
   3752         let mut cargo = sample_cargo_metadata();
   3753         cargo.packages.clear();
   3754         invalid(cargo);
   3755         let mut cargo = sample_cargo_metadata();
   3756         cargo.workspace_members.clear();
   3757         invalid(cargo);
   3758         let mut cargo = sample_cargo_metadata();
   3759         cargo.packages[0].version = "0.2.0".into();
   3760         invalid(cargo);
   3761         let mut cargo = sample_cargo_metadata();
   3762         cargo.packages[0].targets.clear();
   3763         invalid(cargo);
   3764         let mut cargo = sample_cargo_metadata();
   3765         cargo.packages[0].name = "other".into();
   3766         invalid(cargo);
   3767         let mut cargo = sample_cargo_metadata();
   3768         cargo.packages.push(package(
   3769             "second-root",
   3770             "fixture-service",
   3771             None,
   3772             None,
   3773             Some("MIT"),
   3774             true,
   3775         ));
   3776         cargo.workspace_members.push("second-root".into());
   3777         invalid(cargo);
   3778         let mut cargo = sample_cargo_metadata();
   3779         cargo.packages[1].id = cargo.packages[0].id.clone();
   3780         invalid(cargo);
   3781         let mut cargo = sample_cargo_metadata();
   3782         cargo.workspace_members.push("missing".into());
   3783         invalid(cargo);
   3784         let mut cargo = sample_cargo_metadata();
   3785         cargo.resolve = None;
   3786         invalid(cargo);
   3787         let mut cargo = sample_cargo_metadata();
   3788         cargo.resolve.as_mut().expect("resolve").nodes.clear();
   3789         invalid(cargo);
   3790         let mut cargo = sample_cargo_metadata();
   3791         let duplicate = cargo.resolve.as_ref().expect("resolve").nodes[0].id.clone();
   3792         cargo.resolve.as_mut().expect("resolve").nodes[1].id = duplicate;
   3793         invalid(cargo);
   3794         let mut cargo = sample_cargo_metadata();
   3795         cargo.resolve.as_mut().expect("resolve").nodes[0].id = "missing".into();
   3796         invalid(cargo);
   3797         let mut cargo = sample_cargo_metadata();
   3798         cargo.resolve.as_mut().expect("resolve").nodes[1]
   3799             .dependencies
   3800             .push("missing".into());
   3801         invalid(cargo);
   3802 
   3803         let root_id = "path+file:///fixture#fixture-service@0.1.0-alpha";
   3804         let root_only = CargoMetadata {
   3805             packages: vec![package(
   3806                 root_id,
   3807                 "fixture-service",
   3808                 None,
   3809                 None,
   3810                 Some("MIT"),
   3811                 true,
   3812             )],
   3813             workspace_members: vec![root_id.into()],
   3814             resolve: Some(CargoResolve {
   3815                 nodes: vec![CargoNode {
   3816                     id: root_id.into(),
   3817                     dependencies: Vec::new(),
   3818                 }],
   3819             }),
   3820         };
   3821         let (_, notices, licenses) =
   3822             build_supply_chain_documents(&sample_metadata(), root_only).expect("root-only graph");
   3823         assert!(notices.contains("No third-party Cargo packages are present."));
   3824         assert!(licenses.contains("No third-party Cargo packages are present."));
   3825     }
   3826 
   3827     #[test]
   3828     fn file_admission_predicates_reject_each_independent_drift() {
   3829         let root = TempDir::new().expect("file fixture");
   3830         let regular = root.path().join("regular");
   3831         write_file(&regular, b"bytes");
   3832         let empty = root.path().join("empty");
   3833         write_file(&empty, b"");
   3834         let oversized = root.path().join("oversized");
   3835         fs::File::create(&oversized)
   3836             .and_then(|file| file.set_len(6))
   3837             .expect("sparse file");
   3838         assert!(matches!(
   3839             hash_regular(root.path(), 5),
   3840             Err(ReleaseArtifactError::InvalidInputArtifact)
   3841         ));
   3842         assert!(matches!(
   3843             hash_regular(&oversized, 5),
   3844             Err(ReleaseArtifactError::InvalidInputArtifact)
   3845         ));
   3846         assert!(matches!(
   3847             validate_regular_input(&empty, 5),
   3848             Err(ReleaseArtifactError::InvalidInputArtifact)
   3849         ));
   3850         assert!(matches!(
   3851             validate_regular_input(root.path(), 5),
   3852             Err(ReleaseArtifactError::InvalidInputArtifact)
   3853         ));
   3854         assert!(matches!(
   3855             validate_regular_input(&oversized, 5),
   3856             Err(ReleaseArtifactError::InvalidInputArtifact)
   3857         ));
   3858         assert_eq!(
   3859             validate_absolute_directory(&regular, ReleaseArtifactError::InvalidInputRoot),
   3860             Err(ReleaseArtifactError::InvalidInputRoot)
   3861         );
   3862         assert_eq!(
   3863             validate_absolute_directory(
   3864                 Path::new("relative"),
   3865                 ReleaseArtifactError::InvalidInputRoot
   3866             ),
   3867             Err(ReleaseArtifactError::InvalidInputRoot)
   3868         );
   3869         assert_eq!(
   3870             output_maximum("unknown"),
   3871             Err(ReleaseArtifactError::GenerationFailure)
   3872         );
   3873 
   3874         #[cfg(unix)]
   3875         {
   3876             let symlink = root.path().join("symlink");
   3877             std::os::unix::fs::symlink(&regular, &symlink).expect("symlink");
   3878             assert!(matches!(
   3879                 hash_regular(&symlink, 5),
   3880                 Err(ReleaseArtifactError::InvalidInputArtifact)
   3881             ));
   3882             assert!(matches!(
   3883                 validate_regular_input(&symlink, 5),
   3884                 Err(ReleaseArtifactError::InvalidInputArtifact)
   3885             ));
   3886             assert_eq!(
   3887                 validate_absolute_directory(&symlink, ReleaseArtifactError::InvalidInputRoot),
   3888                 Err(ReleaseArtifactError::InvalidInputRoot)
   3889             );
   3890         }
   3891     }
   3892 
   3893     #[test]
   3894     fn release_metadata_and_text_admission_reject_each_field_drift() {
   3895         let root = TempDir::new().expect("metadata fixture");
   3896         let canonical = r#"[workspace.metadata.radroots.service_release]
   3897 service = "fixture_service"
   3898 service_package = "fixture-service"
   3899 binary_name = "fixture-service"
   3900 version = "0.1.0-alpha"
   3901 "#;
   3902         for (from, to) in [
   3903             ("fixture_service", "Fixture"),
   3904             (
   3905                 "service_package = \"fixture-service\"",
   3906                 "service_package = \"fixture--service\"",
   3907             ),
   3908             (
   3909                 "binary_name = \"fixture-service\"",
   3910                 "binary_name = \"fixture--service\"",
   3911             ),
   3912             ("version = \"0.1.0-alpha\"", "version = \"invalid\""),
   3913         ] {
   3914             write_file(
   3915                 &root.path().join("Cargo.toml"),
   3916                 canonical.replacen(from, to, 1).as_bytes(),
   3917             );
   3918             assert!(matches!(
   3919                 read_release_metadata(root.path()),
   3920                 Err(ReleaseArtifactError::InvalidServiceMetadata)
   3921             ));
   3922         }
   3923         write_file(
   3924             &root.path().join("Cargo.toml"),
   3925             canonical
   3926                 .replacen("0.1.0-alpha", &"a".repeat(129), 1)
   3927                 .as_bytes(),
   3928         );
   3929         assert!(matches!(
   3930             read_release_metadata(root.path()),
   3931             Err(ReleaseArtifactError::InvalidServiceMetadata)
   3932         ));
   3933 
   3934         for (name, bytes) in [
   3935             ("plain.txt", b"contains\0nul".as_slice()),
   3936             ("config.schema.json", b"not json".as_slice()),
   3937             ("config.example.toml", b"not = [toml".as_slice()),
   3938         ] {
   3939             let path = root.path().join(name);
   3940             write_file(&path, bytes);
   3941             assert_eq!(
   3942                 validate_text_artifact(&path, name),
   3943                 Err(ReleaseArtifactError::InvalidInputArtifact)
   3944             );
   3945         }
   3946         assert_eq!(
   3947             write_generated(&root.path().join("empty-generated"), b""),
   3948             Err(ReleaseArtifactError::GenerationFailure)
   3949         );
   3950     }
   3951 
   3952     #[test]
   3953     fn output_scope_remote_and_inventory_reject_each_drift() {
   3954         let root = TempDir::new().expect("output fixture");
   3955         let service = root.path().join("service");
   3956         let input = root.path().join("input");
   3957         let output_parent = root.path().join("output");
   3958         fs::create_dir(&service).expect("service");
   3959         fs::create_dir(&input).expect("input");
   3960         fs::create_dir(&output_parent).expect("output");
   3961         assert_eq!(
   3962             validate_output_parent(Path::new("relative"), &service, &input),
   3963             Err(ReleaseArtifactError::InvalidOutputRoot)
   3964         );
   3965         assert_eq!(
   3966             validate_output_parent(root.path(), &service, &input),
   3967             Err(ReleaseArtifactError::InvalidOutputRoot)
   3968         );
   3969         let file_output = output_parent.join("file");
   3970         write_file(&file_output, b"file");
   3971         assert_eq!(
   3972             validate_output_parent(&file_output, &service, &input),
   3973             Err(ReleaseArtifactError::InvalidOutputRoot)
   3974         );
   3975 
   3976         let inventory = root.path().join("inventory");
   3977         fs::create_dir(&inventory).expect("inventory");
   3978         fs::create_dir(inventory.join("directory-entry")).expect("directory entry");
   3979         assert_eq!(
   3980             directory_inventory(&inventory, ReleaseArtifactError::InvalidInputRoot),
   3981             Err(ReleaseArtifactError::InvalidInputRoot)
   3982         );
   3983         fs::remove_dir(inventory.join("directory-entry")).expect("remove entry");
   3984         for index in 0..=OUTPUT_NAMES.len() {
   3985             write_file(&inventory.join(format!("file-{index}")), b"x");
   3986         }
   3987         assert_eq!(
   3988             directory_inventory(&inventory, ReleaseArtifactError::InvalidInputRoot),
   3989             Err(ReleaseArtifactError::InvalidInputRoot)
   3990         );
   3991 
   3992         write_file(&service.join("fixture"), b"fixture");
   3993         initialize_git(&service, "https://example.invalid/service");
   3994         assert_eq!(
   3995             git_remote(&service),
   3996             Err(ReleaseArtifactError::InvalidServiceRoot)
   3997         );
   3998         git(
   3999             &service,
   4000             &[
   4001                 "remote",
   4002                 "set-url",
   4003                 "origin",
   4004                 "ssh://git@github.com/user/repo\nbad",
   4005             ],
   4006         );
   4007         assert_eq!(
   4008             git_remote(&service),
   4009             Err(ReleaseArtifactError::InvalidServiceRoot)
   4010         );
   4011         git(
   4012             &service,
   4013             &[
   4014                 "remote",
   4015                 "set-url",
   4016                 "origin",
   4017                 &format!("https://github.com/{}", "a".repeat(MAX_TEXT_FIELD_BYTES)),
   4018             ],
   4019         );
   4020         assert_eq!(
   4021             git_remote(&service),
   4022             Err(ReleaseArtifactError::InvalidServiceRoot)
   4023         );
   4024     }
   4025 
   4026     #[cfg(unix)]
   4027     #[test]
   4028     fn snapshot_backed_inventory_rejects_file_and_root_mode_races() {
   4029         use std::os::unix::fs::PermissionsExt as _;
   4030 
   4031         let directory = TempDir::new().expect("inventory fixture");
   4032         let root = directory
   4033             .path()
   4034             .canonicalize()
   4035             .expect("canonical inventory fixture");
   4036         set_directory_mode(&root).expect("directory mode");
   4037         let file = root.join("LICENSE-MIT");
   4038         write_file(&file, b"license\n");
   4039         set_file_mode(&file).expect("file mode");
   4040 
   4041         assert_eq!(
   4042             inventory_records_impl(&root, || {
   4043                 fs::set_permissions(&file, fs::Permissions::from_mode(0o600))
   4044                     .expect("change file mode");
   4045             }),
   4046             Err(ReleaseArtifactError::GenerationFailure)
   4047         );
   4048         set_file_mode(&file).expect("restore file mode");
   4049         assert_eq!(
   4050             inventory_records_impl(&root, || {
   4051                 fs::set_permissions(&root, fs::Permissions::from_mode(0o700))
   4052                     .expect("change root mode");
   4053             }),
   4054             Err(ReleaseArtifactError::GenerationFailure)
   4055         );
   4056         set_directory_mode(&root).expect("restore directory mode");
   4057         fs::set_permissions(&file, fs::Permissions::from_mode(0o4644))
   4058             .expect("add file special mode bit");
   4059         assert_eq!(
   4060             inventory_records(&root),
   4061             Err(ReleaseArtifactError::GenerationFailure)
   4062         );
   4063         set_file_mode(&file).expect("restore file mode");
   4064         fs::set_permissions(&root, fs::Permissions::from_mode(0o1755))
   4065             .expect("add directory special mode bit");
   4066         assert_eq!(
   4067             inventory_records(&root),
   4068             Err(ReleaseArtifactError::GenerationFailure)
   4069         );
   4070     }
   4071 
   4072     #[test]
   4073     fn remaining_release_boundaries_fail_closed() {
   4074         let fixture = ReleaseFixture::new();
   4075         assert_eq!(
   4076             fixture.check(&fixture.output_a),
   4077             Err(ReleaseArtifactError::StaleOutput)
   4078         );
   4079 
   4080         let source_lock = ServiceSourceLockV3::from_canonical_bytes(
   4081             &fs::read(fixture.service.join(LOCK_FILENAME)).expect("source lock"),
   4082         )
   4083         .expect("source lock");
   4084         fs::write(fixture.service.join("flake.lock"), b"different").expect("flake drift");
   4085         assert_eq!(
   4086             validate_source_lock_files(&fixture.service, &source_lock),
   4087             Err(ReleaseArtifactError::InvalidSourceLock)
   4088         );
   4089 
   4090         let mut packages = sample_cargo_metadata();
   4091         packages.packages = (0..=MAX_PACKAGES)
   4092             .map(|index| {
   4093                 package(
   4094                     &format!("id-{index}"),
   4095                     "dep",
   4096                     None,
   4097                     None,
   4098                     Some("MIT"),
   4099                     false,
   4100                 )
   4101             })
   4102             .collect();
   4103         assert!(matches!(
   4104             build_supply_chain_documents(&sample_metadata(), packages),
   4105             Err(ReleaseArtifactError::InvalidPackageInventory)
   4106         ));
   4107         let mut workspace = sample_cargo_metadata();
   4108         workspace.workspace_members = (0..=MAX_WORKSPACE_PACKAGES)
   4109             .map(|index| format!("member-{index}"))
   4110             .collect();
   4111         assert!(matches!(
   4112             build_supply_chain_documents(&sample_metadata(), workspace),
   4113             Err(ReleaseArtifactError::InvalidPackageInventory)
   4114         ));
   4115         for (target_name, kind) in [("other", "bin"), ("fixture-service", "lib")] {
   4116             let mut cargo = sample_cargo_metadata();
   4117             cargo.packages[0].targets = vec![CargoTarget {
   4118                 name: target_name.into(),
   4119                 kind: vec![kind.into()],
   4120             }];
   4121             assert!(matches!(
   4122                 build_supply_chain_documents(&sample_metadata(), cargo),
   4123                 Err(ReleaseArtifactError::InvalidPackageInventory)
   4124             ));
   4125         }
   4126         let mut outside_workspace = sample_cargo_metadata();
   4127         outside_workspace.workspace_members = vec!["other".into()];
   4128         assert!(matches!(
   4129             build_supply_chain_documents(&sample_metadata(), outside_workspace),
   4130             Err(ReleaseArtifactError::InvalidPackageInventory)
   4131         ));
   4132 
   4133         let scope = TempDir::new().expect("scope fixture");
   4134         let service = scope.path().join("service");
   4135         let input_parent = TempDir::new().expect("input scope");
   4136         let input = input_parent.path().join("input");
   4137         fs::create_dir(&service).expect("service");
   4138         fs::create_dir(&input).expect("input");
   4139         assert_eq!(
   4140             validate_output_parent(&scope.path().join("bad\\name"), &service, &input),
   4141             Err(ReleaseArtifactError::InvalidOutputRoot)
   4142         );
   4143         assert_eq!(
   4144             validate_output_parent(input_parent.path(), &service, &input),
   4145             Err(ReleaseArtifactError::InvalidOutputRoot)
   4146         );
   4147         #[cfg(unix)]
   4148         {
   4149             let foreign = scope.path().join("foreign");
   4150             fs::create_dir(&foreign).expect("foreign");
   4151             let output = scope.path().join("output-link");
   4152             std::os::unix::fs::symlink(&foreign, &output).expect("output symlink");
   4153             assert_eq!(
   4154                 validate_output_parent(&output, &service, &input),
   4155                 Err(ReleaseArtifactError::InvalidOutputRoot)
   4156             );
   4157         }
   4158 
   4159         write_file(&service.join("tracked"), b"tracked");
   4160         initialize_git(&service, "https://github.com/radrootslabs/service");
   4161         let child = service.join("child");
   4162         fs::create_dir(&child).expect("child");
   4163         assert_eq!(
   4164             validate_git_root(&child),
   4165             Err(ReleaseArtifactError::InvalidServiceRoot)
   4166         );
   4167         write_file(&service.join("untracked"), b"dirty");
   4168         assert_eq!(
   4169             validate_clean_git(&service),
   4170             Err(ReleaseArtifactError::DirtyServiceSource)
   4171         );
   4172 
   4173         assert_eq!(
   4174             write_generated(
   4175                 &scope.path().join("oversized-generated"),
   4176                 &vec![b'x'; MAX_GENERATED_DOCUMENT_BYTES as usize + 1]
   4177             ),
   4178             Err(ReleaseArtifactError::GenerationFailure)
   4179         );
   4180     }
   4181 
   4182     #[test]
   4183     fn low_level_release_admission_and_comparison_branches_are_qualified() {
   4184         let root = TempDir::new().expect("low-level release fixture");
   4185         let regular = root.path().join("regular");
   4186         write_file(&regular, b"same");
   4187         assert_eq!(
   4188             read_bounded_regular(&regular, 4, ReleaseArtifactError::InvalidInputArtifact)
   4189                 .expect("bounded regular file"),
   4190             b"same"
   4191         );
   4192         let directory = root.path().join("directory");
   4193         fs::create_dir(&directory).expect("directory fixture");
   4194         assert_eq!(
   4195             read_bounded_regular(&directory, 4, ReleaseArtifactError::InvalidInputArtifact),
   4196             Err(ReleaseArtifactError::InvalidInputArtifact)
   4197         );
   4198         let oversized = root.path().join("oversized");
   4199         write_file(&oversized, b"12345");
   4200         assert_eq!(
   4201             read_bounded_regular(&oversized, 4, ReleaseArtifactError::InvalidInputArtifact),
   4202             Err(ReleaseArtifactError::InvalidInputArtifact)
   4203         );
   4204 
   4205         #[cfg(unix)]
   4206         {
   4207             use std::os::unix::fs::{MetadataExt as _, PermissionsExt as _};
   4208 
   4209             let symlink = root.path().join("regular-link");
   4210             std::os::unix::fs::symlink(&regular, &symlink).expect("regular symlink");
   4211             assert_eq!(
   4212                 read_bounded_regular(&symlink, 4, ReleaseArtifactError::InvalidInputArtifact),
   4213                 Err(ReleaseArtifactError::InvalidInputArtifact)
   4214             );
   4215 
   4216             let expected = fs::symlink_metadata(&regular).expect("regular metadata");
   4217             assert_eq!(
   4218                 validate_unchanged_input(&symlink, &expected),
   4219                 Err(ReleaseArtifactError::InvalidInputArtifact)
   4220             );
   4221             assert_eq!(
   4222                 validate_unchanged_input(&directory, &expected),
   4223                 Err(ReleaseArtifactError::InvalidInputArtifact)
   4224             );
   4225 
   4226             let other = root.path().join("other");
   4227             write_file(&other, b"same");
   4228             assert_eq!(
   4229                 validate_unchanged_input(&other, &expected),
   4230                 Err(ReleaseArtifactError::InvalidInputArtifact)
   4231             );
   4232 
   4233             let device_metadata = fs::symlink_metadata("/dev/null").expect("device metadata");
   4234             assert_ne!(device_metadata.dev(), expected.dev());
   4235             assert_eq!(
   4236                 validate_unchanged_input(&regular, &device_metadata),
   4237                 Err(ReleaseArtifactError::InvalidInputArtifact)
   4238             );
   4239 
   4240             let same_inode = fs::symlink_metadata(&regular).expect("same-inode metadata");
   4241             write_file(&regular, b"changed length");
   4242             assert_eq!(
   4243                 validate_unchanged_input(&regular, &same_inode),
   4244                 Err(ReleaseArtifactError::InvalidInputArtifact)
   4245             );
   4246 
   4247             let inventory = root.path().join("symlink-inventory");
   4248             fs::create_dir(&inventory).expect("symlink inventory");
   4249             std::os::unix::fs::symlink(&other, inventory.join("entry")).expect("inventory symlink");
   4250             assert_eq!(
   4251                 directory_inventory(&inventory, ReleaseArtifactError::InvalidInputRoot),
   4252                 Err(ReleaseArtifactError::InvalidInputRoot)
   4253             );
   4254 
   4255             let mode_file = root.path().join("mode-file");
   4256             write_file(&mode_file, b"mode");
   4257             fs::set_permissions(&mode_file, fs::Permissions::from_mode(0o600))
   4258                 .expect("set invalid file mode");
   4259             assert_eq!(
   4260                 validate_file_mode(&mode_file),
   4261                 Err(ReleaseArtifactError::StaleOutput)
   4262             );
   4263             fs::set_permissions(&directory, fs::Permissions::from_mode(0o700))
   4264                 .expect("set invalid directory mode");
   4265             assert_eq!(
   4266                 validate_directory_mode(&directory),
   4267                 Err(ReleaseArtifactError::StaleOutput)
   4268             );
   4269         }
   4270 
   4271         let incomplete = root.path().join("incomplete-output");
   4272         fs::create_dir(&incomplete).expect("incomplete output");
   4273         write_file(&incomplete.join("LICENSE-MIT"), b"x");
   4274         assert_eq!(
   4275             validate_exact_output_inventory(&incomplete),
   4276             Err(ReleaseArtifactError::GenerationFailure)
   4277         );
   4278 
   4279         let expected = root.path().join("expected-output");
   4280         let actual = root.path().join("actual-output");
   4281         fs::create_dir(&expected).expect("expected output");
   4282         fs::create_dir(&actual).expect("actual output");
   4283         set_directory_mode(&expected).expect("expected directory mode");
   4284         set_directory_mode(&actual).expect("actual directory mode");
   4285         for name in OUTPUT_NAMES {
   4286             write_file(&expected.join(name), b"a");
   4287             write_file(&actual.join(name), b"a");
   4288             set_file_mode(&expected.join(name)).expect("expected file mode");
   4289             set_file_mode(&actual.join(name)).expect("actual file mode");
   4290         }
   4291         compare_output(&expected, &actual).expect("matching output");
   4292         let records = inventory_records(&actual).expect("actual records");
   4293         validate_output_records(&actual, &records).expect("matching records");
   4294         write_file(&actual.join("LICENSE-MIT"), b"b");
   4295         set_file_mode(&actual.join("LICENSE-MIT")).expect("restored file mode");
   4296         assert_eq!(
   4297             compare_output(&expected, &actual),
   4298             Err(ReleaseArtifactError::StaleOutput)
   4299         );
   4300 
   4301         let mut oversized_stdout = Command::new("sh");
   4302         oversized_stdout.args(["-c", "printf 12345"]);
   4303         assert_eq!(command_stdout(&mut oversized_stdout, 4), Err(()));
   4304         let mut failed_stdout = Command::new("sh");
   4305         failed_stdout.args(["-c", "exit 7"]);
   4306         assert_eq!(command_stdout(&mut failed_stdout, 4), Err(()));
   4307     }
   4308 
   4309     #[test]
   4310     fn release_service_and_workspace_binding_fail_closed() {
   4311         let fixture = ReleaseFixture::new();
   4312         let mismatched = fs::read_to_string(fixture.service.join(LOCK_FILENAME))
   4313             .expect("source lock")
   4314             .replace("service = \"myc\"", "service = \"rhi\"");
   4315         write_file(&fixture.service.join(LOCK_FILENAME), mismatched.as_bytes());
   4316         git(&fixture.service, &["add", LOCK_FILENAME]);
   4317         git(
   4318             &fixture.service,
   4319             &["commit", "--quiet", "-m", "mismatched service lock"],
   4320         );
   4321         assert_eq!(
   4322             fixture.check(&fixture.output_a),
   4323             Err(ReleaseArtifactError::InvalidSourceLock)
   4324         );
   4325 
   4326         let mut cargo = sample_cargo_metadata();
   4327         let dependency_id = cargo.packages[1].id.clone();
   4328         cargo.packages[0].source =
   4329             Some("registry+https://github.com/rust-lang/crates.io-index".into());
   4330         cargo.packages[0].checksum = Some("a".repeat(64));
   4331         cargo.workspace_members = vec![dependency_id];
   4332         assert!(matches!(
   4333             build_supply_chain_documents(&sample_metadata(), cargo),
   4334             Err(ReleaseArtifactError::InvalidPackageInventory)
   4335         ));
   4336     }
   4337 
   4338     #[test]
   4339     fn identifier_predicates_reject_each_independent_boundary() {
   4340         for value in ["", "1service", "service_", "service__name", "service-name"] {
   4341             assert!(!valid_snake_identifier(value), "{value}");
   4342         }
   4343         assert!(!valid_snake_identifier(&"a".repeat(129)));
   4344         for value in ["", "1service", "service-", "service--name", "service_name"] {
   4345             assert!(!valid_kebab_identifier(value), "{value}");
   4346         }
   4347         assert!(!valid_kebab_identifier(&"a".repeat(129)));
   4348         assert!(!valid_metadata_text(""));
   4349         assert!(!valid_metadata_text(&"a".repeat(MAX_TEXT_FIELD_BYTES + 1)));
   4350         assert!(!valid_metadata_text("bad\rvalue"));
   4351         assert!(!valid_metadata_text("-----BEGIN PRIVATE KEY-----"));
   4352         assert!(!valid_public_source("registry+http://example.invalid"));
   4353         assert!(!valid_public_source(
   4354             "registry+https://user@github.com/private"
   4355         ));
   4356         assert!(!valid_lower_hex("a", 2));
   4357         assert!(!valid_lower_hex("ag", 2));
   4358     }
   4359 
   4360     #[test]
   4361     fn binary_archive_is_reproducible_and_metadata_is_fixed() {
   4362         let root = TempDir::new().expect("archive fixture");
   4363         let root = root
   4364             .path()
   4365             .canonicalize()
   4366             .expect("canonical archive fixture");
   4367         let source = root.join("service");
   4368         let first = root.join("first.tar.gz");
   4369         let second = root.join("second.tar.gz");
   4370         write_file(&source, b"exact executable bytes");
   4371         create_binary_archive(&source, &first, "fixture-service", 1_700_000_000)
   4372             .expect("first archive");
   4373         create_binary_archive(&source, &second, "fixture-service", 1_700_000_000)
   4374             .expect("second archive");
   4375         assert_eq!(
   4376             fs::read(&first).expect("first"),
   4377             fs::read(&second).expect("second")
   4378         );
   4379 
   4380         let decoder = flate2::read::GzDecoder::new(fs::File::open(first).expect("archive"));
   4381         let mut archive = tar::Archive::new(decoder);
   4382         let mut entries = archive.entries().expect("entries");
   4383         let mut entry = entries.next().expect("one entry").expect("entry");
   4384         assert_eq!(
   4385             entry.path().expect("path").as_ref(),
   4386             Path::new("bin/fixture-service")
   4387         );
   4388         assert_eq!(entry.header().mode().expect("mode"), 0o755);
   4389         assert_eq!(entry.header().mtime().expect("mtime"), 1_700_000_000);
   4390         let mut bytes = Vec::new();
   4391         entry.read_to_end(&mut bytes).expect("archive bytes");
   4392         assert_eq!(bytes, b"exact executable bytes");
   4393         assert!(entries.next().is_none());
   4394     }
   4395 
   4396     #[test]
   4397     fn full_artifact_set_is_reproducible_immutable_and_verifiable() {
   4398         let fixture = ReleaseFixture::new();
   4399         fixture.write(&fixture.output_a).expect("first release");
   4400         fixture.write(&fixture.output_b).expect("second release");
   4401         fixture.check(&fixture.output_a).expect("check release");
   4402         assert_eq!(
   4403             directory_inventory(&fixture.output_a, ReleaseArtifactError::StaleOutput)
   4404                 .expect("first inventory"),
   4405             OUTPUT_NAMES.into_iter().map(str::to_owned).collect()
   4406         );
   4407         for name in OUTPUT_NAMES {
   4408             assert_eq!(
   4409                 fs::read(fixture.output_a.join(name)).expect("first output"),
   4410                 fs::read(fixture.output_b.join(name)).expect("second output"),
   4411                 "{name}"
   4412             );
   4413         }
   4414 
   4415         let sums =
   4416             fs::read_to_string(fixture.output_a.join("SHA256SUMS")).expect("checksum inventory");
   4417         let mut prior = "";
   4418         let mut count = 0;
   4419         for line in sums.lines() {
   4420             let (digest, name) = line.split_once("  ").expect("checksum line");
   4421             assert!(prior < name);
   4422             assert!(valid_lower_hex(digest, 64));
   4423             assert_eq!(
   4424                 digest,
   4425                 hash_regular(
   4426                     &fixture.output_a.join(name),
   4427                     output_maximum(name).expect("known output")
   4428                 )
   4429                 .expect("artifact hash")
   4430                 .sha256
   4431             );
   4432             prior = name;
   4433             count += 1;
   4434         }
   4435         assert_eq!(count, OUTPUT_NAMES.len() - 1);
   4436 
   4437         for name in [
   4438             "artifact-manifest.v2.json",
   4439             "artifact-scan.v1.json",
   4440             "oci-image.v1.json",
   4441             "provenance.intoto.jsonl",
   4442             "sbom.cdx.json",
   4443             "source-archives.v3.json",
   4444         ] {
   4445             let bytes = fs::read(fixture.output_a.join(name)).expect("JSON output");
   4446             assert_eq!(bytes.last(), Some(&b'\n'));
   4447             assert_eq!(bytes.iter().filter(|byte| **byte == b'\n').count(), 1);
   4448             serde_json::from_slice::<serde_json::Value>(&bytes).expect("valid JSON");
   4449         }
   4450         let provenance: serde_json::Value = serde_json::from_slice(
   4451             &fs::read(fixture.output_a.join("provenance.intoto.jsonl")).expect("provenance"),
   4452         )
   4453         .expect("provenance JSON");
   4454         assert_eq!(provenance["_type"], "https://in-toto.io/Statement/v1");
   4455         assert_eq!(
   4456             provenance["predicateType"],
   4457             "https://slsa.dev/provenance/v1"
   4458         );
   4459         assert_eq!(
   4460             provenance["predicate"]["buildDefinition"]["externalParameters"]["candidate_digest"],
   4461             format!("sha256:{}", "a".repeat(64))
   4462         );
   4463 
   4464         write_file(
   4465             &fixture.output_a.join("config.example.toml"),
   4466             b"tampered = true\n",
   4467         );
   4468         assert_eq!(
   4469             fixture.check(&fixture.output_a),
   4470             Err(ReleaseArtifactError::StaleOutput)
   4471         );
   4472         assert_eq!(
   4473             fixture.write(&fixture.output_a),
   4474             Err(ReleaseArtifactError::StaleOutput)
   4475         );
   4476     }
   4477 
   4478     #[test]
   4479     fn qualified_nix_material_is_required_and_preserved() {
   4480         let fixture = ReleaseFixture::new();
   4481         fixture
   4482             .write(&fixture.output_a)
   4483             .expect("qualified-Nix release");
   4484 
   4485         let document: serde_json::Value = serde_json::from_slice(
   4486             &fs::read(fixture.output_a.join("source-archives.v3.json"))
   4487                 .expect("source archive document"),
   4488         )
   4489         .expect("source archive JSON");
   4490         assert_eq!(document["schema"], "radroots.service.source-archives.v3");
   4491         assert_eq!(document["contract_version"], 3);
   4492         assert!(valid_lower_hex(
   4493             document["flake_lock_sha256"]
   4494                 .as_str()
   4495                 .expect("flake digest"),
   4496             64
   4497         ));
   4498 
   4499         let lock = ServiceSourceLockV3::from_canonical_bytes(
   4500             &fs::read(fixture.service.join(LOCK_FILENAME)).expect("source lock"),
   4501         )
   4502         .expect("valid source lock");
   4503         for name in ["flake.nix", "flake.lock"] {
   4504             let path = fixture.service.join(name);
   4505             let original = fs::read(&path).expect("qualified Nix file");
   4506             fs::remove_file(&path).expect("remove qualified Nix file");
   4507             assert_eq!(
   4508                 validate_source_lock_files(&fixture.service, &lock),
   4509                 Err(ReleaseArtifactError::InvalidSourceLock),
   4510                 "accepted missing qualified-Nix file {name}"
   4511             );
   4512             write_file(&path, &original);
   4513         }
   4514         write_file(
   4515             &fixture.service.join(PREDECESSOR_LOCK_FILENAME),
   4516             b"unexpected",
   4517         );
   4518         assert_eq!(
   4519             validate_source_lock_files(&fixture.service, &lock),
   4520             Err(ReleaseArtifactError::InvalidSourceLock)
   4521         );
   4522     }
   4523 
   4524     #[test]
   4525     fn protected_text_and_invalid_inventory_fail_closed() {
   4526         let fixture = ReleaseFixture::new();
   4527         let mut secret = b"-----BEGIN PRIVATE KEY-----\n".to_vec();
   4528         secret.extend_from_slice(&[b'A'; 48]);
   4529         secret.extend_from_slice(b"\n-----END PRIVATE KEY-----\n");
   4530         write_file(&fixture.input.join("config.example.toml"), &secret);
   4531         assert_eq!(
   4532             fixture.write(&fixture.output_a),
   4533             Err(ReleaseArtifactError::ProtectedMaterialDetected)
   4534         );
   4535         write_file(
   4536             &fixture.input.join("config.example.toml"),
   4537             b"enabled = true\n",
   4538         );
   4539         write_file(&fixture.input.join("unexpected"), b"unexpected\n");
   4540         assert_eq!(
   4541             fixture.write(&fixture.output_a),
   4542             Err(ReleaseArtifactError::InvalidInputRoot)
   4543         );
   4544     }
   4545 
   4546     #[test]
   4547     fn source_lock_and_source_archive_drift_fail_closed() {
   4548         let fixture = ReleaseFixture::new();
   4549         write_file(
   4550             &fixture.service.join("Cargo.lock"),
   4551             b"version = 4\n# changed after source lock\n",
   4552         );
   4553         git(&fixture.service, &["add", "Cargo.lock"]);
   4554         git(
   4555             &fixture.service,
   4556             &["commit", "--quiet", "-m", "change lock"],
   4557         );
   4558         assert_eq!(
   4559             fixture.write(&fixture.output_a),
   4560             Err(ReleaseArtifactError::InvalidSourceLock)
   4561         );
   4562 
   4563         let fixture = ReleaseFixture::new();
   4564         let current = ServiceSourceLockV3::from_canonical_bytes(
   4565             &fs::read(fixture.service.join(LOCK_FILENAME)).expect("source lock"),
   4566         )
   4567         .expect("source lock");
   4568         let drifted = String::from_utf8(current.canonical_bytes().to_vec())
   4569             .expect("UTF-8 lock")
   4570             .replace(current.source_archive_sha256(), &"0".repeat(64));
   4571         write_file(&fixture.service.join(LOCK_FILENAME), drifted.as_bytes());
   4572         git(&fixture.service, &["add", LOCK_FILENAME]);
   4573         git(
   4574             &fixture.service,
   4575             &["commit", "--quiet", "-m", "drift source archive digest"],
   4576         );
   4577         create_oci_fixture(
   4578             &fixture.input.join("oci-image.tar.gz"),
   4579             &git_output(&fixture.service, &["rev-parse", "HEAD"]),
   4580             current.revision(),
   4581         );
   4582         assert_eq!(
   4583             fixture.write(&fixture.output_a),
   4584             Err(ReleaseArtifactError::InvalidSourceBundle)
   4585         );
   4586     }
   4587 
   4588     #[test]
   4589     fn output_name_is_one_bounded_component() {
   4590         assert!(valid_output_component("release-v1"));
   4591         for value in ["", ".", "..", "a/b", "/absolute", "a\\b"] {
   4592             assert!(!valid_output_component(value), "{value}");
   4593         }
   4594         assert!(!valid_output_component(&"a".repeat(129)));
   4595     }
   4596 
   4597     #[test]
   4598     fn output_scope_and_target_admission_are_fail_closed() {
   4599         let fixture = ReleaseFixture::new();
   4600         assert_eq!(
   4601             fixture.write(&fixture.service.join("release")),
   4602             Err(ReleaseArtifactError::InvalidOutputRoot)
   4603         );
   4604         assert_eq!(
   4605             fixture.write(&fixture.input.join("release")),
   4606             Err(ReleaseArtifactError::InvalidOutputRoot)
   4607         );
   4608         assert_eq!(
   4609             run_inner(Arguments {
   4610                 mode: CommandMode::Write,
   4611                 service_root: &fixture.service,
   4612                 lib_root: &fixture.lib,
   4613                 input_root: &fixture.input,
   4614                 output_root: &fixture.output_a,
   4615                 target: "x86_64-apple-darwin",
   4616                 source_date_epoch: 1_700_000_000,
   4617                 candidate_digest: &"a".repeat(64),
   4618             }),
   4619             Err(ReleaseArtifactError::InvalidServiceMetadata)
   4620         );
   4621         assert_eq!(
   4622             run_inner(Arguments {
   4623                 mode: CommandMode::Write,
   4624                 service_root: &fixture.service,
   4625                 lib_root: &fixture.lib,
   4626                 input_root: &fixture.input,
   4627                 output_root: &fixture.output_a,
   4628                 target: "x86_64-unknown-linux-gnu",
   4629                 source_date_epoch: 0,
   4630                 candidate_digest: &"a".repeat(64),
   4631             }),
   4632             Err(ReleaseArtifactError::InvalidServiceMetadata)
   4633         );
   4634         assert_eq!(
   4635             run_inner(Arguments {
   4636                 mode: CommandMode::Write,
   4637                 service_root: &fixture.service,
   4638                 lib_root: &fixture.lib,
   4639                 input_root: &fixture.input,
   4640                 output_root: &fixture.output_a,
   4641                 target: native_fixture_target(),
   4642                 source_date_epoch: 1_700_000_000,
   4643                 candidate_digest: &"A".repeat(64),
   4644             }),
   4645             Err(ReleaseArtifactError::InvalidServiceMetadata)
   4646         );
   4647     }
   4648 
   4649     #[test]
   4650     fn secret_scanner_detects_a_pattern_across_chunk_boundaries() {
   4651         let mut scanner = SecretScanner::default();
   4652         scanner.scan(b"prefix -----BEGIN OPENSSH").expect("prefix");
   4653         assert_eq!(
   4654             scanner.scan(
   4655                 b" PRIVATE KEY-----\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n-----END OPENSSH PRIVATE KEY----- suffix"
   4656             ),
   4657             Err(ReleaseArtifactError::ProtectedMaterialDetected)
   4658         );
   4659     }
   4660 
   4661     #[test]
   4662     fn release_metadata_rejects_well_typed_but_invalid_identifiers_and_versions() {
   4663         let temporary = TempDir::new().unwrap();
   4664         let original: toml::Value = toml::from_str(
   4665             r#"
   4666             [workspace.package]
   4667             license = "AGPL-3.0-or-later"
   4668             [workspace.metadata.radroots.service_release]
   4669             service = "myc"
   4670             service_package = "fixture-service"
   4671             binary_name = "fixture-service"
   4672             version = "0.1.0-alpha"
   4673         "#,
   4674         )
   4675         .unwrap();
   4676         let manifest = temporary.path().join("Cargo.toml");
   4677         fs::write(&manifest, toml::to_string(&original).unwrap()).unwrap();
   4678         read_release_metadata(temporary.path()).unwrap();
   4679         for (field, replacement) in [
   4680             ("service", "Invalid".to_owned()),
   4681             ("service_package", "Invalid".to_owned()),
   4682             ("binary_name", "../escape".to_owned()),
   4683             ("version", "x".repeat(129)),
   4684             ("version", "invalid".to_owned()),
   4685             ("version", "01.0.0".to_owned()),
   4686         ] {
   4687             let mut value = original.clone();
   4688             value["workspace"]["metadata"]["radroots"]["service_release"][field] =
   4689                 toml::Value::String(replacement);
   4690             fs::write(&manifest, toml::to_string(&value).unwrap()).unwrap();
   4691             assert!(matches!(
   4692                 read_release_metadata(temporary.path()),
   4693                 Err(ReleaseArtifactError::InvalidServiceMetadata)
   4694             ));
   4695         }
   4696         let mut value = original;
   4697         value["workspace"]["package"]["license"] = toml::Value::String("MIT".to_owned());
   4698         fs::write(&manifest, toml::to_string(&value).unwrap()).unwrap();
   4699         assert!(matches!(
   4700             read_release_metadata(temporary.path()),
   4701             Err(ReleaseArtifactError::InvalidServiceMetadata)
   4702         ));
   4703     }
   4704 
   4705     #[test]
   4706     fn sbom_dependency_edges_composition_and_artifact_hashes_are_exact() {
   4707         let artifacts = vec![ArtifactRecord {
   4708             path: "binary.tar.gz".to_owned(),
   4709             byte_length: 42,
   4710             sha256: "a".repeat(64),
   4711         }];
   4712         for case in 0..7 {
   4713             let (mut sbom, _, _) =
   4714                 build_supply_chain_documents(&sample_metadata(), sample_cargo_metadata()).unwrap();
   4715             reconcile_sbom_artifacts(&mut sbom, &artifacts);
   4716             validate_cyclonedx_profile(&sbom, &artifacts).unwrap();
   4717             match case {
   4718                 0 => sbom.components[0].bom_ref = sbom.metadata.component.bom_ref.clone(),
   4719                 1 => {
   4720                     sbom.dependencies.pop();
   4721                 }
   4722                 2 => sbom.dependencies[0].reference = "unknown".to_owned(),
   4723                 3 => sbom.dependencies[0].depends_on.push("unknown".to_owned()),
   4724                 4 => sbom.compositions[0].aggregate = "incomplete",
   4725                 5 => sbom.compositions[0].assemblies.clear(),
   4726                 _ => {
   4727                     let artifact = sbom
   4728                         .components
   4729                         .iter_mut()
   4730                         .find(|row| row.bom_ref.starts_with("artifact:"))
   4731                         .unwrap();
   4732                     artifact.hashes[0].content = "b".repeat(64);
   4733                 }
   4734             }
   4735             assert_eq!(
   4736                 validate_cyclonedx_profile(&sbom, &artifacts),
   4737                 Err(ReleaseArtifactError::GenerationFailure),
   4738                 "SBOM case {case}"
   4739             );
   4740         }
   4741     }
   4742 
   4743     #[test]
   4744     fn schema_reconciliation_attribution_and_subjects_fail_closed() {
   4745         let (mut sbom, _, _) =
   4746             build_supply_chain_documents(&sample_metadata(), sample_cargo_metadata())
   4747                 .expect("supply-chain documents");
   4748         let artifacts = vec![ArtifactRecord {
   4749             path: "binary.tar.gz".to_owned(),
   4750             byte_length: 42,
   4751             sha256: "a".repeat(64),
   4752         }];
   4753         reconcile_sbom_artifacts(&mut sbom, &artifacts);
   4754         validate_cyclonedx_profile(&sbom, &artifacts).expect("CycloneDX profile");
   4755         sbom.spec_version = "1.5";
   4756         assert_eq!(
   4757             validate_cyclonedx_profile(&sbom, &artifacts),
   4758             Err(ReleaseArtifactError::GenerationFailure)
   4759         );
   4760         sbom.spec_version = "1.6";
   4761         sbom.components.pop();
   4762         assert_eq!(
   4763             validate_cyclonedx_profile(&sbom, &artifacts),
   4764             Err(ReleaseArtifactError::GenerationFailure)
   4765         );
   4766 
   4767         let mut cargo = sample_cargo_metadata();
   4768         cargo.packages[1].license_texts[0].sha256 = "b".repeat(64);
   4769         assert!(matches!(
   4770             build_supply_chain_documents(&sample_metadata(), cargo),
   4771             Err(ReleaseArtifactError::InvalidPackageInventory)
   4772         ));
   4773 
   4774         let mut provenance = build_provenance(
   4775             ProvenanceInput {
   4776                 candidate_digest: &"a".repeat(64),
   4777                 service: "myc",
   4778                 target: native_fixture_target(),
   4779                 source_date_epoch: 1_700_000_000,
   4780                 service_repository: "https://github.com/radrootslabs/mycelium",
   4781                 service_revision: &"1".repeat(40),
   4782                 lib_revision: &"2".repeat(40),
   4783                 source_lock_sha256: &"3".repeat(64),
   4784                 manifest_sha256: &"4".repeat(64),
   4785             },
   4786             &artifacts,
   4787         );
   4788         validate_provenance_subjects(&provenance, &"a".repeat(64), &artifacts)
   4789             .expect("exact provenance subjects");
   4790         provenance.statement_type = "unknown";
   4791         assert!(validate_provenance_subjects(&provenance, &"a".repeat(64), &artifacts).is_err());
   4792         provenance.statement_type = "https://in-toto.io/Statement/v1";
   4793         provenance.predicate_type = "unknown";
   4794         assert!(validate_provenance_subjects(&provenance, &"a".repeat(64), &artifacts).is_err());
   4795         provenance.predicate_type = "https://slsa.dev/provenance/v1";
   4796         assert!(validate_provenance_subjects(&provenance, &"b".repeat(64), &artifacts).is_err());
   4797         provenance.subject.clear();
   4798         assert_eq!(
   4799             validate_provenance_subjects(&provenance, &"a".repeat(64), &artifacts),
   4800             Err(ReleaseArtifactError::GenerationFailure)
   4801         );
   4802 
   4803         let scan_evidence = FileEvidence {
   4804             byte_length: 7,
   4805             sha256: "5".repeat(64),
   4806         };
   4807         let scan = ScanDocument {
   4808             schema: "radroots.service.artifact-scan.v1",
   4809             contract_version: 1,
   4810             candidate_digest: "a".repeat(64),
   4811             state: "no_protected_material_detected",
   4812             ruleset_sha256: "6".repeat(64),
   4813             scanned_artifacts: artifacts.clone(),
   4814             nested_members_scanned: 1,
   4815             expanded_bytes_scanned: 42,
   4816         };
   4817         for case in 0..6 {
   4818             let mut manifest = ArtifactManifestDocument {
   4819                 schema: "radroots.service.release-artifacts.v2",
   4820                 contract_version: 2,
   4821                 candidate_digest: "a".repeat(64),
   4822                 service: "myc".to_owned(),
   4823                 version: "0.1.0-alpha".to_owned(),
   4824                 target: native_fixture_target().to_owned(),
   4825                 source_date_epoch: 1_700_000_000,
   4826                 service_revision: "1".repeat(40),
   4827                 lib_revision: "2".repeat(40),
   4828                 rust_version: "1.97.1",
   4829                 host_feature_profile: "service-host",
   4830                 contract_versions: ContractVersionsDocument {
   4831                     config: 1,
   4832                     state: 1,
   4833                     admin: 1,
   4834                     status: 1,
   4835                     provider: 1,
   4836                 },
   4837                 confidentiality: ConfidentialityDocument {
   4838                     state: scan.state,
   4839                     derived_from: artifact_record("artifact-scan.v1.json", &scan_evidence),
   4840                     protected_material_included: false,
   4841                 },
   4842                 artifacts: artifacts.clone(),
   4843             };
   4844             validate_confidentiality_binding(&manifest, &scan, &scan_evidence, &artifacts)
   4845                 .expect("derived confidentiality");
   4846             match case {
   4847                 0 => manifest.confidentiality.state = "invented_clean_state",
   4848                 1 => manifest.candidate_digest = "b".repeat(64),
   4849                 2 => manifest.confidentiality.protected_material_included = true,
   4850                 3 => manifest.confidentiality.derived_from.path = "unknown".to_owned(),
   4851                 4 => manifest.confidentiality.derived_from.sha256 = "b".repeat(64),
   4852                 _ => manifest.artifacts.clear(),
   4853             }
   4854             assert_eq!(
   4855                 validate_confidentiality_binding(&manifest, &scan, &scan_evidence, &artifacts),
   4856                 Err(ReleaseArtifactError::GenerationFailure)
   4857             );
   4858         }
   4859     }
   4860 
   4861     #[test]
   4862     fn secret_path_and_history_archive_vectors_fail_closed() {
   4863         let root = TempDir::new().expect("archive vector root");
   4864         let mut token = b"ghp_".to_vec();
   4865         token.extend(std::iter::repeat_n(b'A', 36));
   4866 
   4867         let source = root.path().join("source.tar");
   4868         write_tar_fixture(&source, "src/value.bin", &token, false);
   4869         assert_eq!(
   4870             scan_tar_members(&source, false),
   4871             Err(ReleaseArtifactError::ProtectedMaterialDetected)
   4872         );
   4873 
   4874         let sensitive = root.path().join("sensitive.tar");
   4875         write_tar_fixture(&sensitive, ".git/config", b"clean", false);
   4876         assert_eq!(
   4877             scan_tar_members(&sensitive, false),
   4878             Err(ReleaseArtifactError::ProtectedMaterialDetected)
   4879         );
   4880 
   4881         let binary = root.path().join("binary.tar.gz");
   4882         write_tar_fixture(&binary, "bin/service", &token, true);
   4883         assert_eq!(
   4884             scan_tar_gzip_members(&binary),
   4885             Err(ReleaseArtifactError::ProtectedMaterialDetected)
   4886         );
   4887 
   4888         let layer = root.path().join("layer.tar");
   4889         write_tar_fixture(&layer, "nix/store/service", &token, false);
   4890         assert_eq!(
   4891             scan_tar_members(&layer, true),
   4892             Err(ReleaseArtifactError::ProtectedMaterialDetected)
   4893         );
   4894 
   4895         let bundle = root.path().join("history.bundle");
   4896         write_file(&bundle, b"not an exact-tree archive");
   4897         assert_eq!(
   4898             scan_tar_members(&bundle, false),
   4899             Err(ReleaseArtifactError::InvalidInputArtifact)
   4900         );
   4901     }
   4902 
   4903     #[test]
   4904     fn archive_scanning_enforces_member_types_counts_lengths_and_protected_paths() {
   4905         let root = TempDir::new().unwrap();
   4906         let path = root.path().join("input.tar");
   4907         for kind in [
   4908             tar::EntryType::Directory,
   4909             tar::EntryType::Symlink,
   4910             tar::EntryType::Link,
   4911             tar::EntryType::Fifo,
   4912         ] {
   4913             let mut builder = TarBuilder::new(Vec::new());
   4914             let mut header = TarHeader::new_gnu();
   4915             header.set_path("member").unwrap();
   4916             header.set_entry_type(kind);
   4917             header.set_size(0);
   4918             if kind.is_symlink() || kind.is_hard_link() {
   4919                 header.set_link_name("target").unwrap();
   4920             }
   4921             header.set_cksum();
   4922             builder.append(&header, &[][..]).unwrap();
   4923             let bytes = builder.into_inner().unwrap();
   4924             fs::write(&path, &bytes).unwrap();
   4925             assert!(scan_tar_members(&path, false).is_err());
   4926             assert_eq!(
   4927                 scan_tar_members(&path, true).is_ok(),
   4928                 kind != tar::EntryType::Fifo
   4929             );
   4930             let gzip = root.path().join("binary.tar.gz");
   4931             let mut encoder = GzBuilder::new().write(Vec::new(), Compression::best());
   4932             encoder.write_all(&bytes).unwrap();
   4933             fs::write(&gzip, encoder.finish().unwrap()).unwrap();
   4934             assert_eq!(scan_tar_gzip_members(&gzip).is_ok(), kind.is_dir());
   4935         }
   4936         let empty = TarBuilder::new(Vec::new()).into_inner().unwrap();
   4937         fs::write(&path, &empty).unwrap();
   4938         assert!(scan_tar_members(&path, true).is_err());
   4939         let mut encoder = GzBuilder::new().write(Vec::new(), Compression::best());
   4940         encoder.write_all(&empty).unwrap();
   4941         fs::write(&path, encoder.finish().unwrap()).unwrap();
   4942         assert!(scan_tar_gzip_members(&path).is_err());
   4943         for length in [2, 4] {
   4944             assert!(scan_reader(&mut &b"abc"[..], length).is_err());
   4945         }
   4946         assert_eq!(scan_reader(&mut &b"abc"[..], 3).unwrap(), 3);
   4947         for path in [
   4948             "/absolute",
   4949             "../escape",
   4950             "src/.ssh/config",
   4951             &"a".repeat(MAX_ARCHIVE_PATH_BYTES + 1),
   4952         ] {
   4953             assert_eq!(
   4954                 validate_scanned_path(path),
   4955                 Err(ReleaseArtifactError::ProtectedMaterialDetected)
   4956             );
   4957         }
   4958     }
   4959 
   4960     fn write_tar_fixture(path: &Path, member: &str, bytes: &[u8], gzip: bool) {
   4961         let mut header = TarHeader::new_gnu();
   4962         header.set_entry_type(tar::EntryType::Regular);
   4963         header.set_size(bytes.len() as u64);
   4964         header.set_mode(0o644);
   4965         header.set_uid(0);
   4966         header.set_gid(0);
   4967         header.set_mtime(1);
   4968         header.set_cksum();
   4969         if gzip {
   4970             let output = fs::File::create(path).expect("gzip tar fixture");
   4971             let encoder = GzBuilder::new()
   4972                 .mtime(1)
   4973                 .operating_system(255)
   4974                 .write(output, Compression::best());
   4975             let mut archive = TarBuilder::new(encoder);
   4976             archive
   4977                 .append_data(&mut header, member, bytes)
   4978                 .expect("gzip tar member");
   4979             let encoder = archive.into_inner().expect("gzip tar archive");
   4980             encoder.finish().expect("gzip tar finish");
   4981         } else {
   4982             let output = fs::File::create(path).expect("tar fixture");
   4983             let mut archive = TarBuilder::new(output);
   4984             archive
   4985                 .append_data(&mut header, member, bytes)
   4986                 .expect("tar member");
   4987             archive.finish().expect("tar finish");
   4988         }
   4989     }
   4990 
   4991     #[test]
   4992     fn identifiers_and_sources_are_closed() {
   4993         assert!(valid_snake_identifier("fixture_service"));
   4994         assert!(!valid_snake_identifier("fixture__service"));
   4995         assert!(valid_kebab_identifier("fixture-service"));
   4996         assert!(!valid_kebab_identifier("fixture--service"));
   4997         assert!(valid_public_source(
   4998             "registry+https://github.com/rust-lang/crates.io-index"
   4999         ));
   5000         assert!(valid_public_source(
   5001             "git+https://github.com/radrootslabs/lib?rev=1111111111111111111111111111111111111111#1111111111111111111111111111111111111111"
   5002         ));
   5003         assert!(!valid_public_source(
   5004             "git+ssh://git@github.com/private/repo"
   5005         ));
   5006         assert!(!valid_public_source("path+file:///secret"));
   5007     }
   5008 
   5009     #[test]
   5010     fn errors_are_stable_and_source_free() {
   5011         let errors = [
   5012             ReleaseArtifactError::InvalidContract,
   5013             ReleaseArtifactError::InvalidServiceRoot,
   5014             ReleaseArtifactError::DirtyServiceSource,
   5015             ReleaseArtifactError::InvalidServiceMetadata,
   5016             ReleaseArtifactError::InvalidInputRoot,
   5017             ReleaseArtifactError::InvalidInputArtifact,
   5018             ReleaseArtifactError::InvalidSourceLock,
   5019             ReleaseArtifactError::InvalidSourceBundle,
   5020             ReleaseArtifactError::InvalidPackageInventory,
   5021             ReleaseArtifactError::ProtectedMaterialDetected,
   5022             ReleaseArtifactError::InvalidOutputRoot,
   5023             ReleaseArtifactError::StaleOutput,
   5024             ReleaseArtifactError::GenerationFailure,
   5025         ];
   5026         for error in errors {
   5027             let display = error.to_string();
   5028             let debug = format!("{error:?}");
   5029             assert!(!display.contains('/'));
   5030             assert!(!display.contains("secret"));
   5031             assert!(!debug.contains('/'));
   5032             assert!(std::error::Error::source(&error).is_none());
   5033         }
   5034     }
   5035 }