use crate::error::{Error, Result};
use base64::Engine;
use sigstore_rekor::body::RekorEntryBody;
use sigstore_types::bundle::VerificationMaterialContent;
use sigstore_types::{Bundle, SignatureContent, TransparencyLogEntry};
pub fn verify_tlog_consistency(
bundle: &Bundle,
artifact: &sigstore_types::Artifact<'_>,
) -> Result<()> {
for entry in &bundle.verification_material.tlog_entries {
match &bundle.content {
SignatureContent::DsseEnvelope(envelope) => match entry.kind_version.kind.as_str() {
"hashedrekord" => match entry.kind_version.version.as_str() {
"0.0.2" => {
super::hashedrekord::verify_hashedrekord_entry(entry, bundle, artifact)?;
}
version => {
return Err(Error::Verification(format!(
"unsupported hashedrekord entry version for DSSE envelope: {}",
version
)))
}
},
"dsse" => match entry.kind_version.version.as_str() {
"0.0.1" => verify_dsse_v001(entry, envelope, bundle)?,
version => {
return Err(Error::Verification(format!(
"unsupported dsse entry version: {}",
version
)))
}
},
"intoto" => match entry.kind_version.version.as_str() {
"0.0.2" => verify_intoto_v002(entry, envelope)?,
version => {
return Err(Error::Verification(format!(
"unsupported intoto entry version: {}",
version
)))
}
},
kind => {
return Err(Error::Verification(format!(
"unsupported log entry kind for DSSE envelope: {}",
kind
)))
}
},
SignatureContent::MessageSignature(_) => match entry.kind_version.kind.as_str() {
"hashedrekord" => match entry.kind_version.version.as_str() {
"0.0.1" | "0.0.2" => {
super::hashedrekord::verify_hashedrekord_entry(entry, bundle, artifact)?;
}
version => {
return Err(Error::Verification(format!(
"unsupported hashedrekord entry version: {}",
version
)))
}
},
kind => {
return Err(Error::Verification(format!(
"unsupported log entry kind for MessageSignature: {}",
kind
)))
}
},
}
}
Ok(())
}
fn verify_dsse_v001(
entry: &TransparencyLogEntry,
envelope: &sigstore_types::DsseEnvelope,
bundle: &Bundle,
) -> Result<()> {
let body = RekorEntryBody::from_base64_json(
&entry.canonicalized_body.to_base64(),
&entry.kind_version.kind,
&entry.kind_version.version,
)
.map_err(|e| Error::Verification(format!("failed to parse Rekor body: {}", e)))?;
let (expected_hash, rekor_signatures) = match &body {
RekorEntryBody::DsseV001(dsse_body) => (
&dsse_body.spec.payload_hash.value,
&dsse_body.spec.signatures,
),
_ => {
return Err(Error::Verification(
"expected DSSE v0.0.1 body, got different type".to_string(),
))
}
};
let payload_bytes = envelope.payload.as_bytes();
let payload_hash = sigstore_crypto::sha256(payload_bytes);
let payload_hash_hex = hex::encode(payload_hash);
if &payload_hash_hex != expected_hash {
return Err(Error::Verification(format!(
"DSSE payload hash mismatch: computed {}, expected {}",
payload_hash_hex, expected_hash
)));
}
let bundle_cert = match &bundle.verification_material.content {
VerificationMaterialContent::X509CertificateChain { certificates } => {
certificates.first().map(|c| c.raw_bytes.clone())
}
VerificationMaterialContent::Certificate(cert) => Some(cert.raw_bytes.clone()),
VerificationMaterialContent::PublicKey { .. } => None,
};
if envelope.signatures.len() != rekor_signatures.len() {
return Err(Error::Verification(format!(
"DSSE signature count mismatch: bundle has {}, Rekor entry has {}",
envelope.signatures.len(),
rekor_signatures.len()
)));
}
for bundle_sig in &envelope.signatures {
let mut found = false;
for rekor_sig in rekor_signatures {
if bundle_sig.sig.as_bytes() != rekor_sig.signature.as_bytes() {
continue;
}
match &bundle_cert {
Some(cert) => {
let rekor_cert_der = rekor_sig
.to_certificate()
.map_err(|e| Error::Verification(format!("{}", e)))?;
if cert.as_bytes() == rekor_cert_der.as_bytes() {
found = true;
break;
}
}
None => {
found = true;
break;
}
}
}
if !found {
return Err(Error::Verification(
"DSSE signature in bundle does not match any signature in Rekor entry (signature or verifier mismatch)".to_string(),
));
}
}
Ok(())
}
fn verify_intoto_v002(
entry: &TransparencyLogEntry,
envelope: &sigstore_types::DsseEnvelope,
) -> Result<()> {
let body = RekorEntryBody::from_base64_json(
&entry.canonicalized_body.to_base64(),
&entry.kind_version.kind,
&entry.kind_version.version,
)
.map_err(|e| Error::Verification(format!("failed to parse Rekor body: {}", e)))?;
let (rekor_payload_b64, rekor_signatures) = match &body {
RekorEntryBody::IntotoV002(intoto_body) => (
&intoto_body.spec.content.envelope.payload,
&intoto_body.spec.content.envelope.signatures,
),
_ => {
return Err(Error::Verification(
"expected Intoto v0.0.2 body, got different type".to_string(),
))
}
};
let rekor_payload_bytes = base64::engine::general_purpose::STANDARD
.decode(rekor_payload_b64.as_bytes())
.map_err(|e| Error::Verification(format!("failed to decode Rekor payload: {}", e)))?;
if envelope.payload.as_bytes() != rekor_payload_bytes.as_slice() {
return Err(Error::Verification(
"DSSE payload in bundle does not match intoto Rekor entry".to_string(),
));
}
let mut found_match = false;
for bundle_sig in &envelope.signatures {
for rekor_sig in rekor_signatures {
let rekor_sig_decoded = base64::engine::general_purpose::STANDARD
.decode(rekor_sig.sig.as_bytes())
.map_err(|e| {
Error::Verification(format!("failed to decode Rekor signature: {}", e))
})?;
if bundle_sig.sig.as_bytes() == rekor_sig_decoded.as_slice() {
found_match = true;
break;
}
}
if found_match {
break;
}
}
if !found_match {
return Err(Error::Verification(
"DSSE signature in bundle does not match intoto Rekor entry".to_string(),
));
}
Ok(())
}