use crate::error::{Error, Result};
use sigstore_rekor::body::RekorEntryBody;
use sigstore_types::bundle::VerificationMaterialContent;
use sigstore_types::{Artifact, Bundle, Sha256Hash, SignatureContent, TransparencyLogEntry};
use x509_cert::der::Decode;
use x509_cert::Certificate;
pub(crate) fn verify_hashedrekord_entry(
entry: &TransparencyLogEntry,
bundle: &Bundle,
artifact: &Artifact<'_>,
) -> 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 hash = match &bundle.content {
SignatureContent::MessageSignature(_) => compute_artifact_digest(artifact)?,
SignatureContent::DsseEnvelope(envelope) => sigstore_crypto::sha256(&envelope.pae()),
};
match &body {
RekorEntryBody::HashedRekordV001(rekord) => {
let expected = Sha256Hash::from_hex(rekord.spec.data.hash.value.as_str())
.map_err(|e| Error::Verification(format!("invalid hash in Rekor entry: {}", e)))?;
validate_artifact_hash(&hash, &expected)?;
}
RekorEntryBody::HashedRekordV002(rekord) => {
let expected = Sha256Hash::try_from_slice(&rekord.spec.hashed_rekord_v002.data.digest)
.map_err(|e| {
Error::Verification(format!("invalid digest in Rekor entry: {}", e))
})?;
validate_artifact_hash(&hash, &expected)?;
}
_ => {
return Err(Error::Verification(format!(
"expected HashedRekord body, got different type for version {}",
entry.kind_version.version
)));
}
};
validate_certificate_match(entry, &body, bundle)?;
validate_signature_match(entry, &body, bundle)?;
validate_integrated_time(entry, bundle)?;
Ok(())
}
fn compute_artifact_digest(artifact: &Artifact<'_>) -> Result<Sha256Hash> {
match artifact {
Artifact::Bytes(bytes) => Ok(sigstore_crypto::sha256(bytes)),
Artifact::Digest(hash) => Sha256Hash::try_from_slice(hash).map_err(|_| {
Error::Verification(
"Rekor entry verification requires a 32-byte SHA-256 digest".to_string(),
)
}),
}
}
fn validate_artifact_hash(artifact_hash: &Sha256Hash, expected_hash: &Sha256Hash) -> Result<()> {
if artifact_hash != expected_hash {
return Err(Error::Verification(
"artifact hash mismatch for hashedrekord entry".to_string(),
));
}
Ok(())
}
fn validate_certificate_match(
_entry: &TransparencyLogEntry,
body: &RekorEntryBody,
bundle: &Bundle,
) -> Result<()> {
let bundle_cert = match &bundle.verification_material.content {
VerificationMaterialContent::X509CertificateChain { certificates } => {
certificates.first().map(|c| &c.raw_bytes)
}
VerificationMaterialContent::Certificate(cert) => Some(&cert.raw_bytes),
VerificationMaterialContent::PublicKey { .. } => None,
};
let Some(bundle_cert) = bundle_cert else {
return Ok(());
};
let rekor_cert_der_opt = match body {
RekorEntryBody::HashedRekordV001(rekord) => {
let cert = rekord
.spec
.signature
.public_key
.to_certificate()
.map_err(|e| Error::Verification(format!("{}", e)))?;
Some(cert.as_bytes().to_vec())
}
RekorEntryBody::HashedRekordV002(rekord) => {
rekord
.spec
.hashed_rekord_v002
.signature
.verifier
.x509_certificate
.as_ref()
.map(|cert| cert.raw_bytes.as_bytes().to_vec())
}
_ => None,
};
if let Some(rekor_cert_der) = rekor_cert_der_opt {
if bundle_cert.as_bytes() != rekor_cert_der {
return Err(Error::Verification(
"certificate in bundle does not match certificate in Rekor entry".to_string(),
));
}
}
Ok(())
}
fn validate_signature_match(
_entry: &TransparencyLogEntry,
body: &RekorEntryBody,
bundle: &Bundle,
) -> Result<()> {
let rekor_sig = match body {
RekorEntryBody::HashedRekordV001(rekord) => {
Some(&rekord.spec.signature.content)
}
RekorEntryBody::HashedRekordV002(rekord) => {
Some(&rekord.spec.hashed_rekord_v002.signature.content)
}
_ => None,
};
if let Some(rekor_sig) = rekor_sig {
match &bundle.content {
SignatureContent::MessageSignature(sig) => {
let bundle_sig = &sig.signature;
if bundle_sig != rekor_sig {
return Err(Error::Verification(
"signature in bundle does not match signature in Rekor entry".to_string(),
));
}
}
SignatureContent::DsseEnvelope(envelope) => {
if let Some(first_sig) = envelope.signatures.first() {
if &first_sig.sig != rekor_sig {
return Err(Error::Verification(
"DSSE signature in bundle does not match signature in Rekor entry"
.to_string(),
));
}
} else {
return Err(Error::Verification(
"No signatures found in DSSE envelope".to_string(),
));
}
}
}
}
Ok(())
}
fn validate_integrated_time(entry: &TransparencyLogEntry, bundle: &Bundle) -> Result<()> {
let bundle_cert = match &bundle.verification_material.content {
VerificationMaterialContent::X509CertificateChain { certificates } => {
certificates.first().map(|c| &c.raw_bytes)
}
VerificationMaterialContent::Certificate(cert) => Some(&cert.raw_bytes),
_ => None,
};
if let Some(bundle_cert) = bundle_cert {
let bundle_cert_der = bundle_cert.as_bytes();
if entry.kind_version.version == "0.0.1" && entry.integrated_time != 0 {
let cert = Certificate::from_der(bundle_cert_der).map_err(|e| {
Error::Verification(format!(
"failed to parse certificate for time validation: {}",
e
))
})?;
use std::time::UNIX_EPOCH;
let not_before_system = cert.tbs_certificate.validity.not_before.to_system_time();
let not_after_system = cert.tbs_certificate.validity.not_after.to_system_time();
let not_before = not_before_system
.duration_since(UNIX_EPOCH)
.map_err(|e| {
Error::Verification(format!("failed to convert notBefore to Unix time: {}", e))
})?
.as_secs() as i64;
let not_after = not_after_system
.duration_since(UNIX_EPOCH)
.map_err(|e| {
Error::Verification(format!("failed to convert notAfter to Unix time: {}", e))
})?
.as_secs() as i64;
let integrated_time = entry.integrated_time;
if integrated_time < not_before || integrated_time > not_after {
return Err(Error::Verification(format!(
"integrated time {} is outside certificate validity period ({} to {})",
integrated_time, not_before, not_after
)));
}
}
}
Ok(())
}