Skip to content

Commit e585631

Browse files
lehorsmlieberman85
authored andcommitted
Add support for SLSA Provenance v0.2
Signed-off-by: Arnaud J Le Hors <lehors@us.ibm.com>
1 parent d60c322 commit e585631

File tree

10 files changed

+1309
-17
lines changed

10 files changed

+1309
-17
lines changed

src/bin/bin.rs

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! A CLI tool for validating supply chain metadata documents.
22
//!
33
//! This tool currently supports validating In-Toto v1 documents with
4-
//! SLSA Provenance v1 predicates.
4+
//! SLSA Provenance v1 and v0.2 predicates.
55
//! TODO(mlieberman85): The CLI commands and args could probably be generalized better to minimize duplication.
66
77
use std::{path::PathBuf, process};
@@ -13,8 +13,8 @@ use serde_json::Value;
1313
use spector::{
1414
models::{
1515
intoto::{
16-
predicate::Predicate, provenance::SLSAProvenanceV1Predicate,
17-
statement::{InTotoStatementV1}, scai::SCAIV02Predicate,
16+
predicate::Predicate, provenancev1::SLSAProvenanceV1Predicate, provenancev02::SLSAProvenanceV02Predicate,
17+
statement::InTotoStatementV1, scai::SCAIV02Predicate,
1818
},
1919
sbom::{spdx22::Spdx22Document, spdx23::Spdx23},
2020
},
@@ -154,11 +154,14 @@ struct GenerateInTotoV1 {
154154
#[derive(Debug, Copy, Clone, ValueEnum)]
155155
enum PredicateOption {
156156
SLSAProvenanceV1,
157+
SLSAProvenanceV02,
157158
SCAIV02Predicate,
158159
}
159160

160161
#[derive(Parser)]
161162
struct SLSAProvenanceV1 {}
163+
#[derive(Parser)]
164+
struct SLSAProvenanceV02 {}
162165

163166
/// Validates the specified document.
164167
fn validate_cmd(validate: Validate) -> Result<()> {
@@ -208,6 +211,26 @@ fn validate_intoto_v1(in_toto: ValidateInTotoV1) -> Result<()> {
208211
Ok(())
209212
}
210213
},
214+
Predicate::SLSAProvenanceV02(_) => match in_toto.predicate {
215+
Some(PredicateOption::SLSAProvenanceV02) => {
216+
println!("Valid InTotoV1 SLSAProvenanceV02 document");
217+
println!("Document: {}", &pretty_json);
218+
Ok(())
219+
}
220+
// TODO(mlieberman85): Uncomment below once additional predicate types are supported.
221+
Some(_) => {
222+
eprintln!("Invalid InTotoV1 SLSAProvenanceV02 document. Unexpected predicateType: {:?}", in_toto.predicate);
223+
eprintln!("Document: {}", &pretty_json);
224+
Err(anyhow::anyhow!(
225+
"Invalid InTotoV1 SLSAProvenanceV02 document"
226+
))
227+
}
228+
None => {
229+
println!("Valid InTotoV1 SLSAProvenanceV02 document");
230+
println!("Document: {}", &pretty_json);
231+
Ok(())
232+
}
233+
},
211234
Predicate::SCAIV02(_) => match in_toto.predicate {
212235
Some(PredicateOption::SCAIV02Predicate) => {
213236
println!("Valid InTotoV1 SCAIV02Predicate document");
@@ -235,8 +258,15 @@ fn validate_intoto_v1(in_toto: ValidateInTotoV1) -> Result<()> {
235258
"Unexpected predicateType: {:?}",
236259
statement.predicate_type.as_str()
237260
))
238-
}
239-
else if let Some(PredicateOption::SCAIV02Predicate) = in_toto.predicate {
261+
262+
} else if let Some(PredicateOption::SLSAProvenanceV02) = in_toto.predicate {
263+
eprintln!("Invalid InTotoV1 SLSAProvenanceV02 document");
264+
eprintln!("Document: {}", &pretty_json);
265+
Err(anyhow::anyhow!(
266+
"Unexpected predicateType: {:?}",
267+
statement.predicate_type.as_str()
268+
))
269+
} else if let Some(PredicateOption::SCAIV02Predicate) = in_toto.predicate {
240270
eprintln!("Invalid InTotoV1 SCAIV02Predicate document");
241271
eprintln!("Document: {}", &pretty_json);
242272
Err(anyhow::anyhow!(
@@ -288,6 +318,7 @@ fn validate_document<T: DeserializeOwned>(file_path: PathBuf) -> Result<()> {
288318
fn generate_intoto_v1(in_toto: GenerateInTotoV1) -> Result<()> {
289319
match in_toto.predicate {
290320
Some(PredicateOption::SLSAProvenanceV1) => print_schema::<SLSAProvenanceV1Predicate>(),
321+
Some(PredicateOption::SLSAProvenanceV02) => print_schema::<SLSAProvenanceV02Predicate>(),
291322
Some(PredicateOption::SCAIV02Predicate) => print_schema::<SCAIV02Predicate>(),
292323
None => print_schema::<InTotoStatementV1>(),
293324
}

src/models/intoto/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
pub mod predicate;
2-
pub mod provenance;
2+
pub mod provenancev1;
3+
pub mod provenancev02;
34
pub mod statement;
45
pub mod scai;
56

src/models/intoto/predicate.rs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
//! to handle different predicate types, including known types such as `SLSAProvenanceV1`
55
//! and generic `Other` variants.
66
7-
use super::provenance::SLSAProvenanceV1Predicate;
7+
use super::provenancev1::SLSAProvenanceV1Predicate;
8+
use super::provenancev02::SLSAProvenanceV02Predicate;
89
use super::scai::SCAIV02Predicate;
910
use schemars::JsonSchema;
1011
use serde::{de::DeserializeOwned, Serialize};
@@ -21,6 +22,7 @@ use serde_json::Value;
2122
#[serde(untagged)]
2223
pub enum Predicate {
2324
SLSAProvenanceV1(SLSAProvenanceV1Predicate),
25+
SLSAProvenanceV02(SLSAProvenanceV02Predicate),
2426
SCAIV02(SCAIV02Predicate),
2527
Other(Value),
2628
}
@@ -45,6 +47,10 @@ pub fn deserialize_predicate(
4547
let slsa_provenance = deserialize_helper::<SLSAProvenanceV1Predicate>(predicate_json)?;
4648
Ok(Predicate::SLSAProvenanceV1(slsa_provenance))
4749
}
50+
"https://slsa.dev/provenance/v0.2" => {
51+
let slsa_provenance: SLSAProvenanceV02Predicate = deserialize_helper::<SLSAProvenanceV02Predicate>(predicate_json)?;
52+
Ok(Predicate::SLSAProvenanceV02(slsa_provenance))
53+
}
4854
"https://in-toto.io/attestation/scai/attribute-report" => {
4955
let scai_v02 = deserialize_helper::<SCAIV02Predicate>(predicate_json)?;
5056
Ok(Predicate::SCAIV02(scai_v02))
@@ -86,6 +92,29 @@ mod tests {
8692
assert!(matches!(result, Ok(Predicate::SLSAProvenanceV1(_))));
8793
}
8894

95+
#[test]
96+
fn test_deserialize_slsa_provenance_v02_predicate() {
97+
let predicate_type = "https://slsa.dev/provenance/v0.2";
98+
let predicate_json = json!({
99+
"buildType": "https://slsa.dev/provenance/v0.2",
100+
"invocation": {
101+
"parameters": {},
102+
"environment": {}
103+
},
104+
"builder": {
105+
"id": "https://example.com/builder"
106+
},
107+
"materials": [],
108+
"metadata": {
109+
"buildInvocationId": "test-invocation-id",
110+
"buildStartedOn": "2022-01-01T00:00:00Z"
111+
}
112+
});
113+
114+
let result = deserialize_predicate(predicate_type, &predicate_json);
115+
assert!(matches!(result, Ok(Predicate::SLSAProvenanceV02(_))));
116+
}
117+
89118
#[test]
90119
fn test_deserialize_other_predicate() {
91120
let predicate_type = "https://unknown.example.com";

src/models/intoto/provenancev02.rs

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
//! SLSA provenance predicate model and associated structures.
2+
//!
3+
//! This module provides structs for the SLSAProvenanceV02Predicate and its related structures.
4+
//! It also includes the necessary (de)serialization code for handling SLSA provenance predicates.
5+
6+
use chrono::{DateTime, Utc};
7+
use schemars::JsonSchema;
8+
use serde::{Deserialize, Serialize};
9+
use std::collections::HashMap;
10+
use url::Url;
11+
12+
/// A structure representing the SLSA Provenance v0.2 Predicate.
13+
#[derive(Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
14+
pub struct SLSAProvenanceV02Predicate {
15+
/// The entity that executed the invocation, which is trusted to have correctly performed the operation and populated this provenance.
16+
pub builder: Builder,
17+
#[serde(rename = "buildType")]
18+
/// The type of build that was performed.
19+
pub build_type: Url,
20+
#[serde(skip_serializing_if = "Option::is_none")]
21+
/// The event that kicked off the build.
22+
pub invocation: Option<Invocation>,
23+
#[serde(rename = "buildConfig", skip_serializing_if = "Option::is_none")]
24+
/// The steps in the build. If invocation.configSource is not available, buildConfig can be used to verify information about the build.
25+
pub build_config: Option<serde_json::Map<String, serde_json::Value>>,
26+
#[serde(skip_serializing_if = "Option::is_none")]
27+
/// Metadata about this particular execution of the build.
28+
pub metadata: Option<BuildMetadata>,
29+
#[serde(rename = "materials", skip_serializing_if = "Option::is_none")]
30+
/// 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.
31+
pub materials: Option<Vec<ResourceDescriptor>>,
32+
}
33+
34+
/// A structure representing the builder information of the SLSA Provenance v0.2 Predicate.
35+
#[derive(Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
36+
pub struct Builder {
37+
pub id: Url
38+
}
39+
40+
/// A structure identifying the event that kicked off the build in the SLSA Provenance v0.2 Predicate.
41+
#[derive(Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
42+
pub struct Invocation {
43+
#[serde(rename = "configSource", skip_serializing_if = "Option::is_none")]
44+
/// 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.
45+
pub config_source: Option<ConfigSource>,
46+
#[serde(rename = "parameters", skip_serializing_if = "Option::is_none")]
47+
/// Collection of all external inputs that influenced the build on top of invocation.configSource.
48+
pub parameters: Option<serde_json::Map<String, serde_json::Value>>,
49+
#[serde(rename = "environment", skip_serializing_if = "Option::is_none")]
50+
/// 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.
51+
pub environment: Option<serde_json::Map<String, serde_json::Value>>,
52+
53+
}
54+
55+
/// A structure representing the description of where the config file that kicked off the build came from in the SLSA Provenance v0.2 Predicate.
56+
#[derive(Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
57+
pub struct ConfigSource {
58+
/// The identity of the source of the config.
59+
#[serde(skip_serializing_if = "Option::is_none")]
60+
pub uri: Option<Url>,
61+
/// A set of cryptographic digests of the contents of the resource or artifact.
62+
#[serde(skip_serializing_if = "Option::is_none")]
63+
pub digest: Option<HashMap<String, String>>,
64+
/// The entry point into the build. This is often a path to a configuration file and/or a target label within that file.
65+
#[serde(rename = "entryPoint", skip_serializing_if = "Option::is_none")]
66+
pub entry_point: Option<String>,
67+
}
68+
69+
/// A structure representing the metadata of the SLSA Provenance v0.2 Predicate.
70+
#[derive(Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
71+
pub struct BuildMetadata {
72+
#[serde(rename = "buildInvocationId", skip_serializing_if = "Option::is_none")]
73+
/// 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.
74+
pub invocation_id: Option<String>,
75+
#[serde(rename = "buildStartedOn", skip_serializing_if = "Option::is_none")]
76+
/// The timestamp of when the build started.
77+
pub started_on: Option<DateTime<Utc>>,
78+
#[serde(rename = "buildFinishedOn", skip_serializing_if = "Option::is_none")]
79+
/// The timestamp of when the build completed.
80+
pub finished_on: Option<DateTime<Utc>>,
81+
#[serde(rename = "completeness", skip_serializing_if = "Option::is_none")]
82+
/// Information on how complete the provided information is.
83+
pub completeness: Option<Completeness>,
84+
#[serde(rename = "reproducible", skip_serializing_if = "Option::is_none")]
85+
/// Whether the builder claims that running invocation on materials will produce bit-for-bit identical output.
86+
pub reproducible: Option<bool>,
87+
}
88+
89+
/// A structure representing the completeness claims of the SLSA Provenance v0.2 Predicate.
90+
#[derive(Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
91+
pub struct Completeness {
92+
#[serde(rename = "parameters", skip_serializing_if = "Option::is_none")]
93+
/// Whether the builder claims that nvocation.parameters is complete, meaning that all external inputs are properly captured in invocation.parameters.
94+
pub parameters: Option<bool>,
95+
#[serde(rename = "environment", skip_serializing_if = "Option::is_none")]
96+
/// Whether the builder claims that invocation.environment is complete.
97+
pub environment: Option<bool>,
98+
#[serde(rename = "materials", skip_serializing_if = "Option::is_none")]
99+
/// Whether the builder claims that materials is complete, usually through some controls to prevent network access.
100+
pub materials: Option<bool>,
101+
}
102+
103+
/// A size-efficient description of any software artifact or resource (mutable or immutable).
104+
#[derive(Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
105+
pub struct ResourceDescriptor {
106+
#[serde(skip_serializing_if = "Option::is_none")]
107+
/// A URI used to identify the resource or artifact globally. This field is REQUIRED unless digest is set.
108+
pub uri: Option<Url>,
109+
/// A set of cryptographic digests of the contents of the resource or artifact. This field is REQUIRED unless uri is set.
110+
#[serde(skip_serializing_if = "Option::is_none")]
111+
pub digest: Option<HashMap<String, String>>,
112+
}
113+
114+
#[cfg(test)]
115+
mod tests {
116+
use super::*;
117+
use maplit::hashmap;
118+
use serde_json::json;
119+
120+
fn get_test_slsa_provenance() -> SLSAProvenanceV02Predicate {
121+
SLSAProvenanceV02Predicate {
122+
builder: Builder {
123+
id: Url::parse("https://example.com/builder/v1").unwrap(),
124+
},
125+
build_type: Url::parse("https://example.com/buildType/v1").unwrap(),
126+
invocation: Some(Invocation {
127+
config_source: Some(ConfigSource {
128+
uri: Some(Url::parse("https://example.com/source1").unwrap()),
129+
digest: Some(hashmap! {"algorithm1".to_string() => "digest1".to_string()}),
130+
entry_point: Some("myentrypoint".to_string()),
131+
}),
132+
parameters: Some(json!({"key": "value"}).as_object().unwrap().clone()),
133+
environment: Some(json!({"key": "value"}).as_object().unwrap().clone()),
134+
}),
135+
build_config: Some(json!({"key": "value"}).as_object().unwrap().clone()),
136+
metadata: Some(BuildMetadata {
137+
invocation_id: Some("invocation1".to_string()),
138+
started_on: Some(DateTime::parse_from_rfc3339("2023-01-01T12:34:56Z")
139+
.unwrap()
140+
.with_timezone(&Utc)),
141+
finished_on: Some(
142+
DateTime::parse_from_rfc3339("2023-01-01T13:34:56Z")
143+
.unwrap()
144+
.with_timezone(&Utc),
145+
),
146+
completeness: Some(Completeness {
147+
parameters: Some(true),
148+
environment: Some(true),
149+
materials: Some(true),
150+
}),
151+
reproducible: Some(false),
152+
}),
153+
materials: Some(vec![ResourceDescriptor {
154+
uri: Some(Url::parse("https://example.com/material1").unwrap()),
155+
digest: Some(hashmap! {"algorithm1".to_string() => "digest1".to_string()}),
156+
}]),
157+
}
158+
}
159+
160+
fn get_test_slsa_provenance_json() -> serde_json::Value {
161+
json!({
162+
"builder": {
163+
"id": "https://example.com/builder/v1",
164+
},
165+
"buildType": "https://example.com/buildType/v1",
166+
"invocation": {
167+
"configSource": {
168+
"uri": "https://example.com/source1",
169+
"digest": {
170+
"algorithm1": "digest1"
171+
},
172+
"entryPoint": "myentrypoint"
173+
},
174+
"parameters": {
175+
"key": "value"
176+
},
177+
"environment": {
178+
"key": "value"
179+
}
180+
},
181+
"buildConfig": {
182+
"key": "value",
183+
},
184+
"metadata": {
185+
"buildInvocationId": "invocation1",
186+
"buildStartedOn": "2023-01-01T12:34:56Z",
187+
"buildFinishedOn": "2023-01-01T13:34:56Z",
188+
"completeness": {
189+
"parameters": true,
190+
"environment": true,
191+
"materials": true
192+
},
193+
"reproducible": false
194+
},
195+
"materials": [
196+
{
197+
"uri": "https://example.com/material1",
198+
"digest": {
199+
"algorithm1": "digest1"
200+
}
201+
}
202+
]
203+
})
204+
}
205+
206+
#[test]
207+
fn deserialize_slsa_provenance() {
208+
let json_data = get_test_slsa_provenance_json();
209+
let deserialized_provenance: SLSAProvenanceV02Predicate =
210+
serde_json::from_value(json_data).unwrap();
211+
let expected_provenance = get_test_slsa_provenance();
212+
213+
assert_eq!(deserialized_provenance, expected_provenance);
214+
}
215+
216+
#[test]
217+
fn serialize_slsa_provenance() {
218+
let provenance = get_test_slsa_provenance();
219+
let serialized_provenance = serde_json::to_value(provenance).unwrap();
220+
let expected_json_data = get_test_slsa_provenance_json();
221+
222+
assert_eq!(serialized_provenance, expected_json_data);
223+
}
224+
}

src/models/intoto/scai.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use schemars::JsonSchema;
77
use serde::{Deserialize, Serialize};
88
use std::collections::HashMap;
99

10-
use super::provenance::ResourceDescriptor;
10+
use super::provenancev1::ResourceDescriptor;
1111

1212
/// This is based on the model in:
1313
/// {

src/models/intoto/statement.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,7 @@ use std::collections::HashMap;
1111
use url::Url;
1212
use std::fmt::Debug;
1313

14-
use crate::models::{
15-
intoto::predicate::{deserialize_predicate, Predicate},
16-
};
14+
use crate::models::intoto::predicate::{deserialize_predicate, Predicate};
1715

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

0 commit comments

Comments
 (0)