use a3s_box_core::error::{BoxError, Result};
#[cfg(test)]
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};
mod crypto;
mod sign;
use crypto::*;
#[cfg(test)]
use sign::{extract_pem_content, parse_pem_private_key};
pub use sign::{sign_image, SignResult};
#[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";
#[derive(Debug, Serialize, Deserialize)]
struct CosignSignatureEnvelope {
payload: String,
signature: String,
}
#[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]
#[ignore = "requires registry network access"]
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");
}
}