Skip to main content

atlas_cli/slsa/
cli.rs

1//! # Atlas CLI SLSA Build Provenance Generator
2//!
3//! This module provides Atlas CLI-specific functionality for generating SLSA (Supply-chain
4//! Levels for Software Artifacts) v1 Build Provenance attestations, implementing the logic for
5//! creating signed Build Provenance attestations using Atlas CLI as the builder.
6//!
7//! ## Atlas CLI SLSA Builder
8//!
9//! The generated provenance identifies Atlas CLI as the builder using:
10//! - Builder ID: Uses `ATLAS_CLI_BUILDER_ID` from the generators module
11//! - Build Type: Combines `CLI_NAME` and `CLI_VERSION` as the build type identifier
12//! - External Parameters: Structures inputs and pipeline paths as SLSA external parameters
13//!
14//! ## Examples
15//!
16//! ```no_run
17//! use atlas_cli::slsa::cli::generate_build_provenance;
18//! use atlas_c2pa_lib::cose::HashAlgorithm;
19//! use std::path::PathBuf;
20//!
21//! // Generate Atlas CLI build provenance for a Rust project
22//! generate_build_provenance(
23//!     vec![
24//!         PathBuf::from("src/main.rs"),
25//!         PathBuf::from("Cargo.toml"),
26//!     ],                                           // input source files
27//!     PathBuf::from("build.sh"),                  // build pipeline script
28//!     vec![
29//!         PathBuf::from("target/release/myapp"),  // output artifacts
30//!     ],
31//!     Some(PathBuf::from("signing_key.pem")),     // signing key
32//!     HashAlgorithm::Sha384,                      // hash algorithm
33//!     "json".to_string(),                         // output format
34//!     true,                                       // print to console
35//!     None,                                       // no storage backend
36//!     false,                                      // no TDX support
37//! ).unwrap();
38//! ```
39
40use crate::cli::{CLI_NAME, CLI_VERSION};
41use crate::error::{Error, Result};
42use crate::in_toto;
43use crate::slsa;
44use crate::storage::traits::StorageBackend;
45
46use atlas_c2pa_lib::cose::HashAlgorithm;
47use in_toto_attestation::to_struct;
48use in_toto_attestation::v1::resource_descriptor::ResourceDescriptor;
49use protobuf::well_known_types::struct_::{ListValue, Struct, Value};
50use protobuf::well_known_types::timestamp::Timestamp;
51use serde_json::to_string_pretty;
52use std::path::PathBuf;
53
54/// The Atlas CLI builder identifier for SLSA provenance.
55pub const ATLAS_CLI_BUILDER_ID: &str = "https://github.com/IntelLabs/atlas-cli";
56
57struct ExternalParameters {
58    inputs: Vec<ResourceDescriptor>,
59    pipeline: ResourceDescriptor,
60}
61
62impl ExternalParameters {
63    fn new(
64        inputs_path: Vec<PathBuf>,
65        pipeline_path: PathBuf,
66        hash_alg: &HashAlgorithm,
67    ) -> Result<Self> {
68        let e = ExternalParameters {
69            inputs: generate_file_list_resource_descriptors(inputs_path, &hash_alg)?,
70            pipeline: in_toto::generate_file_resource_descriptor_from_path(
71                pipeline_path.as_path(),
72                &hash_alg,
73            )?,
74        };
75
76        Ok(e)
77    }
78
79    fn to_struct(&self) -> Result<Struct> {
80        let mut external_params = Struct::new();
81
82        let mut inputs_list = ListValue::new();
83        for rd in &self.inputs {
84            let rd_struct = to_struct(rd).map_err(|e| Error::Serialization(e.to_string()))?;
85            let mut rd_val = Value::new();
86            rd_val.set_struct_value(rd_struct);
87            inputs_list.values.push(rd_val);
88        }
89        let mut inputs_val = Value::new();
90        inputs_val.set_list_value(inputs_list);
91
92        // we need to serialize the RD into the Struct proto expected by the external_params field
93        let pipeline_rd_struct =
94            to_struct(&self.pipeline).map_err(|e| Error::Serialization(e.to_string()))?;
95        let mut pipeline_val = Value::new();
96        pipeline_val.set_struct_value(pipeline_rd_struct);
97
98        external_params
99            .fields
100            .insert("inputs".to_string(), inputs_val);
101        external_params
102            .fields
103            .insert("pipeline".to_string(), pipeline_val);
104
105        Ok(external_params)
106    }
107}
108
109/// Generates an Atlas CLI-specific SLSA build provenance attestation.
110///
111/// This function creates a cryptographically signed SLSA build provenance attestation
112/// using Atlas CLI as the identified builder. It processes input files, pipeline
113/// definitions, and output artifacts to generate a complete SLSA v1 provenance
114/// statement that can be verified against the Atlas CLI builder identity.
115///
116/// # Atlas CLI Builder Context
117///
118/// The generated provenance includes Atlas CLI-specific information:
119/// - **Builder ID**: `https://github.com/IntelLabs/atlas-cli`
120/// - **Build Type**: `{CLI_NAME}:{CLI_VERSION}` (e.g., "atlas-cli:1.0.0")
121/// - **External Parameters**: Structured inputs and pipeline information
122/// - **Timestamp**: Current time as build completion timestamp
123///
124/// # Arguments
125///
126/// * `inputs_path` - Vector of paths to input files (source code, dependencies, configs)
127/// * `pipeline_path` - Path to the build script or pipeline definition used by Atlas CLI
128/// * `products_path` - Vector of paths to output artifacts produced by the build
129/// * `key_path` - Optional path to private key for signing (required for valid attestations)
130/// * `hash_alg` - Hash algorithm to use for file integrity and signing operations
131/// * `output_encoding` - Output format: "json" or "cbor"
132/// * `print` - Whether to print the attestation to stdout
133/// * `storage` - Optional storage backend for persisting the attestation
134/// * `_with_tdx` - TDX (Intel Trust Domain Extensions) support flag (reserved for future use)
135///
136/// # Returns
137///
138/// Returns `Ok(())` on successful generation, or an error if any step fails.
139///
140/// # Errors
141///
142/// This function may return errors for:
143/// - **File Access**: Input, pipeline, or product files cannot be read or hashed
144/// - **Key Loading**: Private key file is missing, corrupted, or wrong format
145/// - **Serialization**: Failed to encode attestation in requested format
146/// - **Validation**: Invalid parameters, missing signing key, or unsupported encoding
147/// - **Storage**: Backend storage operations fail (if storage backend provided)
148pub fn generate_build_provenance(
149    inputs_path: Vec<PathBuf>,
150    pipeline_path: PathBuf,
151    products_path: Vec<PathBuf>,
152    key_path: Option<PathBuf>,
153    hash_alg: HashAlgorithm,
154    output_encoding: String,
155    print: bool,
156    storage: Option<&'static dyn StorageBackend>,
157    _with_tdx: bool,
158) -> Result<()> {
159    // Generate the SLSA BuildDefinition.externalParameters
160    let external_params = ExternalParameters::new(inputs_path, pipeline_path, &hash_alg)?;
161    let external_params_proto = external_params.to_struct()?;
162
163    // generate the BuildDefinition
164    let build_def = slsa::generators::make_build_definition_v1(
165        format!("{}:{}", CLI_NAME, CLI_VERSION).as_str(),
166        &external_params_proto,
167        None,
168        None,
169    );
170
171    // generate Builder
172    let builder = slsa::generators::make_builder_v1(ATLAS_CLI_BUILDER_ID, None, None);
173
174    // generate BuildMetadata
175    let build_metadata =
176        slsa::generators::make_build_metadata_v1("", None, Some(&Timestamp::now()));
177
178    // generate RunDetails
179    // FIXME: Add TDX support
180    let run_details = slsa::generators::make_run_details_v1(&builder, Some(&build_metadata), None);
181
182    // generate Provenance predicate!
183    let provenance = slsa::generators::generate_build_provenance_v1(&build_def, &run_details);
184    let provenance_proto =
185        to_struct(&provenance).map_err(|e| Error::Serialization(e.to_string()))?;
186
187    // Generate the statement subjects
188    let subject = generate_file_list_resource_descriptors(products_path, &hash_alg)?;
189
190    let key_path = key_path.ok_or_else(|| {
191        Error::Validation("Signing key is required for SLSA provenance".to_string())
192    })?;
193
194    let envelope = in_toto::generate_signed_statement_v1(
195        &subject,
196        slsa::BUILD_PROVENANCE_PREDICATE_TYPE_V1,
197        &provenance_proto,
198        key_path,
199        hash_alg,
200    )?;
201
202    // Output manifest if requested
203    if print || storage.is_none() {
204        match output_encoding.to_lowercase().as_str() {
205            "json" => {
206                let envelope_json =
207                    to_string_pretty(&envelope).map_err(|e| Error::Serialization(e.to_string()))?;
208                println!("{envelope_json}");
209            }
210            "cbor" => {
211                let envelope_cbor = serde_cbor::to_vec(&envelope)
212                    .map_err(|e| Error::Serialization(e.to_string()))?;
213                println!("{}", hex::encode(&envelope_cbor));
214            }
215            _ => {
216                return Err(Error::Validation(format!(
217                    "Invalid output encoding '{}'. Valid options are: json, cbor",
218                    output_encoding
219                )));
220            }
221        }
222    }
223
224    // Store manifest if storage is provided
225    // FIXME: Add support for SLSA storage in backend
226    if let Some(_storage) = &storage {
227        if !print {
228            let id = 0;
229            println!("Manifest stored successfully with ID: {id}");
230        }
231    }
232
233    Ok(())
234}
235
236fn generate_file_list_resource_descriptors(
237    file_paths: Vec<PathBuf>,
238    algorithm: &HashAlgorithm,
239) -> Result<Vec<ResourceDescriptor>> {
240    let mut rd_vec: Vec<ResourceDescriptor> = Vec::new();
241    for f in file_paths.iter() {
242        let rd = in_toto::generate_file_resource_descriptor_from_path(f.as_path(), algorithm)?;
243        rd_vec.push(rd);
244    }
245
246    Ok(rd_vec)
247}
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252    use crate::signing::test_utils::generate_temp_key;
253    use std::fs;
254    use std::io::Write;
255    use tempfile::TempDir;
256
257    /// Helper function to create a temporary file with content
258    fn create_temp_file(dir: &TempDir, name: &str, content: &[u8]) -> PathBuf {
259        let file_path = dir.path().join(name);
260        let mut file = fs::File::create(&file_path).unwrap();
261        file.write_all(content).unwrap();
262        file_path
263    }
264
265    #[test]
266    fn test_atlas_cli_builder_id_constant() {
267        assert_eq!(
268            ATLAS_CLI_BUILDER_ID,
269            "https://github.com/IntelLabs/atlas-cli"
270        );
271    }
272
273    #[test]
274    fn test_external_parameters_new() {
275        let temp_dir = TempDir::new().unwrap();
276
277        // Create test files
278        let input1 = create_temp_file(&temp_dir, "input1.txt", b"test input 1");
279        let input2 = create_temp_file(&temp_dir, "input2.txt", b"test input 2");
280        let pipeline = create_temp_file(&temp_dir, "build.sh", b"#!/bin/bash\necho 'building'");
281
282        let inputs = vec![input1, input2];
283        let hash_alg = HashAlgorithm::Sha256;
284
285        let external_params = ExternalParameters::new(inputs, pipeline, &hash_alg);
286
287        assert!(external_params.is_ok());
288        let params = external_params.unwrap();
289        assert_eq!(params.inputs.len(), 2);
290        assert!(!params.pipeline.name.is_empty());
291    }
292
293    #[test]
294    fn test_external_parameters_to_struct() {
295        let temp_dir = TempDir::new().unwrap();
296        let input = create_temp_file(&temp_dir, "input.txt", b"test content");
297        let pipeline = create_temp_file(&temp_dir, "pipeline.yml", b"steps: []");
298
299        let external_params =
300            ExternalParameters::new(vec![input], pipeline, &HashAlgorithm::Sha256).unwrap();
301        let struct_result = external_params.to_struct();
302
303        assert!(struct_result.is_ok());
304        let params_struct = struct_result.unwrap();
305        assert!(params_struct.fields.contains_key("inputs"));
306        assert!(params_struct.fields.contains_key("pipeline"));
307    }
308
309    #[test]
310    fn test_external_parameters_empty_inputs() {
311        let temp_dir = TempDir::new().unwrap();
312        let pipeline = create_temp_file(&temp_dir, "build.sh", b"echo hello");
313
314        let external_params =
315            ExternalParameters::new(vec![], pipeline, &HashAlgorithm::Sha256).unwrap();
316        let struct_result = external_params.to_struct();
317
318        assert!(struct_result.is_ok());
319        let params_struct = struct_result.unwrap();
320        assert!(params_struct.fields.contains_key("inputs"));
321        assert!(params_struct.fields.contains_key("pipeline"));
322
323        // Verify inputs list is empty
324        let inputs_field = &params_struct.fields["inputs"];
325        assert!(inputs_field.has_list_value());
326        assert_eq!(inputs_field.list_value().values.len(), 0);
327    }
328
329    #[test]
330    fn test_external_parameters_different_hash_algorithms() {
331        let temp_dir = TempDir::new().unwrap();
332        let input = create_temp_file(&temp_dir, "input.txt", b"test content");
333        let pipeline = create_temp_file(&temp_dir, "build.sh", b"#!/bin/bash\necho build");
334
335        let algorithms = vec![
336            HashAlgorithm::Sha256,
337            HashAlgorithm::Sha384,
338            HashAlgorithm::Sha512,
339        ];
340
341        for alg in algorithms {
342            let result = ExternalParameters::new(vec![input.clone()], pipeline.clone(), &alg);
343            assert!(result.is_ok(), "Failed with algorithm: {:?}", alg);
344
345            let params = result.unwrap();
346            assert_eq!(params.inputs.len(), 1);
347            assert!(!params.inputs[0].digest.is_empty());
348            assert!(!params.pipeline.digest.is_empty());
349        }
350    }
351
352    #[test]
353    fn test_generate_file_list_resource_descriptors() {
354        let temp_dir = TempDir::new().unwrap();
355        let file1 = create_temp_file(&temp_dir, "file1.txt", b"content1");
356        let file2 = create_temp_file(&temp_dir, "file2.txt", b"content2");
357
358        let result =
359            generate_file_list_resource_descriptors(vec![file1, file2], &HashAlgorithm::Sha256);
360
361        assert!(result.is_ok());
362        let descriptors = result.unwrap();
363        assert_eq!(descriptors.len(), 2);
364
365        for descriptor in descriptors {
366            assert!(!descriptor.name.is_empty());
367            assert!(!descriptor.digest.is_empty());
368        }
369    }
370
371    #[test]
372    fn test_generate_file_list_resource_descriptors_empty() {
373        let result = generate_file_list_resource_descriptors(vec![], &HashAlgorithm::Sha256);
374
375        assert!(result.is_ok());
376        let descriptors = result.unwrap();
377        assert_eq!(descriptors.len(), 0);
378    }
379
380    #[test]
381    fn test_generate_build_provenance() {
382        let temp_dir = TempDir::new().unwrap();
383        let input = create_temp_file(&temp_dir, "input.txt", b"test");
384        let pipeline = create_temp_file(&temp_dir, "build.sh", b"build script");
385        let product = create_temp_file(&temp_dir, "output.bin", b"output");
386        let (_secure_key, tmp_dir) = generate_temp_key().unwrap();
387
388        let result = generate_build_provenance(
389            vec![input],
390            pipeline,
391            vec![product],
392            Some(tmp_dir.path().join("test_key.pem")),
393            HashAlgorithm::Sha256,
394            "json".to_string(),
395            true,
396            None,
397            false,
398        );
399
400        assert!(result.is_ok());
401    }
402}