use crate::artifact::PreparedArtifact;
use crate::error::{Error, Result};
use sigstore_rekor::body::RekorEntryBody;
use sigstore_types::bundle::VerificationMaterialContent;
use sigstore_types::{
Bundle, DerPublicKey, KindVersion, Sha256Hash, SignatureContent, TransparencyLogEntry,
};
use x509_cert::der::Decode;
use x509_cert::Certificate;
pub(crate) fn verify_hashedrekord_entry(
entry: &TransparencyLogEntry,
bundle: &Bundle,
artifact: &PreparedArtifact<'_>,
managed_key: Option<&DerPublicKey>,
) -> Result<()> {
let body = RekorEntryBody::parse(&entry.canonicalized_body, entry.kind_version)
.map_err(|e| Error::malformed_entry(format!("failed to parse Rekor body: {}", e)))?;
let hash = match &bundle.content {
SignatureContent::MessageSignature(_) => artifact.sha256()?,
SignatureContent::DsseEnvelope(envelope) => sigstore_crypto::sha256(&envelope.pae()),
_ => {
return Err(Error::UnsupportedBundle(
"unsupported bundle signature content".to_string(),
))
}
};
match &body {
RekorEntryBody::HashedRekordV001(rekord) => {
let expected =
Sha256Hash::from_hex(rekord.spec.data.hash.value.as_str()).map_err(|e| {
Error::malformed_entry(format!("invalid hash in Rekor entry: {}", e))
})?;
validate_artifact_hash(&hash, &expected)?;
}
RekorEntryBody::HashedRekordV002(rekord) => {
let logged = &rekord.spec.hashed_rekord_v002;
if logged.data.algorithm != sigstore_types::HashAlgorithm::Sha2256 {
return Err(Error::UnsupportedBundle(format!(
"unsupported Rekor v2 digest algorithm: {}",
logged.data.algorithm
)));
}
let expected = Sha256Hash::try_from(&logged.data.digest).map_err(|e| {
Error::malformed_entry(format!("invalid digest in Rekor entry: {}", e))
})?;
validate_artifact_hash(&hash, &expected)?;
}
_ => {
return Err(Error::malformed_entry(format!(
"expected HashedRekord body, got different type for version {}",
entry.kind_version.version()
)));
}
};
validate_verifier_match(entry, &body, bundle, managed_key)?;
validate_signature_match(entry, &body, bundle)?;
validate_integrated_time(entry, bundle)?;
Ok(())
}
fn validate_artifact_hash(artifact_hash: &Sha256Hash, expected_hash: &Sha256Hash) -> Result<()> {
if artifact_hash != expected_hash {
return Err(Error::ArtifactMismatch(
"artifact hash mismatch for hashedrekord entry".to_string(),
));
}
Ok(())
}
fn validate_verifier_match(
_entry: &TransparencyLogEntry,
body: &RekorEntryBody,
bundle: &Bundle,
managed_key: Option<&DerPublicKey>,
) -> 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,
_ => {
return Err(Error::UnsupportedBundle(
"unsupported bundle verification material".to_string(),
))
}
};
if bundle_cert.is_none() {
let managed_key = managed_key.ok_or_else(|| {
Error::entry_mismatch(
"Rekor verifier cannot be bound without the managed public key".to_string(),
)
})?;
let rekor_key = match body {
RekorEntryBody::HashedRekordV001(rekord) => rekord
.spec
.signature
.public_key
.parse_public_key()
.map_err(|e| Error::malformed_entry(e.to_string()))?,
RekorEntryBody::HashedRekordV002(rekord) => rekord
.spec
.hashed_rekord_v002
.signature
.verifier
.public_key
.as_ref()
.map(|key| key.raw_bytes.clone())
.ok_or_else(|| {
Error::entry_mismatch(
"managed-key Rekor entry contains no public-key verifier".to_string(),
)
})?,
_ => {
return Err(Error::UnsupportedBundle(
"unsupported Rekor verifier type".to_string(),
))
}
};
if rekor_key.as_bytes() != managed_key.as_bytes() {
return Err(Error::entry_mismatch(
"public key in Rekor entry does not match managed key".to_string(),
));
}
return Ok(());
}
let bundle_cert = bundle_cert.expect("checked above");
let rekor_cert_der_opt = match body {
RekorEntryBody::HashedRekordV001(rekord) => {
let cert = rekord
.spec
.signature
.public_key
.parse_certificate()
.map_err(|e| Error::malformed_entry(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,
};
let rekor_cert_der = rekor_cert_der_opt.ok_or_else(|| {
Error::entry_mismatch("certificate-backed Rekor entry contains no certificate".to_string())
})?;
if bundle_cert.as_bytes() != rekor_cert_der {
return Err(Error::entry_mismatch(
"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::entry_mismatch(
"signature in bundle does not match signature in Rekor entry".to_string(),
));
}
}
SignatureContent::DsseEnvelope(envelope) => {
if &envelope.signature.sig != rekor_sig {
return Err(Error::entry_mismatch(
"DSSE signature in bundle does not match signature in Rekor entry"
.to_string(),
));
}
}
_ => {
return Err(Error::UnsupportedBundle(
"unsupported bundle signature content".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();
let v1_integrated_time = entry
.integrated_time
.filter(|_| entry.kind_version == KindVersion::HashedRekordV001);
if let Some(integrated_time) = v1_integrated_time {
let cert = Certificate::from_der(bundle_cert_der).map_err(|e| {
Error::cert_malformed(format!(
"failed to parse certificate for time validation: {}",
e
))
})?;
let not_before = jiff::Timestamp::try_from(
cert.tbs_certificate.validity.not_before.to_system_time(),
)
.map_err(|e| Error::cert_malformed(format!("invalid notBefore time: {}", e)))?;
let not_after =
jiff::Timestamp::try_from(cert.tbs_certificate.validity.not_after.to_system_time())
.map_err(|e| Error::cert_malformed(format!("invalid notAfter time: {}", e)))?;
if integrated_time < not_before || integrated_time > not_after {
return Err(Error::integrated_time(format!(
"integrated time {} is outside certificate validity period ({} to {})",
integrated_time, not_before, not_after
)));
}
}
}
Ok(())
}