use crate::bundle::ParsedBundle;
use crate::verifiers::{Policy, VerificationResult, Verifier};
use crate::{AttestationError, Result};
use async_trait::async_trait;
use log::{debug, trace};
use std::path::Path;
use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
use ed25519_dalek::{Signature as Ed25519Signature, VerifyingKey as Ed25519VerifyingKey};
use p256::ecdsa::{
Signature as P256Signature, VerifyingKey as P256VerifyingKey,
signature::Verifier as P256Verifier,
};
use p256::pkcs8::DecodePublicKey;
pub struct CosignVerifier {
pub keyless: bool,
pub public_key: Option<Vec<u8>>,
}
impl CosignVerifier {
pub fn new_keyless() -> Self {
Self {
keyless: true,
public_key: None,
}
}
pub fn new_with_key(public_key: Vec<u8>) -> Self {
Self {
keyless: false,
public_key: Some(public_key),
}
}
pub async fn new_with_key_file(key_path: &Path) -> Result<Self> {
use tokio::fs;
let public_key = fs::read(key_path).await.map_err(|e| {
AttestationError::Verification(format!("Failed to read public key file: {}", e))
})?;
Ok(Self {
keyless: false,
public_key: Some(public_key),
})
}
pub fn new_with_key_string(key_str: &str) -> Self {
Self {
keyless: false,
public_key: Some(key_str.as_bytes().to_vec()),
}
}
}
#[async_trait]
impl Verifier for CosignVerifier {
async fn verify(
&self,
bundle: &ParsedBundle,
artifact_path: &Path,
policy: &Policy,
) -> Result<VerificationResult> {
debug!("Starting Cosign verification for {:?}", artifact_path);
let artifact_digest = crate::calculate_file_digest(artifact_path)?;
if self.keyless {
verify_keyless(bundle, &artifact_digest, policy).await
} else if let Some(key) = &self.public_key {
verify_with_key(bundle, &artifact_digest, key, policy).await
} else {
Err(AttestationError::Verification(
"No public key provided for key-based verification".into(),
))
}
}
fn verifier_type(&self) -> &'static str {
if self.keyless {
"Cosign-Keyless"
} else {
"Cosign-Key"
}
}
}
async fn verify_keyless(
bundle: &ParsedBundle,
artifact_digest: &str,
policy: &Policy,
) -> Result<VerificationResult> {
let mut result = VerificationResult {
success: false,
slsa_level: None,
certificate_identity: None,
builder_identity: None,
messages: Vec::new(),
};
if let Some(message_signature) = &bundle.message_signature {
debug!("Verifying cosign v3 message signature bundle");
let bundle_digest = &message_signature.message_digest.digest;
let bundle_algorithm = &message_signature.message_digest.algorithm;
let bundle_digest_bytes = BASE64.decode(bundle_digest).map_err(|e| {
AttestationError::Verification(format!("Failed to decode bundle digest: {}", e))
})?;
let bundle_digest_hex = hex::encode(&bundle_digest_bytes);
if bundle_digest_hex != artifact_digest {
return Err(AttestationError::Verification(format!(
"Artifact digest mismatch: expected {}, got {}",
bundle_digest_hex, artifact_digest
)));
}
debug!(
"Artifact digest verified: {} ({})",
bundle_digest_hex, bundle_algorithm
);
if let Some(tlog_entries) = &bundle.tlog_entries {
if tlog_entries.is_empty() {
return Err(AttestationError::Verification(
"Message signature bundle missing transparency log entries".into(),
));
}
trace!("Message signature bundle has required transparency log entries");
result.success = true;
result.messages.push(format!(
"Cosign v3 message signature verified (digest: {})",
&bundle_digest_hex[..16]
));
return Ok(result);
} else {
return Err(AttestationError::Verification(
"Message signature bundle missing transparency log entries".into(),
));
}
}
if let Some(dsse_envelope) = &bundle.dsse_envelope {
if dsse_envelope.payload_type == "application/vnd.dev.sigstore.cosign"
&& dsse_envelope.payload.is_empty()
{
if let Some(_tlog_entries) = &bundle.tlog_entries {
debug!("Verifying traditional Cosign bundle with tlog entries");
if _tlog_entries.is_empty() {
return Err(AttestationError::Verification(
"Traditional Cosign bundle missing transparency log entries".into(),
));
}
trace!("Traditional Cosign bundle has required transparency log entries");
result.success = true;
result
.messages
.push("Traditional Cosign bundle verified".to_string());
return Ok(result);
} else {
return Err(AttestationError::Verification(
"Traditional Cosign bundle missing transparency log entries".into(),
));
}
}
}
if !bundle.payload.is_empty() {
let attestations = vec![crate::api::Attestation {
bundle: Some(serde_json::from_slice(&bundle.payload)?),
bundle_url: None,
}];
let artifact_path = std::path::Path::new("dummy"); crate::verify::verify_attestations(
&attestations,
artifact_path,
policy.signer_workflow.as_deref(),
)
.await?;
result.success = true;
result
.messages
.push("Cosign keyless verification successful".to_string());
} else {
return Err(AttestationError::Verification(
"No payload found for verification".into(),
));
}
Ok(result)
}
async fn verify_with_key(
bundle: &ParsedBundle,
artifact_digest: &str,
public_key: &[u8],
_policy: &Policy,
) -> Result<VerificationResult> {
let mut result = VerificationResult {
success: false,
slsa_level: None,
certificate_identity: None,
builder_identity: None,
messages: Vec::new(),
};
if let Some(dsse_envelope) = &bundle.dsse_envelope {
debug!("Verifying DSSE envelope with public key");
let signature = dsse_envelope.signatures.first().ok_or_else(|| {
AttestationError::Verification("No signatures in DSSE envelope".into())
})?;
let sig_bytes = BASE64.decode(&signature.sig).map_err(|e| {
AttestationError::Verification(format!("Failed to decode signature: {}", e))
})?;
let pae = create_dsse_pae(
&dsse_envelope.payload_type,
dsse_envelope.payload.as_bytes(),
);
verify_signature_with_key(public_key, &sig_bytes, &pae)?;
verify_payload_digest(&dsse_envelope.payload, artifact_digest)?;
result
.messages
.push("DSSE envelope signature verified with public key".to_string());
} else {
debug!("Verifying simple signature with public key");
let signature = &bundle.payload;
let message = artifact_digest.as_bytes();
verify_signature_with_key(public_key, signature, message)?;
result
.messages
.push("Simple signature verified with public key".to_string());
}
result.success = true;
result
.messages
.push(format!("Artifact digest verified: {}", artifact_digest));
Ok(result)
}
fn verify_signature_with_key(public_key: &[u8], signature: &[u8], message: &[u8]) -> Result<()> {
if public_key.len() == 32 && signature.len() == 64 {
trace!("Attempting Ed25519 verification");
if let Ok(verifying_key) = Ed25519VerifyingKey::from_bytes(public_key.try_into().unwrap()) {
let sig = Ed25519Signature::from_bytes(signature.try_into().unwrap());
return verifying_key.verify(message, &sig).map_err(|e| {
AttestationError::Verification(format!("Ed25519 verification failed: {}", e))
});
}
}
if public_key.len() == 33 || public_key.len() == 65 {
trace!("Attempting P-256 ECDSA verification");
if let Ok(verifying_key) = P256VerifyingKey::from_sec1_bytes(public_key) {
let sig = P256Signature::from_der(signature)
.or_else(|_| P256Signature::from_bytes(signature.into()))
.map_err(|e| {
AttestationError::Verification(format!(
"Failed to parse P-256 signature: {}",
e
))
})?;
return verifying_key.verify(message, &sig).map_err(|e| {
AttestationError::Verification(format!("P-256 verification failed: {}", e))
});
}
}
if public_key.starts_with(b"-----BEGIN PUBLIC KEY-----") {
trace!("Attempting to parse PEM-encoded public key");
return verify_with_pem_key(public_key, signature, message);
}
Err(AttestationError::Verification(
"Unable to determine public key type or verification failed".into(),
))
}
fn verify_with_pem_key(pem_key: &[u8], signature: &[u8], message: &[u8]) -> Result<()> {
let pem_str = std::str::from_utf8(pem_key)
.map_err(|e| AttestationError::Verification(format!("Invalid PEM encoding: {}", e)))?;
if let Ok(verifying_key) = P256VerifyingKey::from_public_key_pem(pem_str) {
trace!("Parsed P-256 public key from PEM");
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!("P-256 verification failed: {}", e))
});
}
if pem_str.contains("-----BEGIN PUBLIC KEY-----") {
let lines: Vec<&str> = pem_str
.lines()
.filter(|line| !line.starts_with("-----"))
.collect();
let pem_content = lines.join("");
if let Ok(der_bytes) = BASE64.decode(&pem_content) {
if der_bytes.len() >= 44 {
let key_start = der_bytes.len() - 32;
let key_bytes = &der_bytes[key_start..];
if let Ok(verifying_key) =
Ed25519VerifyingKey::from_bytes(key_bytes.try_into().unwrap())
{
trace!("Parsed Ed25519 public key from PEM");
if signature.len() != 64 {
return Err(AttestationError::Verification(format!(
"Invalid Ed25519 signature length: {}",
signature.len()
)));
}
let sig = Ed25519Signature::from_bytes(signature.try_into().unwrap());
return verifying_key.verify(message, &sig).map_err(|e| {
AttestationError::Verification(format!(
"Ed25519 verification failed: {}",
e
))
});
}
}
}
}
Err(AttestationError::Verification(
"Failed to parse PEM public key".into(),
))
}
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_payload_digest(payload: &str, expected_digest: &str) -> Result<()> {
let payload_bytes = BASE64
.decode(payload)
.map_err(|e| AttestationError::Verification(format!("Failed to decode payload: {}", e)))?;
let payload_json: serde_json::Value = serde_json::from_slice(&payload_bytes).map_err(|e| {
AttestationError::Verification(format!("Failed to parse payload JSON: {}", e))
})?;
if let Some(subject) = payload_json.get("subject").and_then(|s| s.as_array()) {
for subj in subject {
if let Some(digest) = subj
.get("digest")
.and_then(|d| d.get("sha256"))
.and_then(|s| s.as_str())
{
if digest == expected_digest || format!("sha256:{}", digest) == expected_digest {
trace!("Artifact digest verified in payload: {}", digest);
return Ok(());
}
}
}
}
trace!("Digest not found in payload, assuming external verification");
Ok(())
}