Skip to main content

sigstore_bundle/
builder.rs

1//! Bundle builder for creating Sigstore bundles
2
3use sigstore_rekor::entry::LogEntry;
4use sigstore_types::{
5    bundle::{
6        CertificateContent, CheckpointData, InclusionPromise, InclusionProof, KindVersion, LogId,
7        MessageSignature, Rfc3161Timestamp, SignatureContent, TimestampVerificationData,
8        TransparencyLogEntry, VerificationMaterial, VerificationMaterialContent,
9    },
10    Bundle, CanonicalizedBody, DerCertificate, DsseEnvelope, LogIndex, LogKeyId, MediaType,
11    Sha256Hash, SignatureBytes, SignedTimestamp, TimestampToken,
12};
13
14/// Verification material for v0.3 bundles.
15///
16/// In v0.3 bundles, only a single certificate or a public key hint is allowed.
17/// Certificate chains are NOT permitted in v0.3 format.
18#[derive(Debug, Clone)]
19pub enum VerificationMaterialV03 {
20    /// Single certificate (the common case for Fulcio-issued certs)
21    Certificate(DerCertificate),
22    /// Public key hint (for pre-existing keys)
23    PublicKey { hint: String },
24}
25
26/// A Sigstore bundle in v0.3 format.
27///
28/// The v0.3 format requires:
29/// - A single certificate (not a chain) or public key hint
30/// - Either a message signature or DSSE envelope
31/// - Optional transparency log entries and RFC 3161 timestamps
32///
33/// # Example
34///
35/// ```ignore
36/// use sigstore_bundle::BundleV03;
37///
38/// let bundle = BundleV03::with_certificate_and_signature(cert_der, signature, artifact_hash)
39///     .with_tlog_entry(tlog_entry)
40///     .into_bundle();
41/// ```
42#[derive(Debug, Clone)]
43pub struct BundleV03 {
44    /// Verification material - either a certificate or public key
45    pub verification: VerificationMaterialV03,
46    /// The signature content (message signature or DSSE envelope)
47    pub content: SignatureContent,
48    /// Transparency log entries
49    pub tlog_entries: Vec<TransparencyLogEntry>,
50    /// RFC 3161 timestamps
51    pub rfc3161_timestamps: Vec<Rfc3161Timestamp>,
52}
53
54impl BundleV03 {
55    /// Create a new v0.3 bundle with the required fields.
56    pub fn new(verification: VerificationMaterialV03, content: SignatureContent) -> Self {
57        Self {
58            verification,
59            content,
60            tlog_entries: Vec::new(),
61            rfc3161_timestamps: Vec::new(),
62        }
63    }
64
65    /// Create a new v0.3 bundle with a certificate and message signature.
66    ///
67    /// This is the most common case for Sigstore signing with Fulcio certificates.
68    pub fn with_certificate_and_signature(
69        certificate: DerCertificate,
70        signature: SignatureBytes,
71        artifact_digest: Sha256Hash,
72    ) -> Self {
73        Self::new(
74            VerificationMaterialV03::Certificate(certificate),
75            SignatureContent::MessageSignature(MessageSignature {
76                message_digest: Some(sigstore_types::bundle::MessageDigest {
77                    algorithm: sigstore_types::HashAlgorithm::Sha2256,
78                    digest: artifact_digest,
79                }),
80                signature,
81            }),
82        )
83    }
84
85    /// Create a new v0.3 bundle with a certificate and DSSE envelope.
86    ///
87    /// Used for attestations (in-toto statements).
88    pub fn with_certificate_and_dsse(certificate: DerCertificate, envelope: DsseEnvelope) -> Self {
89        Self::new(
90            VerificationMaterialV03::Certificate(certificate),
91            SignatureContent::DsseEnvelope(envelope),
92        )
93    }
94
95    /// Add a transparency log entry.
96    pub fn with_tlog_entry(mut self, entry: TransparencyLogEntry) -> Self {
97        self.tlog_entries.push(entry);
98        self
99    }
100
101    /// Add an RFC 3161 timestamp.
102    pub fn with_rfc3161_timestamp(mut self, timestamp: TimestampToken) -> Self {
103        self.rfc3161_timestamps.push(Rfc3161Timestamp {
104            signed_timestamp: timestamp,
105        });
106        self
107    }
108
109    /// Convert to a serializable Bundle.
110    pub fn into_bundle(self) -> Bundle {
111        let verification_content = match self.verification {
112            VerificationMaterialV03::Certificate(cert) => {
113                VerificationMaterialContent::Certificate(CertificateContent { raw_bytes: cert })
114            }
115            VerificationMaterialV03::PublicKey { hint } => {
116                VerificationMaterialContent::PublicKey { hint }
117            }
118        };
119
120        Bundle {
121            media_type: MediaType::Bundle0_3.as_str().to_string(),
122            verification_material: VerificationMaterial {
123                content: verification_content,
124                tlog_entries: self.tlog_entries,
125                timestamp_verification_data: TimestampVerificationData {
126                    rfc3161_timestamps: self.rfc3161_timestamps,
127                },
128            },
129            content: self.content,
130        }
131    }
132}
133
134/// Helper to create a transparency log entry.
135pub struct TlogEntryBuilder {
136    log_index: i64,
137    log_id: String,
138    kind: String,
139    kind_version: String,
140    integrated_time: i64,
141    canonicalized_body: Vec<u8>,
142    inclusion_promise: Option<InclusionPromise>,
143    inclusion_proof: Option<InclusionProof>,
144}
145
146impl TlogEntryBuilder {
147    /// Create a new tlog entry builder.
148    pub fn new() -> Self {
149        Self {
150            log_index: 0,
151            log_id: String::new(),
152            kind: "hashedrekord".to_string(),
153            kind_version: "0.0.1".to_string(),
154            integrated_time: 0,
155            canonicalized_body: Vec::new(),
156            inclusion_promise: None,
157            inclusion_proof: None,
158        }
159    }
160
161    /// Create a tlog entry builder from a Rekor LogEntry response.
162    ///
163    /// This method extracts all relevant fields from a Rekor API response
164    /// and populates the builder automatically.
165    ///
166    /// # Arguments
167    /// * `entry` - The LogEntry returned from the Rekor API
168    /// * `kind` - The entry kind (e.g., "hashedrekord", "dsse")
169    /// * `version` - The entry version (e.g., "0.0.1")
170    pub fn from_log_entry(entry: &LogEntry, kind: &str, version: &str) -> Self {
171        // Convert hex log_id to base64 using the type-safe method
172        let log_id_base64 = entry
173            .log_id
174            .to_base64()
175            .unwrap_or_else(|_| entry.log_id.to_string());
176
177        let mut builder = Self {
178            log_index: entry.log_index,
179            log_id: log_id_base64,
180            kind: kind.to_string(),
181            kind_version: version.to_string(),
182            integrated_time: entry.integrated_time,
183            canonicalized_body: entry.body.as_bytes().to_vec(),
184            inclusion_promise: None,
185            inclusion_proof: None,
186        };
187
188        // Add verification data if present
189        if let Some(verification) = &entry.verification {
190            if let Some(set) = &verification.signed_entry_timestamp {
191                builder.inclusion_promise = Some(InclusionPromise {
192                    signed_entry_timestamp: set.clone(),
193                });
194            }
195
196            if let Some(proof) = &verification.inclusion_proof {
197                // Rekor V1 API returns hashes as hex, bundle format expects base64
198                // Convert root_hash from hex to Sha256Hash
199                let root_hash = Sha256Hash::from_hex(&proof.root_hash)
200                    .unwrap_or_else(|_| Sha256Hash::from_bytes([0u8; 32]));
201
202                // Convert all proof hashes from hex to Sha256Hash
203                let hashes: Vec<Sha256Hash> = proof
204                    .hashes
205                    .iter()
206                    .filter_map(|h| Sha256Hash::from_hex(h).ok())
207                    .collect();
208
209                builder.inclusion_proof = Some(InclusionProof {
210                    log_index: LogIndex::new(proof.log_index),
211                    root_hash,
212                    tree_size: proof.tree_size,
213                    hashes,
214                    checkpoint: CheckpointData {
215                        envelope: proof.checkpoint.clone(),
216                    },
217                });
218            }
219        }
220
221        builder
222    }
223
224    /// Set the log index.
225    pub fn log_index(mut self, index: i64) -> Self {
226        self.log_index = index;
227        self
228    }
229
230    /// Set the integrated time (Unix timestamp).
231    pub fn integrated_time(mut self, time: i64) -> Self {
232        self.integrated_time = time;
233        self
234    }
235
236    /// Set the inclusion promise (Signed Entry Timestamp).
237    pub fn inclusion_promise(mut self, signed_entry_timestamp: SignedTimestamp) -> Self {
238        self.inclusion_promise = Some(InclusionPromise {
239            signed_entry_timestamp,
240        });
241        self
242    }
243
244    /// Set the inclusion proof.
245    ///
246    /// # Arguments
247    /// * `log_index` - The log index
248    /// * `root_hash` - The root hash
249    /// * `tree_size` - The tree size
250    /// * `hashes` - The proof hashes
251    /// * `checkpoint` - The checkpoint envelope
252    pub fn inclusion_proof(
253        mut self,
254        log_index: i64,
255        root_hash: Sha256Hash,
256        tree_size: i64,
257        hashes: Vec<Sha256Hash>,
258        checkpoint: String,
259    ) -> Self {
260        self.inclusion_proof = Some(InclusionProof {
261            log_index: LogIndex::from(log_index),
262            root_hash,
263            tree_size,
264            hashes,
265            checkpoint: CheckpointData {
266                envelope: checkpoint,
267            },
268        });
269        self
270    }
271
272    /// Build the transparency log entry.
273    pub fn build(self) -> TransparencyLogEntry {
274        TransparencyLogEntry {
275            log_index: LogIndex::from(self.log_index),
276            log_id: LogId {
277                key_id: LogKeyId::new(self.log_id),
278            },
279            kind_version: KindVersion {
280                kind: self.kind,
281                version: self.kind_version,
282            },
283            integrated_time: self.integrated_time,
284            inclusion_promise: self.inclusion_promise,
285            inclusion_proof: self.inclusion_proof,
286            canonicalized_body: CanonicalizedBody::new(self.canonicalized_body),
287        }
288    }
289}
290
291impl Default for TlogEntryBuilder {
292    fn default() -> Self {
293        Self::new()
294    }
295}