Skip to main content

sigstore_trust_root/
trusted_root.rs

1//! Trusted root types and parsing
2
3use crate::{Error, Result};
4use chrono::{DateTime, Utc};
5use rustls_pki_types::CertificateDer;
6use serde::{Deserialize, Serialize};
7use sigstore_types::{DerCertificate, DerPublicKey, HashAlgorithm, KeyHint, LogId, LogKeyId};
8use std::collections::HashMap;
9
10/// TSA certificate with optional validity period (start, end)
11pub type TsaCertWithValidity = (
12    CertificateDer<'static>,
13    Option<DateTime<Utc>>,
14    Option<DateTime<Utc>>,
15);
16
17/// A trusted root bundle containing all trust anchors
18#[derive(Debug, Clone, Deserialize, Serialize)]
19#[serde(rename_all = "camelCase")]
20pub struct TrustedRoot {
21    /// Media type of the trusted root
22    pub media_type: String,
23
24    /// Transparency logs (Rekor)
25    #[serde(default)]
26    pub tlogs: Vec<TransparencyLog>,
27
28    /// Certificate authorities (Fulcio)
29    #[serde(default)]
30    pub certificate_authorities: Vec<CertificateAuthority>,
31
32    /// Certificate Transparency logs
33    #[serde(default)]
34    pub ctlogs: Vec<CertificateTransparencyLog>,
35
36    /// Timestamp authorities (RFC 3161 TSAs)
37    #[serde(default)]
38    pub timestamp_authorities: Vec<TimestampAuthority>,
39}
40
41/// A transparency log entry (Rekor)
42#[derive(Debug, Clone, Deserialize, Serialize)]
43#[serde(rename_all = "camelCase")]
44pub struct TransparencyLog {
45    /// Base URL of the transparency log
46    pub base_url: String,
47
48    /// Hash algorithm used
49    pub hash_algorithm: HashAlgorithm,
50
51    /// Public key for verification
52    pub public_key: PublicKey,
53
54    /// Log ID
55    pub log_id: LogId,
56}
57
58/// A certificate authority entry (Fulcio)
59#[derive(Debug, Clone, Deserialize, Serialize)]
60#[serde(rename_all = "camelCase")]
61pub struct CertificateAuthority {
62    /// Subject information
63    #[serde(default)]
64    pub subject: CertificateSubject,
65
66    /// URI of the CA
67    pub uri: String,
68
69    /// Certificate chain
70    pub cert_chain: CertChain,
71
72    /// Validity period
73    #[serde(default)]
74    pub valid_for: Option<ValidityPeriod>,
75}
76
77/// A Certificate Transparency log entry
78#[derive(Debug, Clone, Deserialize, Serialize)]
79#[serde(rename_all = "camelCase")]
80pub struct CertificateTransparencyLog {
81    /// Base URL of the CT log
82    pub base_url: String,
83
84    /// Hash algorithm used
85    pub hash_algorithm: HashAlgorithm,
86
87    /// Public key for verification
88    pub public_key: PublicKey,
89
90    /// Log ID
91    pub log_id: LogId,
92}
93
94/// A timestamp authority entry
95#[derive(Debug, Clone, Deserialize, Serialize)]
96#[serde(rename_all = "camelCase")]
97pub struct TimestampAuthority {
98    /// Subject information
99    #[serde(default)]
100    pub subject: CertificateSubject,
101
102    /// URI of the TSA
103    #[serde(default)]
104    pub uri: Option<String>,
105
106    /// Certificate chain
107    pub cert_chain: CertChain,
108
109    /// Validity period
110    #[serde(default)]
111    pub valid_for: Option<ValidityPeriod>,
112}
113
114/// Public key information
115#[derive(Debug, Clone, Deserialize, Serialize)]
116#[serde(rename_all = "camelCase")]
117pub struct PublicKey {
118    /// Raw bytes of the public key (DER-encoded)
119    pub raw_bytes: DerPublicKey,
120
121    /// Key details/type
122    pub key_details: String,
123
124    /// Validity period for this key
125    #[serde(default)]
126    pub valid_for: Option<ValidityPeriod>,
127}
128
129/// Subject information for a certificate.
130///
131/// Note: This is different from `sigstore_types::Subject` which represents
132/// an in-toto Statement subject (artifact name + digest).
133#[derive(Debug, Clone, Default, Deserialize, Serialize)]
134#[serde(rename_all = "camelCase")]
135pub struct CertificateSubject {
136    /// Organization name
137    #[serde(default)]
138    pub organization: Option<String>,
139
140    /// Common name
141    #[serde(default)]
142    pub common_name: Option<String>,
143}
144
145/// Certificate chain
146#[derive(Debug, Clone, Deserialize, Serialize)]
147#[serde(rename_all = "camelCase")]
148pub struct CertChain {
149    /// Certificates in the chain
150    pub certificates: Vec<CertificateEntry>,
151}
152
153/// A certificate entry
154#[derive(Debug, Clone, Deserialize, Serialize)]
155#[serde(rename_all = "camelCase")]
156pub struct CertificateEntry {
157    /// Raw bytes of the certificate (DER-encoded)
158    pub raw_bytes: DerCertificate,
159}
160
161/// Validity period for a key or certificate
162#[derive(Debug, Clone, Deserialize, Serialize)]
163#[serde(rename_all = "camelCase")]
164pub struct ValidityPeriod {
165    /// Start time (ISO 8601)
166    #[serde(default)]
167    pub start: Option<String>,
168
169    /// End time (ISO 8601)
170    #[serde(default)]
171    pub end: Option<String>,
172}
173
174impl TrustedRoot {
175    /// Parse a trusted root from JSON
176    pub fn from_json(json: &str) -> Result<Self> {
177        Ok(serde_json::from_str(json)?)
178    }
179
180    /// Load a trusted root from a file
181    pub fn from_file(path: impl AsRef<std::path::Path>) -> Result<Self> {
182        let json =
183            std::fs::read_to_string(path).map_err(|e| Error::Json(serde_json::Error::io(e)))?;
184        Self::from_json(&json)
185    }
186
187    /// Get all Fulcio certificate authority certificates
188    pub fn fulcio_certs(&self) -> Result<Vec<CertificateDer<'static>>> {
189        let mut certs = Vec::new();
190        for ca in &self.certificate_authorities {
191            for cert_entry in &ca.cert_chain.certificates {
192                certs.push(CertificateDer::from(cert_entry.raw_bytes.as_bytes()).into_owned());
193            }
194        }
195        Ok(certs)
196    }
197
198    /// Get all Rekor public keys mapped by key ID
199    pub fn rekor_keys(&self) -> Result<HashMap<String, Vec<u8>>> {
200        let mut keys = HashMap::new();
201        for tlog in &self.tlogs {
202            keys.insert(
203                tlog.log_id.key_id.to_string(),
204                tlog.public_key.raw_bytes.as_bytes().to_vec(),
205            );
206        }
207        Ok(keys)
208    }
209
210    /// Get all Rekor public keys with their key hints (4-byte identifiers)
211    ///
212    /// Returns a vector of (key_hint, public_key) tuples where key_hint is
213    /// the first 4 bytes of the keyId from the log_id field.
214    pub fn rekor_keys_with_hints(&self) -> Result<Vec<(KeyHint, DerPublicKey)>> {
215        let mut keys = Vec::new();
216        for tlog in &self.tlogs {
217            // Decode the key_id to get the key hint (first 4 bytes)
218            let key_id_bytes = tlog.log_id.key_id.decode()?;
219
220            if key_id_bytes.len() >= 4 {
221                let key_hint = KeyHint::new([
222                    key_id_bytes[0],
223                    key_id_bytes[1],
224                    key_id_bytes[2],
225                    key_id_bytes[3],
226                ]);
227                keys.push((key_hint, tlog.public_key.raw_bytes.clone()));
228            }
229        }
230        Ok(keys)
231    }
232
233    /// Get a specific Rekor public key by log ID
234    pub fn rekor_key_for_log(&self, log_id: &LogKeyId) -> Result<DerPublicKey> {
235        for tlog in &self.tlogs {
236            if &tlog.log_id.key_id == log_id {
237                return Ok(tlog.public_key.raw_bytes.clone());
238            }
239        }
240        Err(Error::KeyNotFound(log_id.to_string()))
241    }
242
243    /// Get all Certificate Transparency log public keys mapped by key ID
244    pub fn ctfe_keys(&self) -> Result<HashMap<LogKeyId, DerPublicKey>> {
245        let mut keys = HashMap::new();
246        for ctlog in &self.ctlogs {
247            keys.insert(
248                ctlog.log_id.key_id.clone(),
249                ctlog.public_key.raw_bytes.clone(),
250            );
251        }
252        Ok(keys)
253    }
254
255    /// Get all Certificate Transparency log public keys with their SHA-256 log IDs
256    /// Returns a list of (log_id, public_key) pairs where log_id is the SHA-256 hash
257    /// of the public key (used for matching against SCTs)
258    pub fn ctfe_keys_with_ids(&self) -> Result<Vec<(Vec<u8>, DerPublicKey)>> {
259        let mut result = Vec::new();
260        for ctlog in &self.ctlogs {
261            let key_bytes = ctlog.public_key.raw_bytes.as_bytes();
262            // Compute SHA-256 hash of the public key to get the log ID
263            let log_id = sigstore_crypto::sha256(key_bytes).as_bytes().to_vec();
264            result.push((log_id, ctlog.public_key.raw_bytes.clone()));
265        }
266        Ok(result)
267    }
268
269    /// Get all TSA certificates with their validity periods
270    pub fn tsa_certs_with_validity(&self) -> Result<Vec<TsaCertWithValidity>> {
271        let mut result = Vec::new();
272
273        for tsa in &self.timestamp_authorities {
274            for cert_entry in &tsa.cert_chain.certificates {
275                let cert_der = cert_entry.raw_bytes.as_bytes().to_vec();
276
277                // Parse validity period
278                let (start, end) = if let Some(valid_for) = &tsa.valid_for {
279                    let start = valid_for
280                        .start
281                        .as_ref()
282                        .and_then(|s| DateTime::parse_from_rfc3339(s).ok())
283                        .map(|dt| dt.with_timezone(&Utc));
284                    let end = valid_for
285                        .end
286                        .as_ref()
287                        .and_then(|s| DateTime::parse_from_rfc3339(s).ok())
288                        .map(|dt| dt.with_timezone(&Utc));
289                    (start, end)
290                } else {
291                    (None, None)
292                };
293
294                result.push((CertificateDer::from(&cert_der[..]).into_owned(), start, end));
295            }
296        }
297
298        Ok(result)
299    }
300
301    /// Get TSA root certificates (for chain validation)
302    pub fn tsa_root_certs(&self) -> Result<Vec<CertificateDer<'static>>> {
303        let mut roots = Vec::new();
304        for tsa in &self.timestamp_authorities {
305            // The last certificate in the chain is typically the root
306            if let Some(cert_entry) = tsa.cert_chain.certificates.last() {
307                roots.push(CertificateDer::from(cert_entry.raw_bytes.as_bytes()).into_owned());
308            }
309        }
310        Ok(roots)
311    }
312
313    /// Get TSA intermediate certificates (for chain validation)
314    pub fn tsa_intermediate_certs(&self) -> Result<Vec<CertificateDer<'static>>> {
315        let mut intermediates = Vec::new();
316        for tsa in &self.timestamp_authorities {
317            // All certificates except the first (leaf) and last (root) are intermediates
318            let chain_len = tsa.cert_chain.certificates.len();
319            if chain_len > 2 {
320                for cert_entry in &tsa.cert_chain.certificates[1..chain_len - 1] {
321                    intermediates
322                        .push(CertificateDer::from(cert_entry.raw_bytes.as_bytes()).into_owned());
323                }
324            }
325        }
326        Ok(intermediates)
327    }
328
329    /// Get TSA leaf certificates (the first certificate in each chain)
330    /// These are the actual TSA signing certificates
331    pub fn tsa_leaf_certs(&self) -> Result<Vec<CertificateDer<'static>>> {
332        let mut leaves = Vec::new();
333        for tsa in &self.timestamp_authorities {
334            // The first certificate in the chain is the leaf (TSA signing cert)
335            if let Some(cert_entry) = tsa.cert_chain.certificates.first() {
336                leaves.push(CertificateDer::from(cert_entry.raw_bytes.as_bytes()).into_owned());
337            }
338        }
339        Ok(leaves)
340    }
341
342    /// Check if a Rekor key ID exists in the trusted root
343    pub fn has_rekor_key(&self, key_id: &LogKeyId) -> bool {
344        self.tlogs.iter().any(|tlog| &tlog.log_id.key_id == key_id)
345    }
346
347    /// Get the validity period for a TSA at a given time
348    pub fn tsa_validity_for_time(
349        &self,
350        timestamp: DateTime<Utc>,
351    ) -> Result<Option<(DateTime<Utc>, DateTime<Utc>)>> {
352        for tsa in &self.timestamp_authorities {
353            if let Some(valid_for) = &tsa.valid_for {
354                let start = valid_for
355                    .start
356                    .as_ref()
357                    .and_then(|s| DateTime::parse_from_rfc3339(s).ok())
358                    .map(|dt| dt.with_timezone(&Utc));
359                let end = valid_for
360                    .end
361                    .as_ref()
362                    .and_then(|s| DateTime::parse_from_rfc3339(s).ok())
363                    .map(|dt| dt.with_timezone(&Utc));
364
365                // Check if timestamp falls within this TSA's validity
366                if let (Some(start_time), Some(end_time)) = (start, end) {
367                    if timestamp >= start_time && timestamp <= end_time {
368                        return Ok(Some((start_time, end_time)));
369                    }
370                } else if let Some(start_time) = start {
371                    // Only start time specified, check if after start
372                    if timestamp >= start_time {
373                        return Ok(start.zip(end));
374                    }
375                }
376            }
377        }
378        Ok(None)
379    }
380
381    /// Check if a timestamp is within any TSA's validity period from the trust root
382    ///
383    /// Returns true if:
384    /// - There are no timestamp authorities configured (no TSA verification)
385    /// - Any TSA has no `valid_for` field (open-ended validity)
386    /// - The timestamp falls within at least one TSA's `valid_for` period
387    ///
388    /// Returns false only if there are TSAs with validity constraints and the
389    /// timestamp doesn't fall within any of them.
390    pub fn is_timestamp_within_tsa_validity(&self, timestamp: DateTime<Utc>) -> bool {
391        // If no TSAs are configured, no validity check needed
392        if self.timestamp_authorities.is_empty() {
393            return true;
394        }
395
396        for tsa in &self.timestamp_authorities {
397            // If a TSA has no valid_for constraint, it's valid for all time
398            let Some(valid_for) = &tsa.valid_for else {
399                return true;
400            };
401
402            let start = valid_for
403                .start
404                .as_ref()
405                .and_then(|s| DateTime::parse_from_rfc3339(s).ok())
406                .map(|dt| dt.with_timezone(&Utc));
407            let end = valid_for
408                .end
409                .as_ref()
410                .and_then(|s| DateTime::parse_from_rfc3339(s).ok())
411                .map(|dt| dt.with_timezone(&Utc));
412
413            // Check if timestamp falls within this TSA's validity period
414            let after_start = start.map_or(true, |s| timestamp >= s);
415            let before_end = end.map_or(true, |e| timestamp <= e);
416
417            if after_start && before_end {
418                return true;
419            }
420        }
421
422        // No TSA's validity period matched
423        false
424    }
425}
426
427/// Embedded production trusted root from <https://tuf-repo-cdn.sigstore.dev/>
428/// This is the default trusted root for Sigstore's public production instance.
429pub const SIGSTORE_PRODUCTION_TRUSTED_ROOT: &str = include_str!("trusted_root.json");
430
431/// Embedded staging trusted root from <https://tuf-repo-cdn.sigstage.dev/>
432/// This is the trusted root for Sigstore's staging/testing instance.
433pub const SIGSTORE_STAGING_TRUSTED_ROOT: &str = include_str!("trusted_root_staging.json");
434
435impl TrustedRoot {
436    /// Load the default Sigstore production trusted root
437    pub fn production() -> Result<Self> {
438        Self::from_json(SIGSTORE_PRODUCTION_TRUSTED_ROOT)
439    }
440
441    /// Load the Sigstore staging trusted root
442    ///
443    /// This is useful for testing against the Sigstore staging environment
444    /// at <https://sigstage.dev>.
445    pub fn staging() -> Result<Self> {
446        Self::from_json(SIGSTORE_STAGING_TRUSTED_ROOT)
447    }
448}
449
450#[cfg(test)]
451mod tests {
452    use super::*;
453
454    const SAMPLE_TRUSTED_ROOT: &str = r#"{
455        "mediaType": "application/vnd.dev.sigstore.trustedroot+json;version=0.1",
456        "tlogs": [{
457            "baseUrl": "https://rekor.sigstore.dev",
458            "hashAlgorithm": "SHA2_256",
459            "publicKey": {
460                "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEYI4heOTrNrZO27elFE8ynfrdPMikttRkbe+vJKQ50G6bfwQ3WyhLpRwwwohelDAm8xRzJ56nYsIa3VHivVvpmA==",
461                "keyDetails": "PKIX_ECDSA_P256_SHA_256"
462            },
463            "logId": {
464                "keyId": "test-key-id"
465            }
466        }],
467        "certificateAuthorities": [],
468        "ctlogs": [],
469        "timestampAuthorities": []
470    }"#;
471
472    #[test]
473    fn test_parse_trusted_root() {
474        let root = TrustedRoot::from_json(SAMPLE_TRUSTED_ROOT).unwrap();
475        assert_eq!(root.tlogs.len(), 1);
476        assert_eq!(
477            root.tlogs[0].log_id.key_id,
478            LogKeyId::new("test-key-id".to_string())
479        );
480    }
481
482    #[test]
483    fn test_rekor_keys() {
484        let root = TrustedRoot::from_json(SAMPLE_TRUSTED_ROOT).unwrap();
485        let keys = root.rekor_keys().unwrap();
486        assert_eq!(keys.len(), 1);
487        assert!(keys.contains_key("test-key-id"));
488    }
489
490    #[test]
491    fn test_has_rekor_key() {
492        let root = TrustedRoot::from_json(SAMPLE_TRUSTED_ROOT).unwrap();
493        assert!(root.has_rekor_key(&LogKeyId::new("test-key-id".to_string())));
494        assert!(!root.has_rekor_key(&LogKeyId::new("non-existent".to_string())));
495    }
496
497    #[test]
498    fn test_production_trusted_root() {
499        let root = TrustedRoot::production().unwrap();
500        assert!(!root.tlogs.is_empty());
501        assert!(!root.certificate_authorities.is_empty());
502        assert!(!root.ctlogs.is_empty());
503    }
504
505    #[test]
506    fn test_staging_trusted_root() {
507        let root = TrustedRoot::staging().unwrap();
508        assert!(!root.tlogs.is_empty());
509        assert!(!root.certificate_authorities.is_empty());
510        assert!(!root.ctlogs.is_empty());
511        // Staging should have different URLs from production
512        assert!(root.tlogs[0].base_url.contains("sigstage.dev"));
513    }
514}