Skip to main content

sigstore_verify/
verify.rs

1//! High-level verification API
2//!
3//! This module provides the main entry point for verifying Sigstore signatures.
4
5use crate::error::{Error, Result};
6use sigstore_bundle::validate_bundle_with_options;
7use sigstore_bundle::ValidationOptions;
8use sigstore_crypto::parse_certificate_info;
9use sigstore_trust_root::TrustedRoot;
10
11use sigstore_types::{Artifact, Bundle, Sha256Hash, SignatureContent, Statement};
12
13/// Default clock skew tolerance in seconds (60 seconds = 1 minute)
14pub const DEFAULT_CLOCK_SKEW_SECONDS: i64 = 60;
15
16/// Policy for verifying signatures
17#[derive(Debug, Clone)]
18pub struct VerificationPolicy {
19    /// Expected identity (email or URI)
20    pub identity: Option<String>,
21    /// Expected issuer
22    pub issuer: Option<String>,
23    /// Verify transparency log inclusion
24    pub verify_tlog: bool,
25    /// Verify timestamp
26    pub verify_timestamp: bool,
27    /// Verify certificate chain
28    pub verify_certificate: bool,
29    /// Clock skew tolerance in seconds for time validation
30    ///
31    /// This allows for a tolerance when checking that integrated times
32    /// are not in the future. Default is 60 seconds.
33    pub clock_skew_seconds: i64,
34}
35
36impl Default for VerificationPolicy {
37    fn default() -> Self {
38        Self {
39            identity: None,
40            issuer: None,
41            verify_tlog: true,
42            verify_timestamp: true,
43            verify_certificate: true,
44            clock_skew_seconds: DEFAULT_CLOCK_SKEW_SECONDS,
45        }
46    }
47}
48
49impl VerificationPolicy {
50    /// Create a policy that requires a specific identity
51    pub fn with_identity(identity: impl Into<String>) -> Self {
52        Self {
53            identity: Some(identity.into()),
54            ..Default::default()
55        }
56    }
57
58    /// Create a policy that requires a specific issuer
59    pub fn with_issuer(issuer: impl Into<String>) -> Self {
60        Self {
61            issuer: Some(issuer.into()),
62            ..Default::default()
63        }
64    }
65
66    /// Require a specific identity
67    pub fn require_identity(mut self, identity: impl Into<String>) -> Self {
68        self.identity = Some(identity.into());
69        self
70    }
71
72    /// Require a specific issuer
73    pub fn require_issuer(mut self, issuer: impl Into<String>) -> Self {
74        self.issuer = Some(issuer.into());
75        self
76    }
77
78    /// Skip transparency log verification
79    pub fn skip_tlog(mut self) -> Self {
80        self.verify_tlog = false;
81        self
82    }
83
84    /// Skip timestamp verification
85    pub fn skip_timestamp(mut self) -> Self {
86        self.verify_timestamp = false;
87        self
88    }
89
90    /// Skip certificate chain verification
91    ///
92    /// WARNING: This is unsafe for production use. Only use for testing
93    /// with bundles that don't chain to the trusted root.
94    pub fn skip_certificate_chain(mut self) -> Self {
95        self.verify_certificate = false;
96        self
97    }
98
99    /// Set the clock skew tolerance in seconds
100    ///
101    /// This allows for a tolerance when checking that integrated times
102    /// are not in the future. Default is 60 seconds.
103    pub fn with_clock_skew_seconds(mut self, seconds: i64) -> Self {
104        self.clock_skew_seconds = seconds;
105        self
106    }
107}
108
109/// Result of verification
110#[derive(Debug)]
111pub struct VerificationResult {
112    /// Whether verification succeeded
113    pub success: bool,
114    /// Identity from the certificate
115    pub identity: Option<String>,
116    /// Issuer from the certificate
117    pub issuer: Option<String>,
118    /// Integrated time from transparency log
119    pub integrated_time: Option<i64>,
120    /// Any warnings during verification
121    pub warnings: Vec<String>,
122}
123
124impl VerificationResult {
125    /// Create a successful result
126    pub fn success() -> Self {
127        Self {
128            success: true,
129            identity: None,
130            issuer: None,
131            integrated_time: None,
132            warnings: Vec::new(),
133        }
134    }
135
136    /// Create a failed result
137    pub fn failure() -> Self {
138        Self {
139            success: false,
140            identity: None,
141            issuer: None,
142            integrated_time: None,
143            warnings: Vec::new(),
144        }
145    }
146}
147
148/// A verifier for Sigstore signatures
149pub struct Verifier {
150    /// Trusted root containing verification material
151    trusted_root: TrustedRoot,
152}
153
154impl Verifier {
155    /// Create a new verifier with a trusted root
156    ///
157    /// The trusted root is required and contains all cryptographic material
158    /// needed for verification (Fulcio CA certs, Rekor keys, TSA certs, etc.)
159    pub fn new(trusted_root: &TrustedRoot) -> Self {
160        Self {
161            trusted_root: trusted_root.clone(),
162        }
163    }
164
165    /// Verify an artifact against a bundle
166    ///
167    /// The artifact can be provided as raw bytes or as a pre-computed SHA-256 digest.
168    /// When using a pre-computed digest, the raw bytes are not needed, which is useful
169    /// for large files or when the digest is already known (e.g., from a registry).
170    ///
171    /// # Example
172    ///
173    /// ```no_run
174    /// use sigstore_verify::{Verifier, VerificationPolicy};
175    /// use sigstore_trust_root::TrustedRoot;
176    /// use sigstore_types::{Artifact, Bundle, Sha256Hash};
177    ///
178    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
179    /// let trusted_root = TrustedRoot::production()?;
180    /// let verifier = Verifier::new(&trusted_root);
181    /// let bundle: Bundle = todo!();
182    /// let policy = VerificationPolicy::default();
183    ///
184    /// // Option 1: Verify with raw bytes
185    /// let artifact_bytes = b"hello world";
186    /// verifier.verify(artifact_bytes.as_slice(), &bundle, &policy)?;
187    ///
188    /// // Option 2: Verify with pre-computed digest (no raw bytes needed!)
189    /// let digest = Sha256Hash::from_hex("b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9")?;
190    /// verifier.verify(digest, &bundle, &policy)?;
191    /// # Ok(())
192    /// # }
193    /// ```
194    ///
195    /// In order to verify an artifact, we need to achieve the following:
196    ///
197    /// 0. Establish a time for the signature.
198    /// 1. Verify that the signing certificate chains to the root of trust
199    ///    and is valid at the time of signing.
200    /// 2. Verify the signing certificate's SCT.
201    /// 3. Verify that the signing certificate conforms to the Sigstore
202    ///    X.509 profile as well as the passed-in `VerificationPolicy`.
203    /// 4. Verify the inclusion proof and signed checkpoint for the log
204    ///    entry.
205    /// 5. Verify the inclusion promise for the log entry, if present.
206    /// 6. Verify the timely insertion of the log entry against the validity
207    ///    period for the signing certificate.
208    /// 7. Verify the signature and input against the signing certificate's
209    ///    public key.
210    /// 8. Verify the transparency log entry's consistency against the other
211    ///    materials, to prevent variants of CVE-2022-36056.
212    pub fn verify<'a>(
213        &self,
214        artifact: impl Into<Artifact<'a>>,
215        bundle: &Bundle,
216        policy: &VerificationPolicy,
217    ) -> Result<VerificationResult> {
218        let artifact = artifact.into();
219        let mut result = VerificationResult::success();
220
221        // Validate bundle structure first
222        let options = ValidationOptions {
223            require_inclusion_proof: policy.verify_tlog,
224            require_timestamp: false, // Don't require timestamps, but verify if present
225        };
226        validate_bundle_with_options(bundle, &options)
227            .map_err(|e| Error::Verification(format!("bundle validation failed: {}", e)))?;
228
229        // Extract certificate for verification
230        let cert = crate::verify_impl::helpers::extract_certificate(
231            &bundle.verification_material.content,
232        )?;
233        let cert_info = parse_certificate_info(cert.as_bytes())
234            .map_err(|e| Error::Verification(format!("failed to parse certificate: {}", e)))?;
235
236        // Store identity and issuer in result
237        result.identity = cert_info.identity.clone();
238        result.issuer = cert_info.issuer.clone();
239
240        // (0): Establish a time for the signature
241        // First, establish verified times for the signature. This is required to
242        // validate the certificate chain, so this step comes first.
243        // These include TSA timestamps and (in the case of rekor v1 entries)
244        // rekor log integrated time.
245        let signature = crate::verify_impl::helpers::extract_signature(&bundle.content)?;
246        let validation_time = crate::verify_impl::helpers::determine_validation_time(
247            bundle,
248            &signature,
249            &self.trusted_root,
250        )?;
251
252        // (1): Verify that the signing certificate chains to the root of trust,
253        //      is valid at the time of signing, and has CODE_SIGNING EKU.
254        if policy.verify_certificate {
255            crate::verify_impl::helpers::verify_certificate_chain(
256                &bundle.verification_material.content,
257                validation_time,
258                &self.trusted_root,
259            )?;
260
261            // Also verify the certificate is within its validity period
262            crate::verify_impl::helpers::validate_certificate_time(validation_time, &cert_info)?;
263
264            // (2): Verify the signing certificate's SCT.
265            crate::verify_impl::helpers::verify_sct(
266                &bundle.verification_material.content,
267                &self.trusted_root,
268            )?;
269        }
270
271        // (3): Verify against the given `VerificationPolicy`.
272
273        // Verify against policy constraints
274        if let Some(ref expected_identity) = policy.identity {
275            match &result.identity {
276                Some(actual_identity) if actual_identity == expected_identity => {}
277                Some(actual_identity) => {
278                    return Err(Error::Verification(format!(
279                        "identity mismatch: expected {}, got {}",
280                        expected_identity, actual_identity
281                    )));
282                }
283                None => {
284                    return Err(Error::Verification(format!(
285                        "certificate is missing identity (SAN), but policy requires: {}",
286                        expected_identity
287                    )));
288                }
289            }
290        }
291
292        if let Some(ref expected_issuer) = policy.issuer {
293            match &result.issuer {
294                Some(actual_issuer) if actual_issuer == expected_issuer => {}
295                Some(actual_issuer) => {
296                    return Err(Error::Verification(format!(
297                        "issuer mismatch: expected {}, got {}",
298                        expected_issuer, actual_issuer
299                    )));
300                }
301                None => {
302                    return Err(Error::Verification(format!(
303                        "certificate is missing issuer (Fulcio OID extension), but policy requires: {}",
304                        expected_issuer
305                    )));
306                }
307            }
308        }
309
310        // (4): Verify the inclusion proof and signed checkpoint for the log entry.
311        // (5): Verify the inclusion promise for the log entry, if present.
312        // (6): Verify the timely insertion of the log entry against the validity
313        //      period for the signing certificate.
314        if policy.verify_tlog {
315            let integrated_time = crate::verify_impl::tlog::verify_tlog_entries(
316                bundle,
317                &self.trusted_root,
318                cert_info.not_before,
319                cert_info.not_after,
320                policy.clock_skew_seconds,
321            )?;
322
323            if let Some(time) = integrated_time {
324                result.integrated_time = Some(time);
325            }
326        }
327
328        // (7): Verify the signature and input against the signing certificate's
329        //      public key.
330        // For DSSE envelopes, verify using PAE (Pre-Authentication Encoding)
331        if let SignatureContent::DsseEnvelope(envelope) = &bundle.content {
332            let payload_bytes = envelope.decode_payload();
333
334            // Compute the PAE that was signed
335            let pae = sigstore_types::pae(&envelope.payload_type, &payload_bytes);
336
337            // Verify at least one signature is cryptographically valid
338            let mut any_sig_valid = false;
339            for sig in &envelope.signatures {
340                if sigstore_crypto::verify_signature(
341                    &cert_info.public_key,
342                    &pae,
343                    &sig.sig,
344                    cert_info.signing_scheme,
345                )
346                .is_ok()
347                {
348                    any_sig_valid = true;
349                    break;
350                }
351            }
352
353            if !any_sig_valid {
354                return Err(Error::Verification(
355                    "DSSE signature verification failed: no valid signatures found".to_string(),
356                ));
357            }
358
359            // Verify artifact hash matches (for DSSE with in-toto statements)
360            if envelope.payload_type == "application/vnd.in-toto+json" {
361                let payload_bytes = envelope.payload.as_bytes();
362
363                let artifact_hash = compute_artifact_digest(&artifact);
364                let artifact_hash_hex = artifact_hash.to_hex();
365
366                let payload_str = std::str::from_utf8(payload_bytes).map_err(|e| {
367                    Error::Verification(format!("payload is not valid UTF-8: {}", e))
368                })?;
369
370                let statement: Statement = serde_json::from_str(payload_str).map_err(|e| {
371                    Error::Verification(format!("failed to parse in-toto statement: {}", e))
372                })?;
373
374                if !statement.subject.is_empty() && !statement.matches_sha256(&artifact_hash_hex) {
375                    return Err(Error::Verification(
376                        "artifact hash does not match any subject in attestation".to_string(),
377                    ));
378                }
379            }
380        }
381
382        // For MessageSignature bundles, verify the messageDigest matches the artifact
383        if let SignatureContent::MessageSignature(msg_sig) = &bundle.content {
384            if let Some(ref digest) = msg_sig.message_digest {
385                let artifact_hash = compute_artifact_digest(&artifact);
386
387                // Compare the digest in the bundle with the computed artifact hash
388                if digest.digest.as_bytes() != artifact_hash.as_bytes() {
389                    return Err(Error::Verification(
390                        "message digest in bundle does not match artifact hash".to_string(),
391                    ));
392                }
393            }
394        }
395        // Note: For hashedrekord (MessageSignature), the signature verification
396        // is performed in step (8) by verify_hashedrekord_entries, which properly
397        // handles prehashed signatures.
398
399        // (8): Verify the transparency log entry's consistency against the other
400        //      materials, to prevent variants of CVE-2022-36056.
401        crate::verify_impl::verify_dsse_entries(bundle)?;
402        crate::verify_impl::verify_intoto_entries(bundle)?;
403        crate::verify_impl::verify_hashedrekord_entries(bundle, &artifact)?;
404
405        Ok(result)
406    }
407}
408
409/// Compute the SHA-256 digest from an artifact
410fn compute_artifact_digest(artifact: &Artifact<'_>) -> Sha256Hash {
411    match artifact {
412        Artifact::Bytes(bytes) => sigstore_crypto::sha256(bytes),
413        Artifact::Digest(hash) => *hash,
414    }
415}
416
417/// Convenience function to verify an artifact against a bundle
418///
419/// This uses the trusted root for all cryptographic material
420/// (Rekor keys, Fulcio certs, TSA certs).
421///
422/// The artifact can be provided as raw bytes or as a pre-computed SHA-256 digest:
423/// - `verify(artifact_bytes, ...)` - pass raw bytes
424/// - `verify(Sha256Hash::from_hex("...")?, ...)` - pass pre-computed digest
425pub fn verify<'a>(
426    artifact: impl Into<Artifact<'a>>,
427    bundle: &Bundle,
428    policy: &VerificationPolicy,
429    trusted_root: &TrustedRoot,
430) -> Result<VerificationResult> {
431    let verifier = Verifier::new(trusted_root);
432    verifier.verify(artifact, bundle, policy)
433}
434
435/// Verify an artifact against a bundle using a provided public key
436///
437/// This is used for managed key verification where the bundle contains a public key
438/// hint instead of a certificate. The actual public key is provided separately.
439///
440/// This verification:
441/// - Verifies the signature using the provided public key
442/// - Verifies transparency log entries (checkpoints, SETs)
443/// - Skips certificate chain verification (no certificate present)
444/// - Skips identity/issuer verification
445///
446/// # Example
447///
448/// ```no_run
449/// use sigstore_verify::verify_with_key;
450/// use sigstore_trust_root::TrustedRoot;
451/// use sigstore_types::{Bundle, DerPublicKey};
452///
453/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
454/// let trusted_root = TrustedRoot::from_file("trusted_root.json")?;
455/// let bundle_json = std::fs::read_to_string("artifact.sigstore.json")?;
456/// let bundle = Bundle::from_json(&bundle_json)?;
457/// let artifact = std::fs::read("artifact.txt")?;
458/// let key_pem = std::fs::read_to_string("key.pub")?;
459/// let public_key = DerPublicKey::from_pem(&key_pem)?;
460///
461/// let result = verify_with_key(&artifact, &bundle, &public_key, &trusted_root)?;
462/// assert!(result.success);
463/// # Ok(())
464/// # }
465/// ```
466pub fn verify_with_key<'a>(
467    artifact: impl Into<Artifact<'a>>,
468    bundle: &Bundle,
469    public_key: &sigstore_types::DerPublicKey,
470    trusted_root: &TrustedRoot,
471) -> Result<VerificationResult> {
472    use sigstore_bundle::{validate_bundle_with_options, ValidationOptions};
473    use sigstore_crypto::{detect_key_type, KeyType, SigningScheme};
474
475    let artifact = artifact.into();
476    let result = VerificationResult::success();
477
478    // Validate bundle structure
479    let options = ValidationOptions {
480        require_inclusion_proof: true,
481        require_timestamp: false,
482    };
483    validate_bundle_with_options(bundle, &options)
484        .map_err(|e| Error::Verification(format!("bundle validation failed: {}", e)))?;
485
486    // Determine signing scheme from public key
487    let signing_scheme = match detect_key_type(public_key) {
488        KeyType::Ed25519 => SigningScheme::Ed25519,
489        KeyType::EcdsaP256 => SigningScheme::EcdsaP256Sha256,
490        KeyType::Unknown => {
491            return Err(Error::Verification(
492                "unsupported or unrecognized public key type".to_string(),
493            ));
494        }
495    };
496
497    // Verify transparency log entries (checkpoints, SETs) without certificate time validation
498    for entry in &bundle.verification_material.tlog_entries {
499        // Verify checkpoint signature if present
500        if let Some(ref inclusion_proof) = entry.inclusion_proof {
501            crate::verify_impl::tlog::verify_checkpoint(
502                &inclusion_proof.checkpoint.envelope,
503                inclusion_proof,
504                trusted_root,
505            )?;
506        }
507
508        // Verify inclusion promise (SET) if present
509        if entry.inclusion_promise.is_some() {
510            crate::verify_impl::tlog::verify_set(entry, trusted_root)?;
511        }
512    }
513
514    // Verify the signature
515    match &bundle.content {
516        SignatureContent::MessageSignature(msg_sig) => {
517            // Verify message digest matches artifact
518            if let Some(ref digest) = msg_sig.message_digest {
519                let artifact_hash = compute_artifact_digest(&artifact);
520                if digest.digest.as_bytes() != artifact_hash.as_bytes() {
521                    return Err(Error::Verification(
522                        "message digest in bundle does not match artifact hash".to_string(),
523                    ));
524                }
525            }
526
527            // Verify signature over the artifact
528            let artifact_hash = compute_artifact_digest(&artifact);
529
530            // Use prehashed verification if supported
531            if signing_scheme.uses_sha256() && signing_scheme.supports_prehashed() {
532                sigstore_crypto::verify_signature_prehashed(
533                    public_key,
534                    &artifact_hash,
535                    &msg_sig.signature,
536                    signing_scheme,
537                )
538                .map_err(|e| {
539                    Error::Verification(format!("signature verification failed: {}", e))
540                })?;
541            } else {
542                // For non-prehashed schemes, we need the original bytes
543                match &artifact {
544                    Artifact::Bytes(bytes) => {
545                        sigstore_crypto::verify_signature(
546                            public_key,
547                            bytes,
548                            &msg_sig.signature,
549                            signing_scheme,
550                        )
551                        .map_err(|e| {
552                            Error::Verification(format!("signature verification failed: {}", e))
553                        })?;
554                    }
555                    Artifact::Digest(_) => {
556                        return Err(Error::Verification(
557                            "cannot verify signature with digest-only for this key type"
558                                .to_string(),
559                        ));
560                    }
561                }
562            }
563        }
564        SignatureContent::DsseEnvelope(envelope) => {
565            let payload_bytes = envelope.decode_payload();
566            let pae = sigstore_types::pae(&envelope.payload_type, &payload_bytes);
567
568            // Verify at least one signature is valid
569            let mut any_sig_valid = false;
570            for sig in &envelope.signatures {
571                if sigstore_crypto::verify_signature(public_key, &pae, &sig.sig, signing_scheme)
572                    .is_ok()
573                {
574                    any_sig_valid = true;
575                    break;
576                }
577            }
578
579            if !any_sig_valid {
580                return Err(Error::Verification(
581                    "DSSE signature verification failed: no valid signatures found".to_string(),
582                ));
583            }
584
585            // Verify artifact hash matches for in-toto statements
586            if envelope.payload_type == "application/vnd.in-toto+json" {
587                let artifact_hash = compute_artifact_digest(&artifact);
588                let artifact_hash_hex = artifact_hash.to_hex();
589
590                let payload_str = std::str::from_utf8(&payload_bytes).map_err(|e| {
591                    Error::Verification(format!("payload is not valid UTF-8: {}", e))
592                })?;
593
594                let statement: Statement = serde_json::from_str(payload_str).map_err(|e| {
595                    Error::Verification(format!("failed to parse in-toto statement: {}", e))
596                })?;
597
598                if !statement.subject.is_empty() && !statement.matches_sha256(&artifact_hash_hex) {
599                    return Err(Error::Verification(
600                        "artifact hash does not match any subject in attestation".to_string(),
601                    ));
602                }
603            }
604        }
605    }
606
607    Ok(result)
608}
609
610#[cfg(test)]
611mod tests {
612    use super::*;
613
614    #[test]
615    fn test_verification_policy_default() {
616        let policy = VerificationPolicy::default();
617        assert!(policy.verify_tlog);
618        assert!(policy.verify_timestamp);
619        assert!(policy.verify_certificate);
620    }
621
622    #[test]
623    fn test_verification_policy_builder() {
624        let policy = VerificationPolicy::default()
625            .require_identity("test@example.com")
626            .require_issuer("https://accounts.google.com")
627            .skip_tlog();
628
629        assert_eq!(policy.identity, Some("test@example.com".to_string()));
630        assert_eq!(
631            policy.issuer,
632            Some("https://accounts.google.com".to_string())
633        );
634        assert!(!policy.verify_tlog);
635    }
636}