1414
1515//! In-toto attestation statement support for creating DSSE bundles.
1616//!
17- //! This module provides a builder API for creating in-toto Statement v0.1 attestations
17+ //! This module provides a builder API for creating in-toto Statement attestations
1818//! that can be signed and wrapped in DSSE envelopes.
1919//!
20- //! Note: This implements the v0.1 specification, which is currently used by cosign and Rekor.
21- //! See: <https://github.com/in-toto/attestation/blob/main/spec/v0.1.0/statement.md>
20+ //! Supports both v0.1 and v1 statement formats:
21+ //! - v0.1: <https://github.com/in-toto/attestation/blob/main/spec/v0.1.0/statement.md>
22+ //! - v1: <https://github.com/in-toto/attestation/blob/main/spec/v1/statement.md>
2223
2324use serde:: { Deserialize , Serialize } ;
2425use std:: collections:: HashMap ;
2526
2627/// The in-toto Statement v0.1 type identifier.
27- /// Note: v0.1 is the currently used version by cosign and Rekor, not v1 .
28- pub const STATEMENT_TYPE_V1 : & str = "https://in-toto.io/Statement/v0.1" ;
28+ /// Used by older Sigstore implementations and some legacy bundles .
29+ pub const STATEMENT_TYPE_V0_1 : & str = "https://in-toto.io/Statement/v0.1" ;
2930
30- /// An in-toto Statement v0.1 attestation.
31+ /// The in-toto Statement v1 type identifier.
32+ /// Used by current Sigstore implementations including GitHub Actions.
33+ pub const STATEMENT_TYPE_V1 : & str = "https://in-toto.io/Statement/v1" ;
34+
35+ /// An in-toto Statement attestation.
3136///
3237/// This represents a verifiable claim about one or more software artifacts.
33- /// Note: This uses the v0.1 specification which is currently used by cosign and Rekor .
38+ /// Supports both v0.1 and v1 statement formats .
3439/// Field order matches the canonical JSON serialization used by cosign.
3540#[ derive( Debug , Clone , Serialize , Deserialize , PartialEq , Eq ) ]
3641pub struct Statement {
37- /// The statement type (always "< https://in-toto.io/Statement/v0.1> ")
42+ /// The statement type (either " https://in-toto.io/Statement/v0.1" or "https://in-toto.io/Statement/v1 ")
3843 #[ serde( rename = "_type" ) ]
3944 pub statement_type : String ,
4045
@@ -145,10 +150,24 @@ impl StatementBuilder {
145150 self
146151 }
147152
148- /// Builds the statement.
153+ /// Builds the statement using the v1 format .
149154 ///
150155 /// Returns an error if required fields are missing.
151156 pub fn build ( self ) -> Result < Statement , & ' static str > {
157+ self . build_with_version ( STATEMENT_TYPE_V1 )
158+ }
159+
160+ /// Builds the statement using the v0.1 format (for backward compatibility).
161+ ///
162+ /// Returns an error if required fields are missing.
163+ pub fn build_v0_1 ( self ) -> Result < Statement , & ' static str > {
164+ self . build_with_version ( STATEMENT_TYPE_V0_1 )
165+ }
166+
167+ /// Builds the statement with a specific version.
168+ ///
169+ /// Returns an error if required fields are missing.
170+ fn build_with_version ( self , version : & str ) -> Result < Statement , & ' static str > {
152171 if self . subjects . is_empty ( ) {
153172 return Err ( "Statement must have at least one subject" ) ;
154173 }
@@ -160,7 +179,7 @@ impl StatementBuilder {
160179 let predicate = self . predicate . ok_or ( "Statement must have a predicate" ) ?;
161180
162181 Ok ( Statement {
163- statement_type : STATEMENT_TYPE_V1 . to_string ( ) ,
182+ statement_type : version . to_string ( ) ,
164183 predicate_type,
165184 subject : self . subjects ,
166185 predicate,
@@ -256,4 +275,49 @@ mod tests {
256275 assert_eq ! ( parsed. statement_type, STATEMENT_TYPE_V1 ) ;
257276 assert_eq ! ( parsed. subject[ 0 ] . name, "test.tar.gz" ) ;
258277 }
278+
279+ #[ test]
280+ fn test_statement_v0_1_compatibility ( ) {
281+ // Test that we can create and parse v0.1 statements
282+ let statement = StatementBuilder :: new ( )
283+ . subject ( Subject :: new ( "test.tar.gz" , "sha256" , "abc123" ) )
284+ . predicate_type ( "https://slsa.dev/provenance/v0.2" )
285+ . predicate ( json ! ( {
286+ "buildType" : "test"
287+ } ) )
288+ . build_v0_1 ( )
289+ . unwrap ( ) ;
290+
291+ assert_eq ! ( statement. statement_type, STATEMENT_TYPE_V0_1 ) ;
292+
293+ // Test that we can round-trip v0.1 statements
294+ let json = serde_json:: to_string ( & statement) . unwrap ( ) ;
295+ let parsed: Statement = serde_json:: from_str ( & json) . unwrap ( ) ;
296+
297+ assert_eq ! ( parsed. statement_type, STATEMENT_TYPE_V0_1 ) ;
298+ assert_eq ! ( parsed. subject[ 0 ] . name, "test.tar.gz" ) ;
299+ }
300+
301+ #[ test]
302+ fn test_statement_accepts_both_versions ( ) {
303+ // Test parsing a v0.1 statement
304+ let v0_1_json = r#"{
305+ "_type": "https://in-toto.io/Statement/v0.1",
306+ "subject": [{"name": "test", "digest": {"sha256": "abc123"}}],
307+ "predicateType": "https://example.com/test",
308+ "predicate": {}
309+ }"# ;
310+ let v0_1: Statement = serde_json:: from_str ( v0_1_json) . unwrap ( ) ;
311+ assert_eq ! ( v0_1. statement_type, STATEMENT_TYPE_V0_1 ) ;
312+
313+ // Test parsing a v1 statement
314+ let v1_json = r#"{
315+ "_type": "https://in-toto.io/Statement/v1",
316+ "subject": [{"name": "test", "digest": {"sha256": "abc123"}}],
317+ "predicateType": "https://example.com/test",
318+ "predicate": {}
319+ }"# ;
320+ let v1: Statement = serde_json:: from_str ( v1_json) . unwrap ( ) ;
321+ assert_eq ! ( v1. statement_type, STATEMENT_TYPE_V1 ) ;
322+ }
259323}
0 commit comments