use crate::error::{Error, Result};
use sigstore_bundle::validate_bundle_with_options;
use sigstore_bundle::ValidationOptions;
use sigstore_crypto::parse_certificate_info;
use sigstore_trust_root::TrustedRoot;
use sigstore_types::{Artifact, Bundle, Sha256Hash, SignatureContent, Statement};
pub const DEFAULT_CLOCK_SKEW_SECONDS: i64 = 60;
#[derive(Debug, Clone)]
pub struct VerificationPolicy {
pub identity: Option<String>,
pub issuer: Option<String>,
pub verify_tlog: bool,
pub verify_timestamp: bool,
pub verify_certificate: bool,
pub verify_sct: bool,
pub clock_skew_seconds: i64,
}
impl Default for VerificationPolicy {
fn default() -> Self {
Self {
identity: None,
issuer: None,
verify_tlog: true,
verify_timestamp: true,
verify_certificate: true,
verify_sct: true,
clock_skew_seconds: DEFAULT_CLOCK_SKEW_SECONDS,
}
}
}
impl VerificationPolicy {
pub fn with_identity(identity: impl Into<String>) -> Self {
Self {
identity: Some(identity.into()),
..Default::default()
}
}
pub fn with_issuer(issuer: impl Into<String>) -> Self {
Self {
issuer: Some(issuer.into()),
..Default::default()
}
}
pub fn require_identity(mut self, identity: impl Into<String>) -> Self {
self.identity = Some(identity.into());
self
}
pub fn require_issuer(mut self, issuer: impl Into<String>) -> Self {
self.issuer = Some(issuer.into());
self
}
pub fn skip_tlog(mut self) -> Self {
self.verify_tlog = false;
self
}
pub fn skip_timestamp(mut self) -> Self {
self.verify_timestamp = false;
self
}
pub fn skip_certificate_chain(mut self) -> Self {
self.verify_certificate = false;
self.verify_sct = false;
self
}
pub fn skip_sct(mut self) -> Self {
self.verify_sct = false;
self
}
pub fn with_clock_skew_seconds(mut self, seconds: i64) -> Self {
self.clock_skew_seconds = seconds;
self
}
}
#[derive(Debug)]
pub struct VerificationResult {
pub success: bool,
pub identity: Option<String>,
pub issuer: Option<String>,
pub integrated_time: Option<i64>,
pub warnings: Vec<String>,
}
impl VerificationResult {
pub fn success() -> Self {
Self {
success: true,
identity: None,
issuer: None,
integrated_time: None,
warnings: Vec::new(),
}
}
pub fn failure() -> Self {
Self {
success: false,
identity: None,
issuer: None,
integrated_time: None,
warnings: Vec::new(),
}
}
}
pub struct Verifier {
trusted_root: TrustedRoot,
}
impl Verifier {
pub fn new(trusted_root: &TrustedRoot) -> Self {
Self {
trusted_root: trusted_root.clone(),
}
}
pub fn verify<'a>(
&self,
artifact: impl Into<Artifact<'a>>,
bundle: &Bundle,
policy: &VerificationPolicy,
) -> Result<VerificationResult> {
let artifact = artifact.into();
let mut result = VerificationResult::success();
let options = ValidationOptions {
require_inclusion_proof: policy.verify_tlog,
require_timestamp: false, };
validate_bundle_with_options(bundle, &options)
.map_err(|e| Error::Verification(format!("bundle validation failed: {}", e)))?;
let cert = crate::verify_impl::helpers::extract_certificate(
&bundle.verification_material.content,
)?;
let cert_info = parse_certificate_info(cert.as_bytes())
.map_err(|e| Error::Verification(format!("failed to parse certificate: {}", e)))?;
result.identity = cert_info.identity.clone();
result.issuer = cert_info.issuer.clone();
let signature = crate::verify_impl::helpers::extract_signature(&bundle.content)?;
let validation_time = crate::verify_impl::helpers::determine_validation_time(
bundle,
&signature,
&self.trusted_root,
)?;
if policy.verify_certificate {
crate::verify_impl::helpers::verify_certificate_chain(
&bundle.verification_material.content,
validation_time,
&self.trusted_root,
)?;
crate::verify_impl::helpers::validate_certificate_time(validation_time, &cert_info)?;
}
if policy.verify_sct {
crate::verify_impl::helpers::verify_sct(
&bundle.verification_material.content,
&self.trusted_root,
)?;
}
if let Some(ref expected_identity) = policy.identity {
match &result.identity {
Some(actual_identity) if actual_identity == expected_identity => {}
Some(actual_identity) => {
return Err(Error::Verification(format!(
"identity mismatch: expected {}, got {}",
expected_identity, actual_identity
)));
}
None => {
return Err(Error::Verification(format!(
"certificate is missing identity (SAN), but policy requires: {}",
expected_identity
)));
}
}
}
if let Some(ref expected_issuer) = policy.issuer {
match &result.issuer {
Some(actual_issuer) if actual_issuer == expected_issuer => {}
Some(actual_issuer) => {
return Err(Error::Verification(format!(
"issuer mismatch: expected {}, got {}",
expected_issuer, actual_issuer
)));
}
None => {
return Err(Error::Verification(format!(
"certificate is missing issuer (Fulcio OID extension), but policy requires: {}",
expected_issuer
)));
}
}
}
if policy.verify_tlog {
let integrated_time = crate::verify_impl::tlog::verify_tlog_entries(
bundle,
&self.trusted_root,
cert_info.not_before,
cert_info.not_after,
policy.clock_skew_seconds,
)?;
if let Some(time) = integrated_time {
result.integrated_time = Some(time);
}
}
if let SignatureContent::DsseEnvelope(envelope) = &bundle.content {
let payload_bytes = envelope.decode_payload();
let pae = sigstore_types::pae(&envelope.payload_type, &payload_bytes);
let mut any_sig_valid = false;
for sig in &envelope.signatures {
if sigstore_crypto::verify_signature(
&cert_info.public_key,
&pae,
&sig.sig,
cert_info.signing_scheme,
)
.is_ok()
{
any_sig_valid = true;
break;
}
}
if !any_sig_valid {
return Err(Error::Verification(
"DSSE signature verification failed: no valid signatures found".to_string(),
));
}
if envelope.payload_type == "application/vnd.in-toto+json" {
let payload_bytes = envelope.payload.as_bytes();
let artifact_hash = compute_artifact_digest(&artifact);
let artifact_hash_hex = artifact_hash.to_hex();
let payload_str = std::str::from_utf8(payload_bytes).map_err(|e| {
Error::Verification(format!("payload is not valid UTF-8: {}", e))
})?;
let statement: Statement = serde_json::from_str(payload_str).map_err(|e| {
Error::Verification(format!("failed to parse in-toto statement: {}", e))
})?;
if !statement.subject.is_empty() && !statement.matches_sha256(&artifact_hash_hex) {
return Err(Error::Verification(
"artifact hash does not match any subject in attestation".to_string(),
));
}
}
}
if let SignatureContent::MessageSignature(msg_sig) = &bundle.content {
if let Some(ref digest) = msg_sig.message_digest {
let artifact_hash = compute_artifact_digest(&artifact);
if digest.digest.as_bytes() != artifact_hash.as_bytes() {
return Err(Error::Verification(
"message digest in bundle does not match artifact hash".to_string(),
));
}
}
}
crate::verify_impl::verify_dsse_entries(bundle)?;
crate::verify_impl::verify_intoto_entries(bundle)?;
crate::verify_impl::verify_hashedrekord_entries(bundle, &artifact)?;
Ok(result)
}
}
fn compute_artifact_digest(artifact: &Artifact<'_>) -> Sha256Hash {
match artifact {
Artifact::Bytes(bytes) => sigstore_crypto::sha256(bytes),
Artifact::Digest(hash) => *hash,
}
}
pub fn verify<'a>(
artifact: impl Into<Artifact<'a>>,
bundle: &Bundle,
policy: &VerificationPolicy,
trusted_root: &TrustedRoot,
) -> Result<VerificationResult> {
let verifier = Verifier::new(trusted_root);
verifier.verify(artifact, bundle, policy)
}
pub fn verify_with_key<'a>(
artifact: impl Into<Artifact<'a>>,
bundle: &Bundle,
public_key: &sigstore_types::DerPublicKey,
trusted_root: &TrustedRoot,
) -> Result<VerificationResult> {
use sigstore_bundle::{validate_bundle_with_options, ValidationOptions};
use sigstore_crypto::{detect_key_type, KeyType, SigningScheme};
let artifact = artifact.into();
let result = VerificationResult::success();
let options = ValidationOptions {
require_inclusion_proof: true,
require_timestamp: false,
};
validate_bundle_with_options(bundle, &options)
.map_err(|e| Error::Verification(format!("bundle validation failed: {}", e)))?;
let signing_scheme = match detect_key_type(public_key) {
KeyType::Ed25519 => SigningScheme::Ed25519,
KeyType::EcdsaP256 => SigningScheme::EcdsaP256Sha256,
KeyType::Unknown => {
return Err(Error::Verification(
"unsupported or unrecognized public key type".to_string(),
));
}
};
for entry in &bundle.verification_material.tlog_entries {
if let Some(ref inclusion_proof) = entry.inclusion_proof {
crate::verify_impl::tlog::verify_checkpoint(
&inclusion_proof.checkpoint.envelope,
inclusion_proof,
trusted_root,
)?;
}
if entry.inclusion_promise.is_some() {
crate::verify_impl::tlog::verify_set(entry, trusted_root)?;
}
}
match &bundle.content {
SignatureContent::MessageSignature(msg_sig) => {
if let Some(ref digest) = msg_sig.message_digest {
let artifact_hash = compute_artifact_digest(&artifact);
if digest.digest.as_bytes() != artifact_hash.as_bytes() {
return Err(Error::Verification(
"message digest in bundle does not match artifact hash".to_string(),
));
}
}
let artifact_hash = compute_artifact_digest(&artifact);
if signing_scheme.uses_sha256() && signing_scheme.supports_prehashed() {
sigstore_crypto::verify_signature_prehashed(
public_key,
&artifact_hash,
&msg_sig.signature,
signing_scheme,
)
.map_err(|e| {
Error::Verification(format!("signature verification failed: {}", e))
})?;
} else {
match &artifact {
Artifact::Bytes(bytes) => {
sigstore_crypto::verify_signature(
public_key,
bytes,
&msg_sig.signature,
signing_scheme,
)
.map_err(|e| {
Error::Verification(format!("signature verification failed: {}", e))
})?;
}
Artifact::Digest(_) => {
return Err(Error::Verification(
"cannot verify signature with digest-only for this key type"
.to_string(),
));
}
}
}
}
SignatureContent::DsseEnvelope(envelope) => {
let payload_bytes = envelope.decode_payload();
let pae = sigstore_types::pae(&envelope.payload_type, &payload_bytes);
let mut any_sig_valid = false;
for sig in &envelope.signatures {
if sigstore_crypto::verify_signature(public_key, &pae, &sig.sig, signing_scheme)
.is_ok()
{
any_sig_valid = true;
break;
}
}
if !any_sig_valid {
return Err(Error::Verification(
"DSSE signature verification failed: no valid signatures found".to_string(),
));
}
if envelope.payload_type == "application/vnd.in-toto+json" {
let artifact_hash = compute_artifact_digest(&artifact);
let artifact_hash_hex = artifact_hash.to_hex();
let payload_str = std::str::from_utf8(&payload_bytes).map_err(|e| {
Error::Verification(format!("payload is not valid UTF-8: {}", e))
})?;
let statement: Statement = serde_json::from_str(payload_str).map_err(|e| {
Error::Verification(format!("failed to parse in-toto statement: {}", e))
})?;
if !statement.subject.is_empty() && !statement.matches_sha256(&artifact_hash_hex) {
return Err(Error::Verification(
"artifact hash does not match any subject in attestation".to_string(),
));
}
}
}
}
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_verification_policy_default() {
let policy = VerificationPolicy::default();
assert!(policy.verify_tlog);
assert!(policy.verify_timestamp);
assert!(policy.verify_certificate);
assert!(policy.verify_sct);
}
#[test]
fn test_verification_policy_builder() {
let policy = VerificationPolicy::default()
.require_identity("test@example.com")
.require_issuer("https://accounts.google.com")
.skip_tlog();
assert_eq!(policy.identity, Some("test@example.com".to_string()));
assert_eq!(
policy.issuer,
Some("https://accounts.google.com".to_string())
);
assert!(!policy.verify_tlog);
}
#[test]
fn test_skip_sct_keeps_certificate_chain_verification() {
let policy = VerificationPolicy::default().skip_sct();
assert!(policy.verify_certificate);
assert!(!policy.verify_sct);
}
#[test]
fn test_skip_certificate_chain_preserves_legacy_sct_skip() {
let policy = VerificationPolicy::default().skip_certificate_chain();
assert!(!policy.verify_certificate);
assert!(!policy.verify_sct);
}
}