use crate::error::{Error, Result};
use base64::Engine;
use serde::Serialize;
use sigstore_crypto::{verify_signature_auto, Checkpoint};
use sigstore_trust_root::TrustedRoot;
use sigstore_types::bundle::InclusionProof;
use sigstore_types::{Bundle, SignatureBytes, TransparencyLogEntry};
pub fn verify_tlog_entries(
bundle: &Bundle,
trusted_root: &TrustedRoot,
not_before: i64,
not_after: i64,
clock_skew_seconds: i64,
) -> Result<Option<i64>> {
let mut integrated_time_result: Option<i64> = None;
for entry in &bundle.verification_material.tlog_entries {
verify_entry_inclusion(entry, trusted_root)?;
let time = entry.integrated_time;
if time > 0 {
let now = jiff::Timestamp::now().as_second();
if time > now + clock_skew_seconds {
return Err(Error::Verification(format!(
"integrated time {} is in the future (current time: {}, tolerance: {}s)",
time, now, clock_skew_seconds
)));
}
if time < not_before {
return Err(Error::Verification(format!(
"integrated time {} is before certificate validity (not_before: {})",
time, not_before
)));
}
if time > not_after {
return Err(Error::Verification(format!(
"integrated time {} is after certificate validity (not_after: {})",
time, not_after
)));
}
integrated_time_result = Some(time);
}
}
Ok(integrated_time_result)
}
pub fn verify_entry_inclusion(
entry: &TransparencyLogEntry,
trusted_root: &TrustedRoot,
) -> Result<()> {
if let Some(ref inclusion_proof) = entry.inclusion_proof {
verify_merkle_inclusion(entry, inclusion_proof)?;
verify_checkpoint(
&inclusion_proof.checkpoint.envelope,
inclusion_proof,
trusted_root,
)?;
}
if entry.inclusion_promise.is_some() {
verify_set(entry, trusted_root)?;
}
Ok(())
}
fn verify_merkle_inclusion(entry: &TransparencyLogEntry, proof: &InclusionProof) -> Result<()> {
let leaf_index: u64 = proof
.log_index
.as_u64()
.ok_or_else(|| Error::Verification("invalid log_index in inclusion proof".to_string()))?;
let tree_size: u64 = proof
.tree_size
.try_into()
.map_err(|_| Error::Verification("invalid tree_size in inclusion proof".to_string()))?;
let leaf_hash = sigstore_merkle::hash_leaf(entry.canonicalized_body.as_bytes());
sigstore_merkle::verify_inclusion_proof(
&leaf_hash,
leaf_index,
tree_size,
&proof.hashes,
&proof.root_hash,
)
.map_err(|e| Error::Verification(format!("inclusion proof verification failed: {}", e)))
}
pub fn verify_checkpoint(
checkpoint_envelope: &str,
inclusion_proof: &InclusionProof,
trusted_root: &TrustedRoot,
) -> Result<()> {
let checkpoint = Checkpoint::from_text(checkpoint_envelope)
.map_err(|e| Error::Verification(format!("Failed to parse checkpoint: {}", e)))?;
let checkpoint_root_hash = &checkpoint.root_hash;
let proof_root_hash = &inclusion_proof.root_hash;
if checkpoint_root_hash.as_bytes() != proof_root_hash.as_bytes() {
return Err(Error::Verification(format!(
"Checkpoint root hash mismatch: expected {}, got {}",
checkpoint_root_hash.to_hex(),
proof_root_hash.to_hex()
)));
}
let rekor_keys = trusted_root
.rekor_keys_with_hints()
.map_err(|e| Error::Verification(format!("Failed to get Rekor keys: {}", e)))?;
for sig in &checkpoint.signatures {
for (key_hint, public_key) in &rekor_keys {
if &sig.key_id == key_hint {
let message = checkpoint.signed_data();
verify_signature_auto(public_key, &sig.signature, message).map_err(|e| {
Error::Verification(format!("Checkpoint signature verification failed: {}", e))
})?;
return Ok(());
}
}
}
Err(Error::Verification(
"No matching Rekor key found for checkpoint signature".to_string(),
))
}
#[derive(Serialize)]
struct RekorPayload {
body: String,
#[serde(rename = "integratedTime")]
integrated_time: i64,
#[serde(rename = "logIndex")]
log_index: i64,
#[serde(rename = "logID")]
log_id: String,
}
pub fn verify_set(entry: &TransparencyLogEntry, trusted_root: &TrustedRoot) -> Result<()> {
let promise = entry
.inclusion_promise
.as_ref()
.ok_or(Error::Verification("Missing inclusion promise".into()))?;
let integrated_time = entry.integrated_time;
let log_key = if integrated_time > 0 {
let integrated_ts = jiff::Timestamp::from_second(integrated_time).map_err(|e| {
Error::Verification(format!(
"Invalid integrated time {}: {}",
integrated_time, e
))
})?;
trusted_root
.rekor_key_for_log_at(&entry.log_id.key_id, integrated_ts)
.map_err(|e| {
Error::Verification(format!(
"No log key valid at integrated time {} for log ID {}: {}",
integrated_ts, entry.log_id.key_id, e
))
})?
} else {
trusted_root
.rekor_key_for_log(&entry.log_id.key_id)
.map_err(|_| Error::Verification(format!("Unknown log ID: {}", entry.log_id.key_id)))?
};
let body = entry.canonicalized_body.to_base64();
let log_index = entry
.log_index
.as_u64()
.ok_or_else(|| Error::Verification("Invalid log index".into()))? as i64;
let log_id_bytes = base64::engine::general_purpose::STANDARD
.decode(entry.log_id.key_id.as_str())
.map_err(|_| Error::Verification("Invalid base64 log ID".into()))?;
let log_id_hex = hex::encode(log_id_bytes);
let payload = RekorPayload {
body,
integrated_time,
log_index,
log_id: log_id_hex,
};
let canonical_json = serde_json_canonicalizer::to_vec(&payload)
.map_err(|e| Error::Verification(format!("Canonicalization failed: {}", e)))?;
let signature = SignatureBytes::new(promise.signed_entry_timestamp.as_bytes().to_vec());
verify_signature_auto(&log_key, &signature, &canonical_json)
.map_err(|e| Error::Verification(format!("SET verification failed: {}", e)))?;
Ok(())
}