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}