use crate::error::{Error, Result};
use sigstore_bundle::validate_bundle_with_options;
use sigstore_bundle::ValidationOptions;
use sigstore_crypto::{parse_certificate_info, SigningScheme};
use sigstore_trust_root::TrustedRoot;
use sigstore_types::{Artifact, Bundle, HashAlgorithm, 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.key_algorithm.default_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(),
));
}
verify_dsse_artifact_binding(envelope, &artifact)?;
}
if let SignatureContent::MessageSignature(msg_sig) = &bundle.content {
if let Some(ref digest) = msg_sig.message_digest {
let artifact_hash = compute_artifact_digest_algo(&artifact, digest.algorithm)?;
if digest.digest != artifact_hash {
return Err(Error::Verification(
"message digest in bundle does not match artifact hash".to_string(),
));
}
}
verify_message_signature_crypto(&cert_info, msg_sig, &artifact)?;
}
if policy.verify_tlog {
crate::verify_impl::verify_tlog_consistency(bundle, &artifact)?;
}
Ok(result)
}
}
fn compute_artifact_digest_algo(artifact: &Artifact<'_>, algo: HashAlgorithm) -> Result<Vec<u8>> {
match artifact {
Artifact::Bytes(bytes) => match algo {
HashAlgorithm::Sha2256 => Ok(sigstore_crypto::sha256(bytes).as_bytes().to_vec()),
HashAlgorithm::Sha2384 => Ok(sigstore_crypto::sha384(bytes)),
HashAlgorithm::Sha2512 => Ok(sigstore_crypto::sha512(bytes)),
},
Artifact::Digest(hash) => {
let expected_len = algo.digest_size();
if hash.len() != expected_len {
return Err(Error::Verification(format!(
"expected digest length {} for {:?}, got {}",
expected_len,
algo,
hash.len()
)));
}
Ok(hash.to_vec())
}
}
}
fn verify_dsse_artifact_binding(
envelope: &sigstore_types::DsseEnvelope,
artifact: &Artifact<'_>,
) -> Result<()> {
if envelope.payload_type != "application/vnd.in-toto+json" {
return Err(Error::Verification(format!(
"unsupported DSSE payload type {:?}: cannot bind artifact to attestation",
envelope.payload_type
)));
}
let artifact_hash = compute_artifact_digest_algo(artifact, HashAlgorithm::Sha2256)?;
let artifact_hash_hex = sigstore_types::Sha256Hash::try_from_slice(&artifact_hash)
.map_err(|_| Error::Verification("invalid SHA-256 hash length".to_string()))?
.to_hex();
let payload_str = std::str::from_utf8(envelope.payload.as_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() {
return Err(Error::Verification(
"in-toto statement has no subjects: cannot bind artifact to attestation".to_string(),
));
}
if !statement.matches_sha256(&artifact_hash_hex) {
return Err(Error::Verification(
"artifact hash does not match any subject in attestation".to_string(),
));
}
Ok(())
}
fn verify_signature_over_artifact(
public_key: &sigstore_types::DerPublicKey,
scheme: SigningScheme,
signature: &sigstore_types::SignatureBytes,
artifact: &Artifact<'_>,
) -> Result<()> {
let result = match artifact {
Artifact::Bytes(bytes) => {
sigstore_crypto::verify_signature(public_key, bytes, signature, scheme)
}
Artifact::Digest(hash) => {
if !scheme.supports_prehashed() {
return Err(Error::Verification(format!(
"cannot verify signature with digest-only - scheme {} does not support prehashed mode",
scheme.name()
)));
}
sigstore_crypto::verify_signature_prehashed(public_key, hash, signature, scheme)
}
};
result.map_err(|e| Error::Verification(format!("signature verification failed: {}", e)))
}
fn verify_message_signature_crypto(
cert_info: &sigstore_crypto::CertificateInfo,
msg_sig: &sigstore_types::bundle::MessageSignature,
artifact: &Artifact<'_>,
) -> Result<()> {
let scheme = match &msg_sig.message_digest {
Some(digest) => cert_info
.key_algorithm
.resolve_signing_scheme(digest.algorithm)?,
None => cert_info.key_algorithm.default_signing_scheme(),
};
verify_signature_over_artifact(&cert_info.public_key, scheme, &msg_sig.signature, artifact)
}
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 {
crate::verify_impl::tlog::verify_entry_inclusion(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_algo(&artifact, digest.algorithm)?;
if digest.digest != artifact_hash {
return Err(Error::Verification(
"message digest in bundle does not match artifact hash".to_string(),
));
}
}
verify_signature_over_artifact(
public_key,
signing_scheme,
&msg_sig.signature,
&artifact,
)?;
}
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(),
));
}
verify_dsse_artifact_binding(envelope, &artifact)?;
}
}
crate::verify_impl::verify_tlog_consistency(bundle, &artifact)?;
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);
}
fn in_toto_envelope(payload: &str) -> sigstore_types::DsseEnvelope {
sigstore_types::DsseEnvelope::new(
"application/vnd.in-toto+json".to_string(),
sigstore_types::PayloadBytes::from_bytes(payload.as_bytes()),
vec![],
)
}
fn statement_with_subject_sha256(hash_hex: &str) -> String {
format!(
r#"{{"_type":"https://in-toto.io/Statement/v1","subject":[{{"name":"artifact","digest":{{"sha256":"{}"}}}}],"predicateType":"https://example.com/predicate/v1","predicate":{{}}}}"#,
hash_hex
)
}
#[test]
fn test_dsse_binding_matching_subject_ok() {
let artifact_bytes = b"hello world";
let hash_hex = sigstore_crypto::sha256(artifact_bytes).to_hex();
let envelope = in_toto_envelope(&statement_with_subject_sha256(&hash_hex));
let artifact = Artifact::from(artifact_bytes.as_slice());
assert!(verify_dsse_artifact_binding(&envelope, &artifact).is_ok());
}
#[test]
fn test_dsse_binding_mismatched_subject_fails() {
let hash_hex = sigstore_crypto::sha256(b"some other artifact").to_hex();
let envelope = in_toto_envelope(&statement_with_subject_sha256(&hash_hex));
let artifact = Artifact::from(b"hello world".as_slice());
let err = verify_dsse_artifact_binding(&envelope, &artifact).unwrap_err();
assert!(err
.to_string()
.contains("does not match any subject in attestation"));
}
#[test]
fn test_dsse_binding_empty_subjects_fails_closed() {
let payload = r#"{"_type":"https://in-toto.io/Statement/v1","subject":[],"predicateType":"https://example.com/predicate/v1","predicate":{}}"#;
let envelope = in_toto_envelope(payload);
let artifact = Artifact::from(b"hello world".as_slice());
let err = verify_dsse_artifact_binding(&envelope, &artifact).unwrap_err();
assert!(err.to_string().contains("no subjects"));
}
#[test]
fn test_dsse_binding_unknown_payload_type_fails_closed() {
let envelope = sigstore_types::DsseEnvelope::new(
"application/vnd.example+json".to_string(),
sigstore_types::PayloadBytes::from_bytes(b"{}"),
vec![],
);
let artifact = Artifact::from(b"hello world".as_slice());
let err = verify_dsse_artifact_binding(&envelope, &artifact).unwrap_err();
assert!(err.to_string().contains("unsupported DSSE payload type"));
}
}