use a3s_box_core::error::{BoxError, Result};
use oci_distribution::secrets::RegistryAuth;
use oci_distribution::{Client, Reference};
use super::crypto::{base64_decode, base64_encode};
use super::{
cosign_signature_tag, CosignCritical, CosignIdentity, CosignImage, CosignPayload,
CosignSignatureEnvelope,
};
#[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,
})
}
pub(super) 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))
}
pub(super) 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))
}