Skip to content

Commit

Permalink
Add support for SLSA Provenance v0.2
Browse files Browse the repository at this point in the history
Signed-off-by: Arnaud J Le Hors <[email protected]>
  • Loading branch information
lehors authored and mlieberman85 committed Dec 25, 2023
1 parent d60c322 commit e585631
Show file tree
Hide file tree
Showing 10 changed files with 1,309 additions and 17 deletions.
41 changes: 36 additions & 5 deletions src/bin/bin.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! A CLI tool for validating supply chain metadata documents.
//!
//! This tool currently supports validating In-Toto v1 documents with
//! SLSA Provenance v1 predicates.
//! SLSA Provenance v1 and v0.2 predicates.
//! TODO(mlieberman85): The CLI commands and args could probably be generalized better to minimize duplication.

use std::{path::PathBuf, process};
Expand All @@ -13,8 +13,8 @@ use serde_json::Value;
use spector::{
models::{
intoto::{
predicate::Predicate, provenance::SLSAProvenanceV1Predicate,
statement::{InTotoStatementV1}, scai::SCAIV02Predicate,
predicate::Predicate, provenancev1::SLSAProvenanceV1Predicate, provenancev02::SLSAProvenanceV02Predicate,
statement::InTotoStatementV1, scai::SCAIV02Predicate,
},
sbom::{spdx22::Spdx22Document, spdx23::Spdx23},
},
Expand Down Expand Up @@ -154,11 +154,14 @@ struct GenerateInTotoV1 {
#[derive(Debug, Copy, Clone, ValueEnum)]
enum PredicateOption {
SLSAProvenanceV1,
SLSAProvenanceV02,
SCAIV02Predicate,
}

#[derive(Parser)]
struct SLSAProvenanceV1 {}
#[derive(Parser)]
struct SLSAProvenanceV02 {}

/// Validates the specified document.
fn validate_cmd(validate: Validate) -> Result<()> {
Expand Down Expand Up @@ -208,6 +211,26 @@ fn validate_intoto_v1(in_toto: ValidateInTotoV1) -> Result<()> {
Ok(())
}
},
Predicate::SLSAProvenanceV02(_) => match in_toto.predicate {
Some(PredicateOption::SLSAProvenanceV02) => {
println!("Valid InTotoV1 SLSAProvenanceV02 document");
println!("Document: {}", &pretty_json);
Ok(())
}
// TODO(mlieberman85): Uncomment below once additional predicate types are supported.
Some(_) => {
eprintln!("Invalid InTotoV1 SLSAProvenanceV02 document. Unexpected predicateType: {:?}", in_toto.predicate);
eprintln!("Document: {}", &pretty_json);
Err(anyhow::anyhow!(
"Invalid InTotoV1 SLSAProvenanceV02 document"
))
}
None => {
println!("Valid InTotoV1 SLSAProvenanceV02 document");
println!("Document: {}", &pretty_json);
Ok(())
}
},
Predicate::SCAIV02(_) => match in_toto.predicate {
Some(PredicateOption::SCAIV02Predicate) => {
println!("Valid InTotoV1 SCAIV02Predicate document");
Expand Down Expand Up @@ -235,8 +258,15 @@ fn validate_intoto_v1(in_toto: ValidateInTotoV1) -> Result<()> {
"Unexpected predicateType: {:?}",
statement.predicate_type.as_str()
))
}
else if let Some(PredicateOption::SCAIV02Predicate) = in_toto.predicate {

} else if let Some(PredicateOption::SLSAProvenanceV02) = in_toto.predicate {
eprintln!("Invalid InTotoV1 SLSAProvenanceV02 document");
eprintln!("Document: {}", &pretty_json);
Err(anyhow::anyhow!(
"Unexpected predicateType: {:?}",
statement.predicate_type.as_str()
))
} else if let Some(PredicateOption::SCAIV02Predicate) = in_toto.predicate {
eprintln!("Invalid InTotoV1 SCAIV02Predicate document");
eprintln!("Document: {}", &pretty_json);
Err(anyhow::anyhow!(
Expand Down Expand Up @@ -288,6 +318,7 @@ fn validate_document<T: DeserializeOwned>(file_path: PathBuf) -> Result<()> {
fn generate_intoto_v1(in_toto: GenerateInTotoV1) -> Result<()> {
match in_toto.predicate {
Some(PredicateOption::SLSAProvenanceV1) => print_schema::<SLSAProvenanceV1Predicate>(),
Some(PredicateOption::SLSAProvenanceV02) => print_schema::<SLSAProvenanceV02Predicate>(),
Some(PredicateOption::SCAIV02Predicate) => print_schema::<SCAIV02Predicate>(),
None => print_schema::<InTotoStatementV1>(),
}
Expand Down
3 changes: 2 additions & 1 deletion src/models/intoto/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub mod predicate;
pub mod provenance;
pub mod provenancev1;
pub mod provenancev02;
pub mod statement;
pub mod scai;

Expand Down
31 changes: 30 additions & 1 deletion src/models/intoto/predicate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
//! to handle different predicate types, including known types such as `SLSAProvenanceV1`
//! and generic `Other` variants.

use super::provenance::SLSAProvenanceV1Predicate;
use super::provenancev1::SLSAProvenanceV1Predicate;
use super::provenancev02::SLSAProvenanceV02Predicate;
use super::scai::SCAIV02Predicate;
use schemars::JsonSchema;
use serde::{de::DeserializeOwned, Serialize};
Expand All @@ -21,6 +22,7 @@ use serde_json::Value;
#[serde(untagged)]
pub enum Predicate {
SLSAProvenanceV1(SLSAProvenanceV1Predicate),
SLSAProvenanceV02(SLSAProvenanceV02Predicate),
SCAIV02(SCAIV02Predicate),
Other(Value),
}
Expand All @@ -45,6 +47,10 @@ pub fn deserialize_predicate(
let slsa_provenance = deserialize_helper::<SLSAProvenanceV1Predicate>(predicate_json)?;
Ok(Predicate::SLSAProvenanceV1(slsa_provenance))
}
"https://slsa.dev/provenance/v0.2" => {
let slsa_provenance: SLSAProvenanceV02Predicate = deserialize_helper::<SLSAProvenanceV02Predicate>(predicate_json)?;
Ok(Predicate::SLSAProvenanceV02(slsa_provenance))
}
"https://in-toto.io/attestation/scai/attribute-report" => {
let scai_v02 = deserialize_helper::<SCAIV02Predicate>(predicate_json)?;
Ok(Predicate::SCAIV02(scai_v02))
Expand Down Expand Up @@ -86,6 +92,29 @@ mod tests {
assert!(matches!(result, Ok(Predicate::SLSAProvenanceV1(_))));
}

#[test]
fn test_deserialize_slsa_provenance_v02_predicate() {
let predicate_type = "https://slsa.dev/provenance/v0.2";
let predicate_json = json!({
"buildType": "https://slsa.dev/provenance/v0.2",
"invocation": {
"parameters": {},
"environment": {}
},
"builder": {
"id": "https://example.com/builder"
},
"materials": [],
"metadata": {
"buildInvocationId": "test-invocation-id",
"buildStartedOn": "2022-01-01T00:00:00Z"
}
});

let result = deserialize_predicate(predicate_type, &predicate_json);
assert!(matches!(result, Ok(Predicate::SLSAProvenanceV02(_))));
}

#[test]
fn test_deserialize_other_predicate() {
let predicate_type = "https://unknown.example.com";
Expand Down
224 changes: 224 additions & 0 deletions src/models/intoto/provenancev02.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
//! SLSA provenance predicate model and associated structures.
//!
//! This module provides structs for the SLSAProvenanceV02Predicate and its related structures.
//! It also includes the necessary (de)serialization code for handling SLSA provenance predicates.

use chrono::{DateTime, Utc};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use url::Url;

/// A structure representing the SLSA Provenance v0.2 Predicate.
#[derive(Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
pub struct SLSAProvenanceV02Predicate {
/// The entity that executed the invocation, which is trusted to have correctly performed the operation and populated this provenance.
pub builder: Builder,
#[serde(rename = "buildType")]
/// The type of build that was performed.
pub build_type: Url,
#[serde(skip_serializing_if = "Option::is_none")]
/// The event that kicked off the build.
pub invocation: Option<Invocation>,
#[serde(rename = "buildConfig", skip_serializing_if = "Option::is_none")]
/// The steps in the build. If invocation.configSource is not available, buildConfig can be used to verify information about the build.
pub build_config: Option<serde_json::Map<String, serde_json::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
/// Metadata about this particular execution of the build.
pub metadata: Option<BuildMetadata>,
#[serde(rename = "materials", skip_serializing_if = "Option::is_none")]
/// Unordered collection of artifacts that influenced the build including sources, dependencies, build tools, base images, and so on. Completeness is best effort, at least through SLSA Build L3. For example, if the build script fetches and executes “example.com/foo.sh”, which in turn fetches “example.com/bar.tar.gz”, then both “foo.sh” and “bar.tar.gz” SHOULD be listed here.
pub materials: Option<Vec<ResourceDescriptor>>,
}

/// A structure representing the builder information of the SLSA Provenance v0.2 Predicate.
#[derive(Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
pub struct Builder {
pub id: Url
}

/// A structure identifying the event that kicked off the build in the SLSA Provenance v0.2 Predicate.
#[derive(Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
pub struct Invocation {
#[serde(rename = "configSource", skip_serializing_if = "Option::is_none")]
/// Description of where the config file that kicked off the build came from. This is effectively a pointer to the source where buildConfig came from.
pub config_source: Option<ConfigSource>,
#[serde(rename = "parameters", skip_serializing_if = "Option::is_none")]
/// Collection of all external inputs that influenced the build on top of invocation.configSource.
pub parameters: Option<serde_json::Map<String, serde_json::Value>>,
#[serde(rename = "environment", skip_serializing_if = "Option::is_none")]
/// Any other builder-controlled inputs necessary for correctly evaluating the build. Usually only needed for reproducing the build but not evaluated as part of policy.
pub environment: Option<serde_json::Map<String, serde_json::Value>>,

}

/// A structure representing the description of where the config file that kicked off the build came from in the SLSA Provenance v0.2 Predicate.
#[derive(Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
pub struct ConfigSource {
/// The identity of the source of the config.
#[serde(skip_serializing_if = "Option::is_none")]
pub uri: Option<Url>,
/// A set of cryptographic digests of the contents of the resource or artifact.
#[serde(skip_serializing_if = "Option::is_none")]
pub digest: Option<HashMap<String, String>>,
/// The entry point into the build. This is often a path to a configuration file and/or a target label within that file.
#[serde(rename = "entryPoint", skip_serializing_if = "Option::is_none")]
pub entry_point: Option<String>,
}

/// A structure representing the metadata of the SLSA Provenance v0.2 Predicate.
#[derive(Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
pub struct BuildMetadata {
#[serde(rename = "buildInvocationId", skip_serializing_if = "Option::is_none")]
/// Identifies this particular build invocation, which can be useful for finding associated logs or other ad-hoc analysis. The exact meaning and format is defined by builder.id; by default it is treated as opaque and case-sensitive. The value SHOULD be globally unique.
pub invocation_id: Option<String>,
#[serde(rename = "buildStartedOn", skip_serializing_if = "Option::is_none")]
/// The timestamp of when the build started.
pub started_on: Option<DateTime<Utc>>,
#[serde(rename = "buildFinishedOn", skip_serializing_if = "Option::is_none")]
/// The timestamp of when the build completed.
pub finished_on: Option<DateTime<Utc>>,
#[serde(rename = "completeness", skip_serializing_if = "Option::is_none")]
/// Information on how complete the provided information is.
pub completeness: Option<Completeness>,
#[serde(rename = "reproducible", skip_serializing_if = "Option::is_none")]
/// Whether the builder claims that running invocation on materials will produce bit-for-bit identical output.
pub reproducible: Option<bool>,
}

/// A structure representing the completeness claims of the SLSA Provenance v0.2 Predicate.
#[derive(Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
pub struct Completeness {
#[serde(rename = "parameters", skip_serializing_if = "Option::is_none")]
/// Whether the builder claims that nvocation.parameters is complete, meaning that all external inputs are properly captured in invocation.parameters.
pub parameters: Option<bool>,
#[serde(rename = "environment", skip_serializing_if = "Option::is_none")]
/// Whether the builder claims that invocation.environment is complete.
pub environment: Option<bool>,
#[serde(rename = "materials", skip_serializing_if = "Option::is_none")]
/// Whether the builder claims that materials is complete, usually through some controls to prevent network access.
pub materials: Option<bool>,
}

/// A size-efficient description of any software artifact or resource (mutable or immutable).
#[derive(Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
pub struct ResourceDescriptor {
#[serde(skip_serializing_if = "Option::is_none")]
/// A URI used to identify the resource or artifact globally. This field is REQUIRED unless digest is set.
pub uri: Option<Url>,
/// A set of cryptographic digests of the contents of the resource or artifact. This field is REQUIRED unless uri is set.
#[serde(skip_serializing_if = "Option::is_none")]
pub digest: Option<HashMap<String, String>>,
}

#[cfg(test)]
mod tests {
use super::*;
use maplit::hashmap;
use serde_json::json;

fn get_test_slsa_provenance() -> SLSAProvenanceV02Predicate {
SLSAProvenanceV02Predicate {
builder: Builder {
id: Url::parse("https://example.com/builder/v1").unwrap(),
},
build_type: Url::parse("https://example.com/buildType/v1").unwrap(),
invocation: Some(Invocation {
config_source: Some(ConfigSource {
uri: Some(Url::parse("https://example.com/source1").unwrap()),
digest: Some(hashmap! {"algorithm1".to_string() => "digest1".to_string()}),
entry_point: Some("myentrypoint".to_string()),
}),
parameters: Some(json!({"key": "value"}).as_object().unwrap().clone()),
environment: Some(json!({"key": "value"}).as_object().unwrap().clone()),
}),
build_config: Some(json!({"key": "value"}).as_object().unwrap().clone()),
metadata: Some(BuildMetadata {
invocation_id: Some("invocation1".to_string()),
started_on: Some(DateTime::parse_from_rfc3339("2023-01-01T12:34:56Z")
.unwrap()
.with_timezone(&Utc)),
finished_on: Some(
DateTime::parse_from_rfc3339("2023-01-01T13:34:56Z")
.unwrap()
.with_timezone(&Utc),
),
completeness: Some(Completeness {
parameters: Some(true),
environment: Some(true),
materials: Some(true),
}),
reproducible: Some(false),
}),
materials: Some(vec![ResourceDescriptor {
uri: Some(Url::parse("https://example.com/material1").unwrap()),
digest: Some(hashmap! {"algorithm1".to_string() => "digest1".to_string()}),
}]),
}
}

fn get_test_slsa_provenance_json() -> serde_json::Value {
json!({
"builder": {
"id": "https://example.com/builder/v1",
},
"buildType": "https://example.com/buildType/v1",
"invocation": {
"configSource": {
"uri": "https://example.com/source1",
"digest": {
"algorithm1": "digest1"
},
"entryPoint": "myentrypoint"
},
"parameters": {
"key": "value"
},
"environment": {
"key": "value"
}
},
"buildConfig": {
"key": "value",
},
"metadata": {
"buildInvocationId": "invocation1",
"buildStartedOn": "2023-01-01T12:34:56Z",
"buildFinishedOn": "2023-01-01T13:34:56Z",
"completeness": {
"parameters": true,
"environment": true,
"materials": true
},
"reproducible": false
},
"materials": [
{
"uri": "https://example.com/material1",
"digest": {
"algorithm1": "digest1"
}
}
]
})
}

#[test]
fn deserialize_slsa_provenance() {
let json_data = get_test_slsa_provenance_json();
let deserialized_provenance: SLSAProvenanceV02Predicate =
serde_json::from_value(json_data).unwrap();
let expected_provenance = get_test_slsa_provenance();

assert_eq!(deserialized_provenance, expected_provenance);
}

#[test]
fn serialize_slsa_provenance() {
let provenance = get_test_slsa_provenance();
let serialized_provenance = serde_json::to_value(provenance).unwrap();
let expected_json_data = get_test_slsa_provenance_json();

assert_eq!(serialized_provenance, expected_json_data);
}
}
File renamed without changes.
2 changes: 1 addition & 1 deletion src/models/intoto/scai.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

use super::provenance::ResourceDescriptor;
use super::provenancev1::ResourceDescriptor;

/// This is based on the model in:
/// {
Expand Down
4 changes: 1 addition & 3 deletions src/models/intoto/statement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,7 @@ use std::collections::HashMap;
use url::Url;
use std::fmt::Debug;

use crate::models::{
intoto::predicate::{deserialize_predicate, Predicate},
};
use crate::models::intoto::predicate::{deserialize_predicate, Predicate};

/// Represents an In-Toto v1 statement.
#[derive(Debug, Serialize, PartialEq, JsonSchema)]
Expand Down
Loading

0 comments on commit e585631

Please sign in to comment.