1use 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
54pub 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 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
109pub 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 let external_params = ExternalParameters::new(inputs_path, pipeline_path, &hash_alg)?;
161 let external_params_proto = external_params.to_struct()?;
162
163 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 let builder = slsa::generators::make_builder_v1(ATLAS_CLI_BUILDER_ID, None, None);
173
174 let build_metadata =
176 slsa::generators::make_build_metadata_v1("", None, Some(&Timestamp::now()));
177
178 let run_details = slsa::generators::make_run_details_v1(&builder, Some(&build_metadata), None);
181
182 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 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 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 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 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 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 let inputs_field = ¶ms_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}