sigstore_bundle/
builder.rs

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