use a3s_box_core::error::{BoxError, Result};
use base64::Engine;
use der::Decode;
use oci_distribution::client::ClientConfig;
use oci_distribution::errors::{OciDistributionError, OciErrorCode};
use oci_distribution::secrets::RegistryAuth;
use oci_distribution::{Client, Reference};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum SignaturePolicy {
#[default]
Skip,
CosignKey {
public_key: String,
},
CosignKeyless {
issuer: String,
identity: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VerifyResult {
Verified,
Skipped,
NoSignature,
Failed(String),
}
impl VerifyResult {
pub fn is_ok(&self) -> bool {
matches!(self, Self::Verified | Self::Skipped)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(super) struct CosignPayload {
pub(super) critical: CosignCritical,
#[serde(default)]
pub(super) optional: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(super) struct CosignCritical {
pub(super) identity: CosignIdentity,
pub(super) image: CosignImage,
#[serde(rename = "type")]
pub(super) sig_type: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(super) struct CosignIdentity {
#[serde(rename = "docker-reference")]
pub(super) docker_reference: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(super) struct CosignImage {
#[serde(rename = "docker-manifest-digest")]
pub(super) docker_manifest_digest: String,
}
mod annotations {
pub const CERTIFICATE: &str = "dev.sigstore.cosign/certificate";
#[allow(dead_code)]
pub const CHAIN: &str = "dev.sigstore.cosign/chain";
#[allow(dead_code)]
pub const BUNDLE: &str = "dev.sigstore.cosign/bundle";
}
fn cosign_signature_tag(manifest_digest: &str) -> String {
let hex = manifest_digest
.strip_prefix("sha256:")
.unwrap_or(manifest_digest);
format!("sha256-{}.sig", hex)
}
struct CosignSignatureData {
layer_data: Vec<u8>,
annotations: std::collections::HashMap<String, String>,
}
async fn fetch_cosign_signature(
registry: &str,
repository: &str,
manifest_digest: &str,
) -> Result<Option<CosignSignatureData>> {
let sig_tag = cosign_signature_tag(manifest_digest);
let reference_str = format!("{}/{}:{}", registry, repository, sig_tag);
let reference: Reference = reference_str.parse().map_err(|e| BoxError::RegistryError {
registry: registry.to_string(),
message: format!("Invalid signature reference: {}", e),
})?;
let config = ClientConfig {
protocol: oci_distribution::client::ClientProtocol::Https,
..Default::default()
};
let client = Client::new(config);
match client
.pull_image_manifest(&reference, &RegistryAuth::Anonymous)
.await
{
Ok((manifest, _digest)) => {
let mut all_annotations = std::collections::HashMap::new();
if let Some(ref anns) = manifest.annotations {
all_annotations.extend(anns.clone());
}
for layer in &manifest.layers {
if let Some(ref anns) = layer.annotations {
all_annotations.extend(anns.clone());
}
}
if let Some(layer) = manifest.layers.first() {
let mut buf = Vec::new();
match client.pull_blob(&reference, layer, &mut buf).await {
Ok(()) => Ok(Some(CosignSignatureData {
layer_data: buf,
annotations: all_annotations,
})),
Err(e) => {
tracing::warn!(
reference = %reference_str,
error = %e,
"Failed to pull cosign signature blob"
);
Ok(None)
}
}
} else {
Ok(None)
}
}
Err(e) => {
let is_not_found = matches!(e, OciDistributionError::ImageManifestNotFoundError(_))
|| matches!(&e, OciDistributionError::RegistryError { envelope, .. }
if envelope.errors.iter().any(|oe| oe.code == OciErrorCode::ManifestUnknown));
if is_not_found {
Ok(None)
} else {
tracing::warn!(
reference = %reference_str,
error = %e,
"Registry error while fetching cosign signature"
);
Err(BoxError::RegistryError {
registry: registry.to_string(),
message: format!("Failed to fetch cosign signature: {}", e),
})
}
}
}
}
fn verify_cosign_payload(payload: &[u8], manifest_digest: &str) -> Result<CosignPayload> {
let cosign_payload: CosignPayload =
serde_json::from_slice(payload).map_err(|e| BoxError::RegistryError {
registry: String::new(),
message: format!("Invalid cosign payload: {}", e),
})?;
if cosign_payload.critical.image.docker_manifest_digest != manifest_digest {
return Err(BoxError::RegistryError {
registry: String::new(),
message: format!(
"Signature digest mismatch: expected {}, got {}",
manifest_digest, cosign_payload.critical.image.docker_manifest_digest
),
});
}
Ok(cosign_payload)
}
pub async fn verify_image_signature(
policy: &SignaturePolicy,
registry: &str,
repository: &str,
manifest_digest: &str,
) -> VerifyResult {
match policy {
SignaturePolicy::Skip => VerifyResult::Skipped,
SignaturePolicy::CosignKey { public_key } => {
verify_cosign_key(public_key, registry, repository, manifest_digest).await
}
SignaturePolicy::CosignKeyless { issuer, identity } => {
verify_cosign_keyless(issuer, identity, registry, repository, manifest_digest).await
}
}
}
async fn verify_cosign_key(
public_key_path: &str,
registry: &str,
repository: &str,
manifest_digest: &str,
) -> VerifyResult {
let pem_bytes = match std::fs::read(public_key_path) {
Ok(b) => b,
Err(e) => {
return VerifyResult::Failed(format!(
"Failed to read public key file '{}': {}",
public_key_path, e
));
}
};
let verifying_key = match parse_pem_public_key(&pem_bytes) {
Ok(k) => k,
Err(e) => {
return VerifyResult::Failed(format!("Failed to parse public key: {}", e));
}
};
let sig_data = match fetch_cosign_signature(registry, repository, manifest_digest).await {
Ok(Some(data)) => data,
Ok(None) => return VerifyResult::NoSignature,
Err(e) => {
return VerifyResult::Failed(format!("Failed to fetch signature: {}", e));
}
};
let sig_envelope: CosignSignatureEnvelope = match serde_json::from_slice(&sig_data.layer_data) {
Ok(e) => e,
Err(e) => {
return VerifyResult::Failed(format!(
"Failed to parse cosign signature envelope: {}",
e
));
}
};
let payload_bytes = match base64_decode(&sig_envelope.payload) {
Ok(b) => b,
Err(e) => {
return VerifyResult::Failed(format!("Failed to decode signature payload: {}", e));
}
};
let signature_bytes = match base64_decode(&sig_envelope.signature) {
Ok(b) => b,
Err(e) => {
return VerifyResult::Failed(format!("Failed to decode signature bytes: {}", e));
}
};
if let Err(e) = verify_ecdsa_p256(&verifying_key, &payload_bytes, &signature_bytes) {
return VerifyResult::Failed(format!("Signature verification failed: {}", e));
}
match verify_cosign_payload(&payload_bytes, manifest_digest) {
Ok(_) => VerifyResult::Verified,
Err(e) => VerifyResult::Failed(format!("Payload validation failed: {}", e)),
}
}
async fn verify_cosign_keyless(
expected_issuer: &str,
expected_identity: &str,
registry: &str,
repository: &str,
manifest_digest: &str,
) -> VerifyResult {
let sig_data = match fetch_cosign_signature(registry, repository, manifest_digest).await {
Ok(Some(data)) => data,
Ok(None) => return VerifyResult::NoSignature,
Err(e) => {
return VerifyResult::Failed(format!("Failed to fetch signature: {}", e));
}
};
let cert_pem = match sig_data.annotations.get(annotations::CERTIFICATE) {
Some(c) => c.clone(),
None => {
return VerifyResult::Failed(
"Keyless signature missing Fulcio certificate annotation \
(dev.sigstore.cosign/certificate)"
.to_string(),
);
}
};
let cert_der = match pem_to_der(&cert_pem) {
Ok(d) => d,
Err(e) => {
return VerifyResult::Failed(format!("Failed to parse Fulcio certificate PEM: {}", e));
}
};
let cert = match x509_cert::Certificate::from_der(&cert_der) {
Ok(c) => c,
Err(e) => {
return VerifyResult::Failed(format!("Failed to parse Fulcio certificate DER: {}", e));
}
};
if let Err(e) = verify_fulcio_issuer(&cert, expected_issuer) {
return VerifyResult::Failed(format!("Fulcio issuer mismatch: {}", e));
}
if let Err(e) = verify_fulcio_identity(&cert, expected_identity) {
return VerifyResult::Failed(format!("Fulcio identity mismatch: {}", e));
}
let pub_key_bytes = match extract_cert_public_key(&cert) {
Ok(k) => k,
Err(e) => {
return VerifyResult::Failed(format!(
"Failed to extract public key from Fulcio cert: {}",
e
));
}
};
let sig_envelope: CosignSignatureEnvelope = match serde_json::from_slice(&sig_data.layer_data) {
Ok(e) => e,
Err(e) => {
return VerifyResult::Failed(format!(
"Failed to parse cosign signature envelope: {}",
e
));
}
};
let payload_bytes = match base64_decode(&sig_envelope.payload) {
Ok(b) => b,
Err(e) => {
return VerifyResult::Failed(format!("Failed to decode signature payload: {}", e));
}
};
let signature_bytes = match base64_decode(&sig_envelope.signature) {
Ok(b) => b,
Err(e) => {
return VerifyResult::Failed(format!("Failed to decode signature bytes: {}", e));
}
};
if let Err(e) = verify_ecdsa_p256(&pub_key_bytes, &payload_bytes, &signature_bytes) {
return VerifyResult::Failed(format!("Keyless signature verification failed: {}", e));
}
match verify_cosign_payload(&payload_bytes, manifest_digest) {
Ok(_) => {
tracing::info!(
digest = %manifest_digest,
issuer = %expected_issuer,
identity = %expected_identity,
"Cosign keyless signature verified"
);
VerifyResult::Verified
}
Err(e) => VerifyResult::Failed(format!("Payload validation failed: {}", e)),
}
}
const FULCIO_ISSUER_OID: &str = "1.3.6.1.4.1.57264.1.1";
fn verify_fulcio_issuer(
cert: &x509_cert::Certificate,
expected_issuer: &str,
) -> std::result::Result<(), String> {
let issuer_oid = der::asn1::ObjectIdentifier::new(FULCIO_ISSUER_OID)
.map_err(|e| format!("Failed to construct Fulcio issuer OID: {}", e))?;
let extensions = cert
.tbs_certificate
.extensions
.as_ref()
.ok_or("Certificate has no extensions")?;
for ext in extensions.iter() {
if ext.extn_id == issuer_oid {
let issuer_value =
if let Ok(utf8) = der::asn1::Utf8StringRef::from_der(ext.extn_value.as_bytes()) {
utf8.to_string()
} else {
String::from_utf8(ext.extn_value.as_bytes().to_vec())
.map_err(|e| format!("Fulcio issuer extension is not valid UTF-8: {}", e))?
};
if issuer_value == expected_issuer {
return Ok(());
} else {
return Err(format!(
"expected '{}', got '{}'",
expected_issuer, issuer_value
));
}
}
}
Err("Fulcio issuer extension (OID 1.3.6.1.4.1.57264.1.1) not found in certificate".into())
}
fn verify_fulcio_identity(
cert: &x509_cert::Certificate,
expected_identity: &str,
) -> std::result::Result<(), String> {
use x509_cert::ext::pkix::SubjectAltName;
let extensions = cert
.tbs_certificate
.extensions
.as_ref()
.ok_or("Certificate has no extensions")?;
let san_oid = der::asn1::ObjectIdentifier::new("2.5.29.17")
.map_err(|e| format!("Failed to construct SAN OID: {}", e))?;
for ext in extensions.iter() {
if ext.extn_id == san_oid {
let san = SubjectAltName::from_der(ext.extn_value.as_bytes())
.map_err(|e| format!("Failed to parse SAN extension: {}", e))?;
for name in san.0.iter() {
match name {
x509_cert::ext::pkix::name::GeneralName::Rfc822Name(email) => {
let email_str: &str = email.as_ref();
if email_str == expected_identity {
return Ok(());
}
}
x509_cert::ext::pkix::name::GeneralName::UniformResourceIdentifier(uri) => {
let uri_str: &str = uri.as_ref();
if uri_str == expected_identity {
return Ok(());
}
}
_ => continue,
}
}
let found: Vec<String> = san
.0
.iter()
.filter_map(|n| match n {
x509_cert::ext::pkix::name::GeneralName::Rfc822Name(e) => Some(e.to_string()),
x509_cert::ext::pkix::name::GeneralName::UniformResourceIdentifier(u) => {
Some(u.to_string())
}
_ => None,
})
.collect();
return Err(format!(
"expected '{}', found [{}]",
expected_identity,
found.join(", ")
));
}
}
Err("Subject Alternative Name extension not found in certificate".into())
}
fn extract_cert_public_key(cert: &x509_cert::Certificate) -> std::result::Result<Vec<u8>, String> {
cert.tbs_certificate
.subject_public_key_info
.subject_public_key
.as_bytes()
.map(|b| b.to_vec())
.ok_or_else(|| "Failed to extract public key bytes from certificate".to_string())
}
fn pem_to_der(pem_str: &str) -> std::result::Result<Vec<u8>, String> {
let begin = pem_str
.find("-----BEGIN ")
.ok_or("No PEM begin marker found")?;
let begin_end = pem_str[begin..]
.find("-----\n")
.or_else(|| pem_str[begin..].find("-----\r\n"))
.ok_or("Malformed PEM begin marker")?
+ begin
+ 6;
let end = pem_str[begin_end..]
.find("-----END ")
.ok_or("No PEM end marker found")?
+ begin_end;
let b64: String = pem_str[begin_end..end]
.chars()
.filter(|c| !c.is_whitespace())
.collect();
base64_decode(&b64).map_err(|e| format!("Failed to decode PEM base64: {}", e))
}
#[derive(Debug, Serialize, Deserialize)]
struct CosignSignatureEnvelope {
payload: String,
signature: String,
}
fn parse_pem_public_key(pem_bytes: &[u8]) -> std::result::Result<Vec<u8>, String> {
let pem_str = std::str::from_utf8(pem_bytes)
.map_err(|e| format!("PEM file is not valid UTF-8: {}", e))?;
let (begin_marker, end_marker) = if pem_str.contains("BEGIN PUBLIC KEY") {
("-----BEGIN PUBLIC KEY-----", "-----END PUBLIC KEY-----")
} else if pem_str.contains("BEGIN EC PUBLIC KEY") {
(
"-----BEGIN EC PUBLIC KEY-----",
"-----END EC PUBLIC KEY-----",
)
} else {
return Err("Unsupported PEM format: expected PUBLIC KEY or EC PUBLIC KEY".to_string());
};
let start = pem_str
.find(begin_marker)
.ok_or("Missing PEM begin marker")?
+ begin_marker.len();
let end = pem_str.find(end_marker).ok_or("Missing PEM end marker")?;
let b64: String = pem_str[start..end]
.chars()
.filter(|c| !c.is_whitespace())
.collect();
let der_bytes =
base64_decode(&b64).map_err(|e| format!("Failed to decode PEM base64: {}", e))?;
if begin_marker.contains("BEGIN PUBLIC KEY") {
extract_spki_public_key(&der_bytes)
} else {
Ok(der_bytes)
}
}
fn extract_spki_public_key(der: &[u8]) -> std::result::Result<Vec<u8>, String> {
use spki::SubjectPublicKeyInfo;
let spki =
SubjectPublicKeyInfo::<der::asn1::AnyRef<'_>, der::asn1::BitStringRef<'_>>::from_der(der)
.map_err(|e| format!("Failed to parse SPKI: {}", e))?;
spki.subject_public_key
.as_bytes()
.map(|b| b.to_vec())
.ok_or_else(|| "Failed to extract public key bytes from SPKI".to_string())
}
fn verify_ecdsa_p256(
public_key_bytes: &[u8],
message: &[u8],
signature: &[u8],
) -> std::result::Result<(), String> {
use p256::ecdsa::{signature::Verifier, Signature, VerifyingKey};
let verifying_key = VerifyingKey::from_sec1_bytes(public_key_bytes)
.map_err(|e| format!("Invalid P-256 public key: {}", e))?;
let result = if let Ok(sig) = p256::ecdsa::DerSignature::from_bytes(signature) {
verifying_key.verify(message, &sig)
} else if signature.len() == 64 {
let sig = Signature::from_slice(signature)
.map_err(|e| format!("Invalid P-256 signature: {}", e))?;
verifying_key.verify(message, &sig)
} else {
return Err(format!(
"Unrecognized signature format ({} bytes)",
signature.len()
));
};
result.map_err(|_| "ECDSA P-256 signature verification failed".to_string())
}
fn base64_decode(input: &str) -> std::result::Result<Vec<u8>, String> {
base64::engine::general_purpose::STANDARD
.decode(input.trim())
.map_err(|e| format!("base64 decode error: {}", e))
}
fn base64_encode(input: &[u8]) -> String {
base64::engine::general_purpose::STANDARD.encode(input)
}
#[derive(Debug, Clone)]
pub struct SignResult {
pub signature_tag: String,
}
pub async fn sign_image(
private_key_path: &str,
registry: &str,
repository: &str,
manifest_digest: &str,
docker_reference: &str,
) -> Result<SignResult> {
use p256::ecdsa::signature::Signer;
let pem_bytes = std::fs::read(private_key_path).map_err(|e| {
BoxError::OciImageError(format!(
"Failed to read signing key '{}': {}",
private_key_path, e
))
})?;
let signing_key = parse_pem_private_key(&pem_bytes)
.map_err(|e| BoxError::OciImageError(format!("Failed to parse signing key: {}", e)))?;
let payload = CosignPayload {
critical: CosignCritical {
identity: CosignIdentity {
docker_reference: docker_reference.to_string(),
},
image: CosignImage {
docker_manifest_digest: manifest_digest.to_string(),
},
sig_type: "cosign container image signature".to_string(),
},
optional: serde_json::json!({}),
};
let payload_bytes = serde_json::to_vec(&payload).map_err(|e| {
BoxError::SerializationError(format!("Failed to serialize cosign payload: {}", e))
})?;
let signature: p256::ecdsa::DerSignature = signing_key.sign(&payload_bytes);
let envelope = CosignSignatureEnvelope {
payload: base64_encode(&payload_bytes),
signature: base64_encode(signature.as_bytes()),
};
let envelope_bytes = serde_json::to_vec(&envelope).map_err(|e| {
BoxError::SerializationError(format!("Failed to serialize signature envelope: {}", e))
})?;
let sig_tag = cosign_signature_tag(manifest_digest);
let sig_reference_str = format!("{}/{}:{}", registry, repository, sig_tag);
let sig_reference: Reference =
sig_reference_str
.parse()
.map_err(|e| BoxError::RegistryError {
registry: registry.to_string(),
message: format!("Invalid signature reference: {}", e),
})?;
let config = oci_distribution::client::ClientConfig {
protocol: oci_distribution::client::ClientProtocol::Https,
..Default::default()
};
let client = Client::new(config);
let sig_layer = oci_distribution::client::ImageLayer::new(
envelope_bytes,
"application/vnd.dev.cosign.simplesigning.v1+json".to_string(),
None,
);
let sig_config = oci_distribution::client::Config::new(
b"{}".to_vec(),
"application/vnd.oci.image.config.v1+json".to_string(),
None,
);
client
.push(
&sig_reference,
&[sig_layer],
sig_config,
&RegistryAuth::Anonymous,
None,
)
.await
.map_err(|e| BoxError::RegistryError {
registry: registry.to_string(),
message: format!("Failed to push signature artifact: {}", e),
})?;
tracing::info!(
digest = %manifest_digest,
signature_tag = %sig_tag,
"Image signed and signature pushed"
);
Ok(SignResult {
signature_tag: sig_tag,
})
}
fn parse_pem_private_key(pem_bytes: &[u8]) -> std::result::Result<p256::ecdsa::SigningKey, String> {
let pem_str = std::str::from_utf8(pem_bytes)
.map_err(|e| format!("PEM file is not valid UTF-8: {}", e))?;
let der_bytes = if pem_str.contains("BEGIN EC PRIVATE KEY") {
extract_pem_content(
pem_str,
"-----BEGIN EC PRIVATE KEY-----",
"-----END EC PRIVATE KEY-----",
)?
} else if pem_str.contains("BEGIN PRIVATE KEY") {
extract_pem_content(
pem_str,
"-----BEGIN PRIVATE KEY-----",
"-----END PRIVATE KEY-----",
)?
} else {
return Err("Unsupported PEM format: expected EC PRIVATE KEY or PRIVATE KEY".to_string());
};
if let Ok(key) = p256::SecretKey::from_sec1_der(&der_bytes) {
return Ok(p256::ecdsa::SigningKey::from(key));
}
use p256::pkcs8::DecodePrivateKey;
p256::SecretKey::from_pkcs8_der(&der_bytes)
.map(p256::ecdsa::SigningKey::from)
.map_err(|e| format!("Failed to parse P-256 private key: {}", e))
}
fn extract_pem_content(
pem_str: &str,
begin_marker: &str,
end_marker: &str,
) -> std::result::Result<Vec<u8>, String> {
let start = pem_str
.find(begin_marker)
.ok_or("Missing PEM begin marker")?
+ begin_marker.len();
let end = pem_str.find(end_marker).ok_or("Missing PEM end marker")?;
let b64: String = pem_str[start..end]
.chars()
.filter(|c| !c.is_whitespace())
.collect();
base64_decode(&b64).map_err(|e| format!("Failed to decode PEM base64: {}", e))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_signature_policy_default_is_skip() {
assert_eq!(SignaturePolicy::default(), SignaturePolicy::Skip);
}
#[test]
fn test_signature_policy_serde_skip() {
let policy = SignaturePolicy::Skip;
let json = serde_json::to_string(&policy).unwrap();
let parsed: SignaturePolicy = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, SignaturePolicy::Skip);
}
#[test]
fn test_signature_policy_serde_cosign_key() {
let policy = SignaturePolicy::CosignKey {
public_key: "/path/to/cosign.pub".to_string(),
};
let json = serde_json::to_string(&policy).unwrap();
let parsed: SignaturePolicy = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, policy);
}
#[test]
fn test_signature_policy_serde_cosign_keyless() {
let policy = SignaturePolicy::CosignKeyless {
issuer: "https://accounts.google.com".to_string(),
identity: "user@example.com".to_string(),
};
let json = serde_json::to_string(&policy).unwrap();
let parsed: SignaturePolicy = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, policy);
}
#[test]
fn test_verify_result_is_ok() {
assert!(VerifyResult::Verified.is_ok());
assert!(VerifyResult::Skipped.is_ok());
assert!(!VerifyResult::NoSignature.is_ok());
assert!(!VerifyResult::Failed("err".to_string()).is_ok());
}
#[test]
fn test_verify_result_debug() {
let r = VerifyResult::Verified;
assert!(format!("{:?}", r).contains("Verified"));
}
#[test]
fn test_cosign_signature_tag_with_prefix() {
let tag = cosign_signature_tag("sha256:abc123def456");
assert_eq!(tag, "sha256-abc123def456.sig");
}
#[test]
fn test_cosign_signature_tag_without_prefix() {
let tag = cosign_signature_tag("abc123def456");
assert_eq!(tag, "sha256-abc123def456.sig");
}
#[test]
fn test_verify_cosign_payload_valid() {
let digest = "sha256:abc123";
let payload = serde_json::json!({
"critical": {
"identity": {
"docker-reference": "docker.io/library/alpine"
},
"image": {
"docker-manifest-digest": digest
},
"type": "cosign container image signature"
},
"optional": {}
});
let bytes = serde_json::to_vec(&payload).unwrap();
let result = verify_cosign_payload(&bytes, digest);
assert!(result.is_ok());
let p = result.unwrap();
assert_eq!(p.critical.image.docker_manifest_digest, digest);
assert_eq!(
p.critical.identity.docker_reference,
"docker.io/library/alpine"
);
}
#[test]
fn test_verify_cosign_payload_digest_mismatch() {
let payload = serde_json::json!({
"critical": {
"identity": {
"docker-reference": "docker.io/library/alpine"
},
"image": {
"docker-manifest-digest": "sha256:wrong"
},
"type": "cosign container image signature"
},
"optional": {}
});
let bytes = serde_json::to_vec(&payload).unwrap();
let result = verify_cosign_payload(&bytes, "sha256:expected");
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("mismatch"));
}
#[test]
fn test_verify_cosign_payload_invalid_json() {
let result = verify_cosign_payload(b"not json", "sha256:abc");
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Invalid cosign payload"));
}
#[tokio::test]
async fn test_verify_image_signature_skip() {
let result = verify_image_signature(
&SignaturePolicy::Skip,
"docker.io",
"library/alpine",
"sha256:abc",
)
.await;
assert_eq!(result, VerifyResult::Skipped);
}
#[tokio::test]
async fn test_verify_image_signature_cosign_key_missing_file() {
let policy = SignaturePolicy::CosignKey {
public_key: "/nonexistent/cosign.pub".to_string(),
};
let result =
verify_image_signature(&policy, "docker.io", "library/alpine", "sha256:abc").await;
match result {
VerifyResult::Failed(msg) => assert!(msg.contains("Failed to read public key")),
other => panic!("Expected Failed, got {:?}", other),
}
}
#[tokio::test]
async fn test_verify_image_signature_cosign_keyless_no_signature() {
let policy = SignaturePolicy::CosignKeyless {
issuer: "https://accounts.google.com".to_string(),
identity: "user@example.com".to_string(),
};
let result =
verify_image_signature(&policy, "docker.io", "library/alpine", "sha256:abc").await;
assert!(!result.is_ok());
}
fn generate_test_p256_key() -> (p256::ecdsa::SigningKey, Vec<u8>, String) {
use p256::ecdsa::SigningKey;
let signing_key = SigningKey::random(&mut rand::thread_rng());
let verifying_key = signing_key.verifying_key();
let pub_bytes = verifying_key.to_encoded_point(false).as_bytes().to_vec();
let spki_der = build_p256_spki_der(&pub_bytes);
let b64 = base64_encode_for_test(&spki_der);
let pem = format!(
"-----BEGIN PUBLIC KEY-----\n{}\n-----END PUBLIC KEY-----\n",
b64
);
(signing_key, pub_bytes, pem)
}
fn build_p256_spki_der(pub_key_bytes: &[u8]) -> Vec<u8> {
let ec_oid: &[u8] = &[0x06, 0x07, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01];
let p256_oid: &[u8] = &[0x06, 0x08, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07];
let alg_content_len = ec_oid.len() + p256_oid.len();
let mut alg_id = vec![0x30];
encode_der_length(&mut alg_id, alg_content_len);
alg_id.extend_from_slice(ec_oid);
alg_id.extend_from_slice(p256_oid);
let bit_string_len = 1 + pub_key_bytes.len(); let mut bit_string = vec![0x03];
encode_der_length(&mut bit_string, bit_string_len);
bit_string.push(0x00); bit_string.extend_from_slice(pub_key_bytes);
let total_content_len = alg_id.len() + bit_string.len();
let mut spki = vec![0x30];
encode_der_length(&mut spki, total_content_len);
spki.extend_from_slice(&alg_id);
spki.extend_from_slice(&bit_string);
spki
}
fn encode_der_length(buf: &mut Vec<u8>, len: usize) {
if len < 0x80 {
buf.push(len as u8);
} else if len < 0x100 {
buf.push(0x81);
buf.push(len as u8);
} else {
buf.push(0x82);
buf.push((len >> 8) as u8);
buf.push(len as u8);
}
}
fn base64_encode_for_test(data: &[u8]) -> String {
base64::engine::general_purpose::STANDARD.encode(data)
}
#[test]
fn test_parse_pem_public_key_spki() {
let (_sk, expected_pub, pem) = generate_test_p256_key();
let parsed = parse_pem_public_key(pem.as_bytes()).unwrap();
assert_eq!(parsed, expected_pub);
}
#[test]
fn test_parse_pem_public_key_invalid() {
let result = parse_pem_public_key(b"not a pem file");
assert!(result.is_err());
}
#[test]
fn test_verify_ecdsa_p256_valid_signature() {
use p256::ecdsa::{signature::Signer, SigningKey};
let signing_key = SigningKey::random(&mut rand::thread_rng());
let verifying_key = signing_key.verifying_key();
let pub_bytes = verifying_key.to_encoded_point(false).as_bytes().to_vec();
let message = b"test payload for cosign verification";
let sig: p256::ecdsa::DerSignature = signing_key.sign(message);
let result = verify_ecdsa_p256(&pub_bytes, message, sig.as_bytes());
assert!(result.is_ok());
}
#[test]
fn test_verify_ecdsa_p256_wrong_key_rejects() {
use p256::ecdsa::{signature::Signer, SigningKey};
let signing_key = SigningKey::random(&mut rand::thread_rng());
let wrong_key = SigningKey::random(&mut rand::thread_rng());
let wrong_pub = wrong_key
.verifying_key()
.to_encoded_point(false)
.as_bytes()
.to_vec();
let message = b"test payload";
let sig: p256::ecdsa::DerSignature = signing_key.sign(message);
let result = verify_ecdsa_p256(&wrong_pub, message, sig.as_bytes());
assert!(result.is_err());
}
#[test]
fn test_verify_ecdsa_p256_tampered_message_rejects() {
use p256::ecdsa::{signature::Signer, SigningKey};
let signing_key = SigningKey::random(&mut rand::thread_rng());
let pub_bytes = signing_key
.verifying_key()
.to_encoded_point(false)
.as_bytes()
.to_vec();
let message = b"original message";
let sig: p256::ecdsa::DerSignature = signing_key.sign(message);
let result = verify_ecdsa_p256(&pub_bytes, b"tampered message", sig.as_bytes());
assert!(result.is_err());
}
#[test]
fn test_verify_ecdsa_p256_fixed_size_signature() {
use p256::ecdsa::{signature::Signer, Signature, SigningKey};
let signing_key = SigningKey::random(&mut rand::thread_rng());
let pub_bytes = signing_key
.verifying_key()
.to_encoded_point(false)
.as_bytes()
.to_vec();
let message = b"test with fixed-size sig";
let sig: Signature = signing_key.sign(message);
assert_eq!(sig.to_bytes().len(), 64);
let result = verify_ecdsa_p256(&pub_bytes, message, &sig.to_bytes());
assert!(result.is_ok());
}
#[test]
fn test_cosign_key_end_to_end_with_temp_file() {
use p256::ecdsa::signature::Signer;
let (signing_key, _pub_bytes, pem) = generate_test_p256_key();
let dir = tempfile::tempdir().unwrap();
let key_path = dir.path().join("cosign.pub");
std::fs::write(&key_path, &pem).unwrap();
let digest = "sha256:abc123def456";
let payload = serde_json::json!({
"critical": {
"identity": { "docker-reference": "docker.io/library/alpine" },
"image": { "docker-manifest-digest": digest },
"type": "cosign container image signature"
},
"optional": {}
});
let payload_bytes = serde_json::to_vec(&payload).unwrap();
let sig: p256::ecdsa::DerSignature = signing_key.sign(&payload_bytes);
let envelope = serde_json::json!({
"payload": base64_encode_for_test(&payload_bytes),
"signature": base64_encode_for_test(sig.as_bytes()),
});
let envelope_bytes = serde_json::to_vec(&envelope).unwrap();
let env: CosignSignatureEnvelope = serde_json::from_slice(&envelope_bytes).unwrap();
let decoded_payload = base64_decode(&env.payload).unwrap();
let decoded_sig = base64_decode(&env.signature).unwrap();
let pem_bytes = std::fs::read(&key_path).unwrap();
let pub_key = parse_pem_public_key(&pem_bytes).unwrap();
assert!(verify_ecdsa_p256(&pub_key, &decoded_payload, &decoded_sig).is_ok());
assert!(verify_cosign_payload(&decoded_payload, digest).is_ok());
}
#[test]
fn test_cosign_payload_serde_roundtrip() {
let payload = CosignPayload {
critical: CosignCritical {
identity: CosignIdentity {
docker_reference: "ghcr.io/myorg/myimage".to_string(),
},
image: CosignImage {
docker_manifest_digest: "sha256:deadbeef".to_string(),
},
sig_type: "cosign container image signature".to_string(),
},
optional: serde_json::json!({"creator": "a3s-box"}),
};
let json = serde_json::to_string(&payload).unwrap();
let parsed: CosignPayload = serde_json::from_str(&json).unwrap();
assert_eq!(
parsed.critical.image.docker_manifest_digest,
"sha256:deadbeef"
);
assert_eq!(
parsed.critical.identity.docker_reference,
"ghcr.io/myorg/myimage"
);
}
#[test]
fn test_pem_to_der_valid() {
let data = vec![0x30, 0x03, 0x01, 0x01, 0xFF]; let b64 = base64_encode_for_test(&data);
let pem = format!(
"-----BEGIN CERTIFICATE-----\n{}\n-----END CERTIFICATE-----\n",
b64
);
let der = pem_to_der(&pem).unwrap();
assert_eq!(der, data);
}
#[test]
fn test_pem_to_der_no_markers() {
let result = pem_to_der("not a pem");
assert!(result.is_err());
assert!(result.unwrap_err().contains("No PEM begin marker"));
}
#[test]
fn test_annotation_keys() {
assert_eq!(annotations::CERTIFICATE, "dev.sigstore.cosign/certificate");
assert_eq!(annotations::CHAIN, "dev.sigstore.cosign/chain");
assert_eq!(annotations::BUNDLE, "dev.sigstore.cosign/bundle");
}
#[test]
fn test_fulcio_issuer_oid_is_valid() {
let oid = der::asn1::ObjectIdentifier::new(FULCIO_ISSUER_OID);
assert!(oid.is_ok());
}
#[test]
fn test_extract_cert_public_key_from_self_signed() {
let key_pair = rcgen::KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256).unwrap();
let params = rcgen::CertificateParams::new(vec!["test".to_string()]).unwrap();
let cert = params.self_signed(&key_pair).unwrap();
let cert_der = cert.der();
let parsed = x509_cert::Certificate::from_der(cert_der).unwrap();
let extracted = extract_cert_public_key(&parsed);
assert!(extracted.is_ok());
assert_eq!(extracted.unwrap().len(), 65);
}
#[test]
fn test_base64_encode_roundtrip() {
let data = b"hello cosign signing";
let encoded = base64_encode(data);
let decoded = base64_decode(&encoded).unwrap();
assert_eq!(decoded, data);
}
#[test]
fn test_cosign_signature_envelope_serde_roundtrip() {
let envelope = CosignSignatureEnvelope {
payload: base64_encode(b"test payload"),
signature: base64_encode(b"test signature"),
};
let json = serde_json::to_string(&envelope).unwrap();
let parsed: CosignSignatureEnvelope = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.payload, envelope.payload);
assert_eq!(parsed.signature, envelope.signature);
}
#[test]
fn test_parse_pem_private_key_sec1() {
let signing_key = p256::ecdsa::SigningKey::random(&mut rand::thread_rng());
let secret_key = signing_key.as_nonzero_scalar();
let sec1_der = secret_key.to_bytes();
use p256::pkcs8::EncodePrivateKey;
let pkcs8_der = p256::SecretKey::from(signing_key.clone())
.to_pkcs8_der()
.unwrap();
let b64 = base64_encode(pkcs8_der.as_bytes());
let pem = format!(
"-----BEGIN PRIVATE KEY-----\n{}\n-----END PRIVATE KEY-----\n",
b64
);
let parsed = parse_pem_private_key(pem.as_bytes());
assert!(parsed.is_ok());
use p256::ecdsa::{signature::Signer, signature::Verifier};
let msg = b"test message";
let sig: p256::ecdsa::DerSignature = parsed.unwrap().sign(msg);
assert!(signing_key.verifying_key().verify(msg, &sig).is_ok());
}
#[test]
fn test_parse_pem_private_key_invalid() {
let result = parse_pem_private_key(b"not a pem file");
assert!(result.is_err());
}
#[test]
fn test_sign_and_verify_roundtrip() {
use p256::ecdsa::{signature::Signer, SigningKey};
let signing_key = SigningKey::random(&mut rand::thread_rng());
let pub_bytes = signing_key
.verifying_key()
.to_encoded_point(false)
.as_bytes()
.to_vec();
let digest = "sha256:deadbeef1234";
let payload = CosignPayload {
critical: CosignCritical {
identity: CosignIdentity {
docker_reference: "ghcr.io/myorg/myimage:latest".to_string(),
},
image: CosignImage {
docker_manifest_digest: digest.to_string(),
},
sig_type: "cosign container image signature".to_string(),
},
optional: serde_json::json!({}),
};
let payload_bytes = serde_json::to_vec(&payload).unwrap();
let sig: p256::ecdsa::DerSignature = signing_key.sign(&payload_bytes);
let envelope = CosignSignatureEnvelope {
payload: base64_encode(&payload_bytes),
signature: base64_encode(sig.as_bytes()),
};
let envelope_bytes = serde_json::to_vec(&envelope).unwrap();
let parsed_env: CosignSignatureEnvelope = serde_json::from_slice(&envelope_bytes).unwrap();
let decoded_payload = base64_decode(&parsed_env.payload).unwrap();
let decoded_sig = base64_decode(&parsed_env.signature).unwrap();
assert!(verify_ecdsa_p256(&pub_bytes, &decoded_payload, &decoded_sig).is_ok());
assert!(verify_cosign_payload(&decoded_payload, digest).is_ok());
}
#[test]
fn test_extract_pem_content_valid() {
let data = vec![1, 2, 3, 4, 5];
let b64 = base64_encode(&data);
let pem = format!("-----BEGIN TEST-----\n{}\n-----END TEST-----\n", b64);
let result = extract_pem_content(&pem, "-----BEGIN TEST-----", "-----END TEST-----");
assert!(result.is_ok());
assert_eq!(result.unwrap(), data);
}
#[test]
fn test_extract_pem_content_missing_begin() {
let result = extract_pem_content("no markers", "-----BEGIN X-----", "-----END X-----");
assert!(result.is_err());
}
#[test]
fn test_sign_result_structure() {
let result = SignResult {
signature_tag: "sha256-abc123.sig".to_string(),
};
assert_eq!(result.signature_tag, "sha256-abc123.sig");
}
}