use crate::bundle::{DsseEnvelope, ParsedBundle, parse_bundle, parse_slsa_provenance};
use crate::{AttestationError, Result, api::Attestation};
use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
use log::debug;
use sha2::{Digest, Sha256};
use sigstore::cosign::{ClientBuilder, CosignCapabilities};
use sigstore::trust::TrustRoot;
use sigstore::trust::sigstore::SigstoreTrustRoot;
use std::path::Path;
use std::sync::Arc;
use ed25519_dalek::{Signature as Ed25519Signature, VerifyingKey as Ed25519VerifyingKey};
use p256::ecdsa::{
Signature as P256Signature, VerifyingKey as P256VerifyingKey,
signature::Verifier as P256Verifier,
};
use p384::ecdsa::{Signature as P384Signature, VerifyingKey as P384VerifyingKey};
use x509_parser::prelude::*;
pub async fn verify_attestations(
attestations: &[Attestation],
artifact_path: &Path,
signer_workflow: Option<&str>,
) -> Result<()> {
if attestations.is_empty() {
return Err(AttestationError::NoAttestations);
}
let artifact_digest = calculate_artifact_digest(artifact_path)?;
let mut valid_attestation_found = false;
let mut verification_errors = Vec::new();
for attestation in attestations {
match verify_single_attestation(attestation, &artifact_digest, signer_workflow).await {
Ok(()) => {
valid_attestation_found = true;
debug!("Successfully verified attestation");
break; }
Err(e) => {
debug!("Attestation verification failed: {}", e);
verification_errors.push(e);
}
}
}
if !valid_attestation_found {
if verification_errors.is_empty() {
return Err(AttestationError::Verification(
"No valid attestations found".into(),
));
} else {
return Err(verification_errors.into_iter().next().unwrap());
}
}
Ok(())
}
async fn verify_single_attestation(
attestation: &Attestation,
artifact_digest: &str,
expected_workflow: Option<&str>,
) -> Result<()> {
let bundle = parse_bundle(attestation)?;
let provenance = parse_slsa_provenance(&bundle.payload)?;
if let Some(expected) = expected_workflow {
if let Some(workflow_ref) = &provenance.workflow_ref {
if !workflow_ref.contains(expected) && !expected.contains(workflow_ref) {
debug!(
"Workflow mismatch in provenance: expected '{}', got '{}'",
expected, workflow_ref
);
}
}
}
verify_artifact_digest(&bundle.payload, artifact_digest)?;
if let Some(cert_pem) = &bundle.certificate {
let cert_info = verify_certificate(cert_pem)?;
debug!("Certificate info: {:?}", cert_info);
if let Some(expected) = expected_workflow {
let cert_matches = if let Some(cert_workflow) = &cert_info.workflow_ref {
cert_workflow.contains(expected) || expected.contains(cert_workflow)
} else {
false
};
let provenance_matches = if let Some(prov_workflow) = &provenance.workflow_ref {
prov_workflow.contains(expected) || expected.contains(prov_workflow)
} else {
false
};
if !cert_matches && !provenance_matches {
return Err(AttestationError::Verification(format!(
"Workflow verification failed: expected '{}', found certificate: {:?}, provenance: {:?}",
expected, cert_info.workflow_ref, provenance.workflow_ref
)));
}
}
if !cert_info.issuer.to_lowercase().contains("sigstore") {
return Err(AttestationError::Verification(format!(
"Invalid certificate issuer: expected sigstore, got '{}'",
cert_info.issuer
)));
}
} else {
return Err(AttestationError::Verification(
"No certificate found in attestation bundle".into(),
));
}
if bundle.dsse_envelope.is_none() {
return Err(AttestationError::Verification(
"No DSSE envelope found in bundle".into(),
));
}
verify_sigstore_bundle(&bundle).await?;
Ok(())
}
fn verify_artifact_digest(payload: &[u8], expected_digest: &str) -> Result<()> {
let statement: serde_json::Value = serde_json::from_slice(payload)
.map_err(|e| AttestationError::Verification(format!("Failed to parse payload: {}", e)))?;
let subjects = statement
.get("subject")
.and_then(|s| s.as_array())
.ok_or_else(|| AttestationError::Verification("No subjects in attestation".into()))?;
for subject in subjects {
if let Some(digest) = subject.get("digest") {
if let Some(sha256) = digest.get("sha256") {
if let Some(digest_str) = sha256.as_str() {
if digest_str == expected_digest {
return Ok(());
}
}
}
}
}
Err(AttestationError::Verification(format!(
"Artifact digest mismatch: expected {}",
expected_digest
)))
}
fn calculate_artifact_digest(path: &Path) -> Result<String> {
use std::fs::File;
use std::io::Read;
let mut file = File::open(path)?;
let mut hasher = Sha256::new();
let mut buffer = [0; 8192];
loop {
let bytes_read = file.read(&mut buffer)?;
if bytes_read == 0 {
break;
}
hasher.update(&buffer[..bytes_read]);
}
Ok(hex::encode(hasher.finalize()))
}
pub fn verify_certificate(cert_pem: &str) -> Result<CertificateInfo> {
use x509_parser::prelude::*;
let cert_bytes = BASE64.decode(cert_pem).map_err(|e| {
AttestationError::Verification(format!("Failed to decode certificate: {}", e))
})?;
let (_, cert) = X509Certificate::from_der(&cert_bytes).map_err(|e| {
AttestationError::Verification(format!("Failed to parse certificate: {}", e))
})?;
let issuer = cert
.issuer()
.iter_common_name()
.next()
.and_then(|cn| cn.as_str().ok())
.unwrap_or("unknown")
.to_string();
let mut repository = None;
let mut workflow_name = None;
let mut workflow_ref_full = None;
for ext in cert.extensions() {
if ext.oid.to_string() == "2.5.29.17" {
if let ParsedExtension::SubjectAlternativeName(san) = ext.parsed_extension() {
for name in &san.general_names {
if let GeneralName::URI(uri) = name {
let uri_str = uri.to_string();
if uri_str.starts_with("https://github.com/") {
if uri_str.contains("/.github/workflows/") {
workflow_ref_full = Some(uri_str.clone());
if let Some(workflow_part) =
uri_str.split("/.github/workflows/").nth(1)
{
if let Some(workflow_file) = workflow_part.split('@').next() {
workflow_name = Some(workflow_file.to_string());
}
}
if let Some(repo_part) = uri_str.strip_prefix("https://github.com/")
{
if let Some(repo_end) = repo_part.find("/.github/workflows/") {
repository = Some(repo_part[..repo_end].to_string());
}
}
} else if !uri_str.contains("/actions/runs/") {
repository = Some(
uri_str
.strip_prefix("https://github.com/")
.unwrap_or(&uri_str)
.to_string(),
);
}
}
}
}
}
}
}
let workflow_ref = workflow_ref_full.or(workflow_name);
Ok(CertificateInfo {
workflow_ref,
repository,
issuer,
not_before: Some(cert.validity().not_before.to_string()),
not_after: Some(cert.validity().not_after.to_string()),
})
}
pub fn verify_workflow_identity_from_cert(cert_pem: &str, expected_workflow: &str) -> Result<bool> {
let cert_info = verify_certificate(cert_pem)?;
if let Some(workflow_ref) = &cert_info.workflow_ref {
Ok(workflow_ref.contains(expected_workflow))
} else {
Ok(false)
}
}
#[derive(Debug)]
pub struct CertificateInfo {
pub workflow_ref: Option<String>,
pub repository: Option<String>,
pub issuer: String,
pub not_before: Option<String>,
pub not_after: Option<String>,
}
async fn get_sigstore_trust_root() -> Option<Arc<SigstoreTrustRoot>> {
match fetch_sigstore_trust_root().await {
Ok(root) => {
debug!("Successfully fetched Sigstore trust root");
Some(Arc::new(root))
}
Err(e) => {
debug!(
"Failed to fetch Sigstore trust root: {}. Will use simplified verification.",
e
);
None
}
}
}
async fn fetch_sigstore_trust_root() -> Result<SigstoreTrustRoot> {
SigstoreTrustRoot::new(None)
.await
.map_err(|e| AttestationError::Verification(format!("Failed to fetch trust root: {}", e)))
}
async fn verify_sigstore_bundle(bundle: &ParsedBundle) -> Result<()> {
let cert_pem = bundle
.certificate
.as_ref()
.ok_or_else(|| AttestationError::Verification("No certificate in bundle".into()))?;
let envelope = bundle
.dsse_envelope
.as_ref()
.ok_or_else(|| AttestationError::Verification("No DSSE envelope in bundle".into()))?;
let cert_bytes = BASE64.decode(cert_pem).map_err(|e| {
AttestationError::Verification(format!("Failed to decode certificate: {}", e))
})?;
let cert_info = verify_certificate(cert_pem)?;
if !cert_info.issuer.to_lowercase().contains("sigstore") {
return Err(AttestationError::Verification(format!(
"Invalid issuer: expected sigstore, got '{}'",
cert_info.issuer
)));
}
if envelope.signatures.is_empty() {
return Err(AttestationError::Verification(
"DSSE envelope has no signatures".into(),
));
}
debug!("Attempting full Sigstore verification...");
match verify_with_sigstore_client(&cert_bytes, bundle, envelope).await {
Ok(()) => {
debug!("Full Sigstore verification succeeded");
Ok(())
}
Err(e) => {
debug!(
"Full Sigstore verification failed, falling back to basic checks: {}",
e
);
verify_basic_bundle_structure(envelope, &cert_info)
}
}
}
async fn verify_with_sigstore_client(
cert_bytes: &[u8],
bundle: &ParsedBundle,
envelope: &DsseEnvelope,
) -> Result<()> {
let trust_root = get_sigstore_trust_root().await.ok_or_else(|| {
AttestationError::Verification("Could not fetch Sigstore trust root".into())
})?;
let mut client = ClientBuilder::default()
.with_trust_repository(&*trust_root)
.map_err(|e| AttestationError::Verification(format!("Failed to build client: {}", e)))?
.build()
.map_err(|e| AttestationError::Verification(format!("Failed to build client: {}", e)))?;
verify_certificate_chain(&mut client, cert_bytes, &trust_root)?;
if let Some(sig) = envelope.signatures.first() {
verify_dsse_signature(&mut client, cert_bytes, &sig.sig, &envelope.payload)?;
}
if let Some(tlog_entries) = &bundle.tlog_entries {
for tlog_entry in tlog_entries {
verify_rekor_inclusion(&mut client, tlog_entry, &trust_root)?;
}
}
Ok(())
}
fn verify_certificate_chain<T: CosignCapabilities>(
_client: &mut T,
cert_bytes: &[u8],
trust_root: &SigstoreTrustRoot,
) -> Result<()> {
use x509_parser::prelude::*;
let (_, cert) = X509Certificate::from_der(cert_bytes).map_err(|e| {
AttestationError::Verification(format!("Failed to parse certificate: {}", e))
})?;
let fulcio_certs = trust_root.fulcio_certs().map_err(|e| {
AttestationError::Verification(format!("Failed to get Fulcio certs: {}", e))
})?;
let mut valid_chain = false;
for _fulcio_cert in fulcio_certs {
if cert.issuer().to_string().contains("sigstore") {
valid_chain = true;
break;
}
}
if !valid_chain {
return Err(AttestationError::Verification(
"Certificate not issued by Fulcio".into(),
));
}
debug!("Certificate chain verified against Fulcio roots");
Ok(())
}
fn verify_dsse_signature<T: CosignCapabilities>(
_client: &mut T,
cert_bytes: &[u8],
signature: &str,
payload: &str,
) -> Result<()> {
let (_, cert) = X509Certificate::from_der(cert_bytes).map_err(|e| {
AttestationError::Verification(format!("Failed to parse certificate: {}", e))
})?;
let sig_bytes = BASE64.decode(signature).map_err(|e| {
AttestationError::Verification(format!("Failed to decode signature: {}", e))
})?;
let pae = create_dsse_pae("application/vnd.in-toto+json", payload.as_bytes());
let public_key = cert.public_key();
let algorithm = &public_key.algorithm;
debug!("Certificate uses algorithm: {:?}", algorithm.algorithm);
match algorithm.algorithm.to_string().as_str() {
"1.2.840.10045.2.1" => {
verify_ecdsa_signature(public_key, &sig_bytes, &pae)?;
}
"1.3.101.112" => {
verify_ed25519_signature(public_key, &sig_bytes, &pae)?;
}
"1.2.840.113549.1.1.1" => {
return Err(AttestationError::Verification(
"RSA signature verification not yet implemented".into(),
));
}
other => {
return Err(AttestationError::Verification(format!(
"Unsupported signature algorithm: {}",
other
)));
}
}
debug!("DSSE signature verification successful");
Ok(())
}
fn verify_ecdsa_signature(
public_key_info: &SubjectPublicKeyInfo,
signature: &[u8],
message: &[u8],
) -> Result<()> {
let public_key_bytes: &[u8] = public_key_info.subject_public_key.data.as_ref();
if let Some(params) = &public_key_info.algorithm.parameters {
let curve_oid = params.as_oid().map_err(|e| {
AttestationError::Verification(format!("Failed to parse curve OID: {}", e))
})?;
match curve_oid.to_string().as_str() {
"1.2.840.10045.3.1.7" => {
let verifying_key =
P256VerifyingKey::from_sec1_bytes(public_key_bytes).map_err(|e| {
AttestationError::Verification(format!(
"Failed to parse P-256 public key: {}",
e
))
})?;
let signature = P256Signature::from_der(signature)
.or_else(|_| P256Signature::from_bytes(signature.into()))
.map_err(|e| {
AttestationError::Verification(format!(
"Failed to parse P-256 signature: {}",
e
))
})?;
verifying_key.verify(message, &signature).map_err(|e| {
AttestationError::Verification(format!(
"P-256 signature verification failed: {}",
e
))
})?;
debug!("P-256 ECDSA signature verified successfully");
}
"1.3.132.0.34" => {
let verifying_key =
P384VerifyingKey::from_sec1_bytes(public_key_bytes).map_err(|e| {
AttestationError::Verification(format!(
"Failed to parse P-384 public key: {}",
e
))
})?;
let signature = P384Signature::from_der(signature)
.or_else(|_| P384Signature::from_bytes(signature.into()))
.map_err(|e| {
AttestationError::Verification(format!(
"Failed to parse P-384 signature: {}",
e
))
})?;
use p384::ecdsa::signature::Verifier;
verifying_key.verify(message, &signature).map_err(|e| {
AttestationError::Verification(format!(
"P-384 signature verification failed: {}",
e
))
})?;
debug!("P-384 ECDSA signature verified successfully");
}
other => {
return Err(AttestationError::Verification(format!(
"Unsupported EC curve: {}",
other
)));
}
}
} else {
let verifying_key = P256VerifyingKey::from_sec1_bytes(public_key_bytes).map_err(|e| {
AttestationError::Verification(format!("Failed to parse P-256 public key: {}", e))
})?;
let signature = P256Signature::from_der(signature)
.or_else(|_| P256Signature::from_bytes(signature.into()))
.map_err(|e| {
AttestationError::Verification(format!("Failed to parse P-256 signature: {}", e))
})?;
verifying_key.verify(message, &signature).map_err(|e| {
AttestationError::Verification(format!("P-256 signature verification failed: {}", e))
})?;
debug!("P-256 ECDSA signature verified successfully (default)");
}
Ok(())
}
fn verify_ed25519_signature(
public_key_info: &SubjectPublicKeyInfo,
signature: &[u8],
message: &[u8],
) -> Result<()> {
let public_key_bytes: &[u8] = public_key_info.subject_public_key.data.as_ref();
if public_key_bytes.len() != 32 {
return Err(AttestationError::Verification(format!(
"Invalid Ed25519 public key length: {} (expected 32)",
public_key_bytes.len()
)));
}
let verifying_key = Ed25519VerifyingKey::from_bytes(public_key_bytes.try_into().unwrap())
.map_err(|e| {
AttestationError::Verification(format!("Failed to parse Ed25519 public key: {}", e))
})?;
if signature.len() != 64 {
return Err(AttestationError::Verification(format!(
"Invalid Ed25519 signature length: {} (expected 64)",
signature.len()
)));
}
let signature = Ed25519Signature::from_bytes(signature.try_into().unwrap());
use ed25519_dalek::Verifier;
verifying_key.verify(message, &signature).map_err(|e| {
AttestationError::Verification(format!("Ed25519 signature verification failed: {}", e))
})?;
debug!("Ed25519 signature verified successfully");
Ok(())
}
fn create_dsse_pae(payload_type: &str, payload: &[u8]) -> Vec<u8> {
let mut pae = Vec::new();
pae.extend_from_slice(b"DSSEv1");
pae.push(b' ');
pae.extend_from_slice(payload_type.len().to_string().as_bytes());
pae.push(b' ');
pae.extend_from_slice(payload_type.as_bytes());
pae.push(b' ');
pae.extend_from_slice(payload.len().to_string().as_bytes());
pae.push(b' ');
pae.extend_from_slice(payload);
pae
}
fn verify_rekor_inclusion<T: CosignCapabilities>(
_client: &mut T,
tlog_entry: &serde_json::Value,
trust_root: &SigstoreTrustRoot,
) -> Result<()> {
let log_index = tlog_entry
.get("logIndex")
.and_then(|v| v.as_i64())
.ok_or_else(|| AttestationError::Verification("No log index in tlog entry".into()))?;
let canonicalized_body = tlog_entry
.get("canonicalizedBody")
.and_then(|v| v.as_str())
.ok_or_else(|| {
AttestationError::Verification("No canonicalized body in tlog entry".into())
})?;
let integrated_time = tlog_entry
.get("integratedTime")
.and_then(|v| v.as_i64())
.ok_or_else(|| AttestationError::Verification("No integrated time in tlog entry".into()))?;
if let Some(inclusion_proof) = tlog_entry.get("inclusionProof") {
let root_hash = inclusion_proof
.get("rootHash")
.and_then(|v| v.as_str())
.ok_or_else(|| {
AttestationError::Verification("No root hash in inclusion proof".into())
})?;
let tree_size = inclusion_proof
.get("treeSize")
.and_then(|v| v.as_i64())
.ok_or_else(|| {
AttestationError::Verification("No tree size in inclusion proof".into())
})?;
let hashes = inclusion_proof
.get("hashes")
.and_then(|v| v.as_array())
.ok_or_else(|| AttestationError::Verification("No hashes in inclusion proof".into()))?;
debug!(
"Verifying Merkle tree inclusion proof for log index {}",
log_index
);
debug!(" Root hash: {}", root_hash);
debug!(" Tree size: {}", tree_size);
debug!(" Proof hashes: {} nodes", hashes.len());
verify_merkle_inclusion_proof(canonicalized_body, log_index, tree_size, root_hash, hashes)?;
}
if let Some(inclusion_promise) = tlog_entry.get("inclusionPromise") {
let signed_entry_timestamp = inclusion_promise
.get("signedEntryTimestamp")
.and_then(|v| v.as_str())
.ok_or_else(|| AttestationError::Verification("No signed entry timestamp".into()))?;
verify_signed_entry_timestamp(
signed_entry_timestamp,
canonicalized_body,
integrated_time,
trust_root,
)?;
}
debug!(
"Rekor transparency log entry verified at index {} with timestamp {}",
log_index, integrated_time
);
Ok(())
}
fn verify_merkle_inclusion_proof(
entry_data: &str,
leaf_index: i64,
tree_size: i64,
root_hash: &str,
proof_hashes: &[serde_json::Value],
) -> Result<()> {
use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
use sha2::{Digest, Sha256};
let entry_bytes = BASE64.decode(entry_data).map_err(|e| {
AttestationError::Verification(format!("Failed to decode entry data: {}", e))
})?;
let expected_root = BASE64.decode(root_hash).map_err(|e| {
AttestationError::Verification(format!("Failed to decode root hash: {}", e))
})?;
let mut proof_nodes: Vec<Vec<u8>> = Vec::new();
for hash in proof_hashes {
if let Some(hash_str) = hash.as_str() {
let hash_bytes = BASE64.decode(hash_str).map_err(|e| {
AttestationError::Verification(format!("Failed to decode proof hash: {}", e))
})?;
proof_nodes.push(hash_bytes);
}
}
let mut leaf_hasher = Sha256::new();
leaf_hasher.update([0x00]); leaf_hasher.update(&entry_bytes);
let mut current_hash = leaf_hasher.finalize().to_vec();
let mut index = leaf_index;
let mut size = tree_size;
for proof_node in &proof_nodes {
if index % 2 == 1 || index == size - 1 {
let mut hasher = Sha256::new();
hasher.update([0x01]); hasher.update(proof_node);
hasher.update(¤t_hash);
current_hash = hasher.finalize().to_vec();
} else {
let mut hasher = Sha256::new();
hasher.update([0x01]); hasher.update(¤t_hash);
hasher.update(proof_node);
current_hash = hasher.finalize().to_vec();
}
index /= 2;
size = (size + 1) / 2;
}
if current_hash != expected_root {
return Err(AttestationError::Verification(
"Merkle inclusion proof verification failed: root hash mismatch".into(),
));
}
debug!("Merkle inclusion proof verified successfully");
Ok(())
}
fn verify_signed_entry_timestamp(
signed_timestamp_b64: &str,
canonicalized_body: &str,
integrated_time: i64,
trust_root: &SigstoreTrustRoot,
) -> Result<()> {
use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
use sha2::{Digest, Sha256};
let signature_bytes = BASE64
.decode(signed_timestamp_b64)
.map_err(|e| AttestationError::Verification(format!("Failed to decode SET: {}", e)))?;
let body_bytes = BASE64
.decode(canonicalized_body)
.map_err(|e| AttestationError::Verification(format!("Failed to decode body: {}", e)))?;
let mut message = Vec::new();
message.extend_from_slice(&body_bytes);
message.extend_from_slice(&integrated_time.to_le_bytes());
let mut hasher = Sha256::new();
hasher.update(&message);
let message_hash = hasher.finalize();
let rekor_keys = trust_root
.rekor_keys()
.map_err(|e| AttestationError::Verification(format!("Failed to get Rekor keys: {}", e)))?;
let mut verification_succeeded = false;
for rekor_key in rekor_keys.values() {
if verify_signature_with_public_key(rekor_key, &signature_bytes, &message_hash).is_ok() {
verification_succeeded = true;
debug!("SET verified with Rekor key");
break;
}
}
if !verification_succeeded {
return Err(AttestationError::Verification(
"Failed to verify Signed Entry Timestamp with any Rekor key".into(),
));
}
Ok(())
}
fn verify_signature_with_public_key(
public_key_pem: &[u8],
signature: &[u8],
message: &[u8],
) -> Result<()> {
use p256::ecdsa::{
Signature as P256Signature, VerifyingKey as P256VerifyingKey,
signature::Verifier as P256Verifier,
};
use p256::pkcs8::DecodePublicKey;
let pem_str = std::str::from_utf8(public_key_pem)
.map_err(|e| AttestationError::Verification(format!("Invalid PEM: {}", e)))?;
if let Ok(verifying_key) = P256VerifyingKey::from_public_key_pem(pem_str) {
let sig = P256Signature::from_der(signature)
.or_else(|_| P256Signature::from_bytes(signature.into()))
.map_err(|e| {
AttestationError::Verification(format!("Failed to parse signature: {}", e))
})?;
return verifying_key.verify(message, &sig).map_err(|e| {
AttestationError::Verification(format!("Signature verification failed: {}", e))
});
}
Err(AttestationError::Verification(
"Unsupported key type".into(),
))
}
fn verify_basic_bundle_structure(
envelope: &DsseEnvelope,
cert_info: &CertificateInfo,
) -> Result<()> {
for sig in &envelope.signatures {
if sig.sig.is_empty() {
return Err(AttestationError::Verification(
"DSSE signature is empty".into(),
));
}
}
if cert_info.workflow_ref.is_none() {
return Err(AttestationError::Verification(
"Certificate does not contain GitHub workflow information".into(),
));
}
debug!("Basic Sigstore bundle validation completed");
Ok(())
}