Skip to content

Commit d3051f1

Browse files
committed
Add DSSE (Dead Simple Signing Envelope) support
This PR adds support for DSSE envelopes used in in-toto attestations: - **src/bundle/dsse.rs**: DSSE envelope implementation with Pre-Authentication Encoding (PAE) support. Provides DsseEnvelope wrapper around protobuf Envelope with convenient APIs for creating and manipulating DSSE envelopes. - **src/bundle/intoto.rs**: in-toto Statement support including StatementBuilder, Subject, and predicate handling. Supports creating in-toto attestations with multiple subjects and custom predicates. - **src/rekor/models/dsse.rs**: Rekor DSSE entry models for submitting DSSE envelopes to the transparency log. - **rust-toolchain**: Pin to Rust 1.90.0 for edition 2024 support Key Features: - Correct PAE computation per DSSE spec - Support for application/vnd.in-toto+json payload type - Builder pattern for creating statements - Multiple digest algorithms per subject - Comprehensive unit tests This PR lays the groundwork for Bundle v0.3 DSSE verification support and attestation signing capabilities.
1 parent af3d8a5 commit d3051f1

File tree

6 files changed

+605
-0
lines changed

6 files changed

+605
-0
lines changed

rust-toolchain

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
1.90.0

src/bundle/dsse.rs

Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
// Copyright 2024 The Sigstore Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
//! DSSE (Dead Simple Signing Envelope) support for Sigstore bundles.
16+
//!
17+
//! This module implements the DSSE specification for creating and verifying
18+
//! Pre-Authentication Encoding (PAE) used in DSSE signatures.
19+
//!
20+
//! See: <https://github.com/secure-systems-lab/dsse/blob/v1.0.0/envelope.md>
21+
22+
use sigstore_protobuf_specs::io::intoto::{Envelope, Signature as DsseSignature};
23+
24+
use crate::bundle::intoto::Statement;
25+
26+
/// The DSSE payload type for in-toto attestations.
27+
pub const PAYLOAD_TYPE_INTOTO: &str = "application/vnd.in-toto+json";
28+
29+
/// A wrapper around the protobuf `Envelope` that provides a convenient API for
30+
/// creating and manipulating DSSE envelopes.
31+
///
32+
/// # Example
33+
///
34+
/// ```no_run
35+
/// use sigstore::bundle::dsse::DsseEnvelope;
36+
/// use sigstore::bundle::intoto::{StatementBuilder, Subject};
37+
/// use serde_json::json;
38+
///
39+
/// let statement = StatementBuilder::new()
40+
/// .subject(Subject::new("myapp.tar.gz", "sha256", "abc123..."))
41+
/// .predicate_type("https://slsa.dev/provenance/v1")
42+
/// .predicate(json!({"buildType": "test"}))
43+
/// .build()
44+
/// .unwrap();
45+
///
46+
/// let mut envelope = DsseEnvelope::from_statement(&statement).unwrap();
47+
///
48+
/// // Compute PAE for signing
49+
/// let pae_bytes = envelope.pae();
50+
///
51+
/// // Add signature (signing logic not shown)
52+
/// let signature_bytes: Vec<u8> = vec![]; // Your signature here
53+
/// envelope.add_signature(signature_bytes, "".to_string());
54+
/// ```
55+
#[derive(Debug, Clone)]
56+
pub struct DsseEnvelope(Envelope);
57+
58+
impl DsseEnvelope {
59+
/// Creates a new DSSE envelope from an in-toto statement.
60+
///
61+
/// The statement is serialized to JSON as the payload.
62+
/// The envelope is returned without signatures - use [`add_signature`] to add signatures.
63+
pub fn from_statement(statement: &Statement) -> Result<Self, serde_json::Error> {
64+
let payload_json = serde_json::to_vec(statement)?;
65+
66+
Ok(Self(Envelope {
67+
payload: payload_json, // Store RAW bytes, not base64!
68+
payload_type: PAYLOAD_TYPE_INTOTO.to_string(),
69+
signatures: vec![],
70+
}))
71+
}
72+
73+
/// Creates a DSSE envelope from a raw protobuf `Envelope`.
74+
pub fn from_envelope(envelope: Envelope) -> Self {
75+
Self(envelope)
76+
}
77+
78+
/// Computes the DSSE Pre-Authentication Encoding (PAE) for this envelope.
79+
///
80+
/// The PAE format is:
81+
/// ```text
82+
/// "DSSEv1" + SP + LEN(type) + SP + type + SP + LEN(body) + SP + body
83+
/// ```
84+
///
85+
/// Where:
86+
/// - `SP` is an ASCII space (0x20)
87+
/// - `LEN(s)` is the length of string `s` in ASCII decimal
88+
/// - `type` is the `payloadType` field
89+
/// - `body` is the `payload` field
90+
///
91+
/// # Example
92+
///
93+
/// ```text
94+
/// DSSEv1 28 application/vnd.in-toto+json 123 <payload-bytes>
95+
/// ```
96+
pub fn pae(&self) -> Vec<u8> {
97+
let payload_type = &self.0.payload_type;
98+
let payload = &self.0.payload;
99+
100+
// Format: "DSSEv1 <type_len> <type> <payload_len> <payload>"
101+
let mut pae = format!("DSSEv1 {} {} ", payload_type.len(), payload_type).into_bytes();
102+
pae.extend_from_slice(format!("{} ", payload.len()).as_bytes());
103+
pae.extend_from_slice(payload);
104+
105+
pae
106+
}
107+
108+
/// Adds a signature to this envelope.
109+
///
110+
/// The signature should be computed over the PAE (Pre-Authentication Encoding) of the envelope.
111+
/// Use [`pae`] to compute the PAE that should be signed.
112+
pub fn add_signature(&mut self, signature: Vec<u8>, keyid: String) {
113+
self.0.signatures.push(DsseSignature {
114+
keyid,
115+
sig: signature,
116+
});
117+
}
118+
119+
/// Returns a reference to the underlying protobuf `Envelope`.
120+
pub fn as_inner(&self) -> &Envelope {
121+
&self.0
122+
}
123+
124+
/// Returns a mutable reference to the underlying protobuf `Envelope`.
125+
pub fn as_inner_mut(&mut self) -> &mut Envelope {
126+
&mut self.0
127+
}
128+
129+
/// Consumes this wrapper and returns the underlying protobuf `Envelope`.
130+
pub fn into_inner(self) -> Envelope {
131+
self.0
132+
}
133+
134+
/// Returns the payload of this envelope.
135+
pub fn payload(&self) -> &[u8] {
136+
&self.0.payload
137+
}
138+
139+
/// Returns the payload type of this envelope.
140+
pub fn payload_type(&self) -> &str {
141+
&self.0.payload_type
142+
}
143+
144+
/// Returns the signatures in this envelope.
145+
pub fn signatures(&self) -> &[DsseSignature] {
146+
&self.0.signatures
147+
}
148+
}
149+
150+
#[cfg(test)]
151+
mod tests {
152+
use super::*;
153+
use crate::bundle::intoto::{StatementBuilder, Subject};
154+
use serde_json::json;
155+
156+
#[test]
157+
fn test_pae_format() {
158+
// Test the PAE format matches the specification
159+
let envelope = DsseEnvelope::from_envelope(Envelope {
160+
payload: b"test payload".to_vec(),
161+
payload_type: "application/test".to_string(),
162+
signatures: vec![],
163+
});
164+
165+
let result = envelope.pae();
166+
let expected = b"DSSEv1 16 application/test 12 test payload";
167+
168+
assert_eq!(result, expected);
169+
}
170+
171+
#[test]
172+
fn test_pae_with_intoto() {
173+
// Test with a typical in-toto payload type
174+
let envelope = DsseEnvelope::from_envelope(Envelope {
175+
payload: b"{\"_type\":\"https://in-toto.io/Statement/v1\"}".to_vec(),
176+
payload_type: "application/vnd.in-toto+json".to_string(),
177+
signatures: vec![],
178+
});
179+
180+
let result = envelope.pae();
181+
182+
// Should start with the correct prefix
183+
assert!(result.starts_with(b"DSSEv1 28 application/vnd.in-toto+json "));
184+
185+
// Should contain the payload length and payload
186+
assert!(result.ends_with(b" {\"_type\":\"https://in-toto.io/Statement/v1\"}"));
187+
}
188+
189+
#[test]
190+
fn test_create_envelope() {
191+
let statement = StatementBuilder::new()
192+
.subject(Subject::new("test.tar.gz", "sha256", "abc123"))
193+
.predicate_type("https://slsa.dev/provenance/v1")
194+
.predicate(json!({"buildType": "test"}))
195+
.build()
196+
.unwrap();
197+
198+
let envelope = DsseEnvelope::from_statement(&statement).unwrap();
199+
200+
assert_eq!(envelope.payload_type(), PAYLOAD_TYPE_INTOTO);
201+
assert_eq!(envelope.signatures().len(), 0);
202+
203+
// Verify the payload matches the statement
204+
let parsed_statement: crate::bundle::intoto::Statement =
205+
serde_json::from_slice(envelope.payload()).unwrap();
206+
207+
assert_eq!(
208+
parsed_statement.statement_type,
209+
crate::bundle::intoto::STATEMENT_TYPE_V1
210+
);
211+
assert_eq!(parsed_statement.subject[0].name, "test.tar.gz");
212+
}
213+
214+
#[test]
215+
fn test_add_signature() {
216+
let statement = StatementBuilder::new()
217+
.subject(Subject::new("test.tar.gz", "sha256", "abc123"))
218+
.predicate_type("https://slsa.dev/provenance/v1")
219+
.predicate(json!({"buildType": "test"}))
220+
.build()
221+
.unwrap();
222+
223+
let mut envelope = DsseEnvelope::from_statement(&statement).unwrap();
224+
let signature = vec![1, 2, 3, 4, 5];
225+
226+
envelope.add_signature(signature.clone(), "test-key".to_string());
227+
228+
assert_eq!(envelope.signatures().len(), 1);
229+
assert_eq!(envelope.signatures()[0].sig, signature);
230+
assert_eq!(envelope.signatures()[0].keyid, "test-key");
231+
}
232+
233+
#[test]
234+
fn test_pae_with_created_envelope() {
235+
let statement = StatementBuilder::new()
236+
.subject(Subject::new("test.tar.gz", "sha256", "abc123"))
237+
.predicate_type("https://slsa.dev/provenance/v1")
238+
.predicate(json!({"buildType": "test"}))
239+
.build()
240+
.unwrap();
241+
242+
let envelope = DsseEnvelope::from_statement(&statement).unwrap();
243+
let pae_result = envelope.pae();
244+
245+
// PAE should start with the correct format
246+
assert!(pae_result.starts_with(b"DSSEv1 28 application/vnd.in-toto+json "));
247+
248+
// PAE should contain the payload
249+
let payload_str = String::from_utf8(envelope.payload().to_vec()).unwrap();
250+
assert!(String::from_utf8(pae_result)
251+
.unwrap()
252+
.contains(&payload_str));
253+
}
254+
255+
#[test]
256+
fn test_accessors() {
257+
let statement = StatementBuilder::new()
258+
.subject(Subject::new("test.tar.gz", "sha256", "abc123"))
259+
.predicate_type("https://slsa.dev/provenance/v1")
260+
.predicate(json!({"buildType": "test"}))
261+
.build()
262+
.unwrap();
263+
264+
let mut envelope = DsseEnvelope::from_statement(&statement).unwrap();
265+
266+
// Test accessors
267+
assert_eq!(envelope.payload_type(), PAYLOAD_TYPE_INTOTO);
268+
assert!(!envelope.payload().is_empty());
269+
assert_eq!(envelope.signatures().len(), 0);
270+
271+
// Test mutable access
272+
envelope.as_inner_mut().payload_type = "test".to_string();
273+
assert_eq!(envelope.payload_type(), "test");
274+
275+
// Test into_inner
276+
let inner = envelope.into_inner();
277+
assert_eq!(inner.payload_type, "test");
278+
}
279+
}

0 commit comments

Comments
 (0)