use crate::error::{Error, Result};
use sigstore_rekor::body::RekorEntryBody;
use sigstore_types::bundle::VerificationMaterialContent;
use sigstore_types::{
Artifact, Bundle, Sha256Hash, SignatureBytes, SignatureContent, TransparencyLogEntry,
};
use x509_cert::der::Decode;
use x509_cert::Certificate;
pub fn verify_hashedrekord_entries(bundle: &Bundle, artifact: &Artifact<'_>) -> Result<()> {
for entry in &bundle.verification_material.tlog_entries {
if entry.kind_version.kind == "hashedrekord" {
verify_hashedrekord_entry(entry, bundle, artifact)?;
}
}
Ok(())
}
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 artifact_hash = compute_artifact_digest(artifact);
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(&artifact_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(&artifact_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)?;
verify_signature_cryptographically(entry, &body, bundle, artifact)?;
Ok(())
}
fn compute_artifact_digest(artifact: &Artifact<'_>) -> Sha256Hash {
match artifact {
Artifact::Bytes(bytes) => sigstore_crypto::sha256(bytes),
Artifact::Digest(hash) => *hash,
}
}
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 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 {
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 bundle_cert_der != 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 {
if let SignatureContent::MessageSignature(sig) = &bundle.content {
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(),
));
}
}
}
Ok(())
}
fn verify_signature_cryptographically(
_entry: &TransparencyLogEntry,
body: &RekorEntryBody,
bundle: &Bundle,
artifact: &Artifact<'_>,
) -> Result<()> {
if let SignatureContent::MessageSignature(_) = &bundle.content {
let signature_bytes = match body {
RekorEntryBody::HashedRekordV001(rekord) => {
SignatureBytes::new(rekord.spec.signature.content.as_bytes().to_vec())
}
RekorEntryBody::HashedRekordV002(rekord) => SignatureBytes::new(
rekord
.spec
.hashed_rekord_v002
.signature
.content
.as_bytes()
.to_vec(),
),
_ => return Ok(()),
};
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 cert_der = bundle_cert.as_bytes();
let cert_info = sigstore_crypto::x509::parse_certificate_info(cert_der)?;
match artifact {
Artifact::Bytes(bytes) => {
sigstore_crypto::verification::verify_signature(
&cert_info.public_key,
bytes,
&signature_bytes,
cert_info.signing_scheme,
)
.map_err(|e| {
Error::Verification(format!(
"cryptographic signature verification failed: {}",
e
))
})?;
}
Artifact::Digest(hash) => {
if cert_info.signing_scheme.uses_sha256()
&& cert_info.signing_scheme.supports_prehashed()
{
tracing::debug!(
"Using prehashed verification for {} with pre-computed digest",
cert_info.signing_scheme.name()
);
sigstore_crypto::verification::verify_signature_prehashed(
&cert_info.public_key,
hash,
&signature_bytes,
cert_info.signing_scheme,
)
.map_err(|e| {
Error::Verification(format!(
"cryptographic signature verification failed: {}",
e
))
})?;
} else {
tracing::debug!(
"Skipping cryptographic signature verification for {} with digest-only - \
scheme uses different hash algorithm or doesn't support prehashed",
cert_info.signing_scheme.name()
);
}
}
}
}
}
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(())
}