a3s_box_runtime/oci/signing/
sign.rs1use a3s_box_core::error::{BoxError, Result};
4use oci_distribution::secrets::RegistryAuth;
5use oci_distribution::{Client, Reference};
6
7use super::crypto::{base64_decode, base64_encode};
8use super::{
9 cosign_signature_tag, CosignCritical, CosignIdentity, CosignImage, CosignPayload,
10 CosignSignatureEnvelope,
11};
12
13#[derive(Debug, Clone)]
15pub struct SignResult {
16 pub signature_tag: String,
18}
19
20pub async fn sign_image(
32 private_key_path: &str,
33 registry: &str,
34 repository: &str,
35 manifest_digest: &str,
36 docker_reference: &str,
37) -> Result<SignResult> {
38 use p256::ecdsa::signature::Signer;
39
40 let pem_bytes = std::fs::read(private_key_path).map_err(|e| {
42 BoxError::OciImageError(format!(
43 "Failed to read signing key '{}': {}",
44 private_key_path, e
45 ))
46 })?;
47 let signing_key = parse_pem_private_key(&pem_bytes)
48 .map_err(|e| BoxError::OciImageError(format!("Failed to parse signing key: {}", e)))?;
49
50 let payload = CosignPayload {
52 critical: CosignCritical {
53 identity: CosignIdentity {
54 docker_reference: docker_reference.to_string(),
55 },
56 image: CosignImage {
57 docker_manifest_digest: manifest_digest.to_string(),
58 },
59 sig_type: "cosign container image signature".to_string(),
60 },
61 optional: serde_json::json!({}),
62 };
63 let payload_bytes = serde_json::to_vec(&payload).map_err(|e| {
64 BoxError::SerializationError(format!("Failed to serialize cosign payload: {}", e))
65 })?;
66
67 let signature: p256::ecdsa::DerSignature = signing_key.sign(&payload_bytes);
69
70 let envelope = CosignSignatureEnvelope {
72 payload: base64_encode(&payload_bytes),
73 signature: base64_encode(signature.as_bytes()),
74 };
75 let envelope_bytes = serde_json::to_vec(&envelope).map_err(|e| {
76 BoxError::SerializationError(format!("Failed to serialize signature envelope: {}", e))
77 })?;
78
79 let sig_tag = cosign_signature_tag(manifest_digest);
81 let sig_reference_str = format!("{}/{}:{}", registry, repository, sig_tag);
82
83 let sig_reference: Reference =
84 sig_reference_str
85 .parse()
86 .map_err(|e| BoxError::RegistryError {
87 registry: registry.to_string(),
88 message: format!("Invalid signature reference: {}", e),
89 })?;
90
91 let config = oci_distribution::client::ClientConfig {
92 protocol: oci_distribution::client::ClientProtocol::Https,
93 ..Default::default()
94 };
95 let client = Client::new(config);
96
97 let sig_layer = oci_distribution::client::ImageLayer::new(
99 envelope_bytes,
100 "application/vnd.dev.cosign.simplesigning.v1+json".to_string(),
101 None,
102 );
103
104 let sig_config = oci_distribution::client::Config::new(
106 b"{}".to_vec(),
107 "application/vnd.oci.image.config.v1+json".to_string(),
108 None,
109 );
110
111 client
112 .push(
113 &sig_reference,
114 &[sig_layer],
115 sig_config,
116 &RegistryAuth::Anonymous,
117 None,
118 )
119 .await
120 .map_err(|e| BoxError::RegistryError {
121 registry: registry.to_string(),
122 message: format!("Failed to push signature artifact: {}", e),
123 })?;
124
125 tracing::info!(
126 digest = %manifest_digest,
127 signature_tag = %sig_tag,
128 "Image signed and signature pushed"
129 );
130
131 Ok(SignResult {
132 signature_tag: sig_tag,
133 })
134}
135
136pub(super) fn parse_pem_private_key(
140 pem_bytes: &[u8],
141) -> std::result::Result<p256::ecdsa::SigningKey, String> {
142 let pem_str = std::str::from_utf8(pem_bytes)
143 .map_err(|e| format!("PEM file is not valid UTF-8: {}", e))?;
144
145 let der_bytes = if pem_str.contains("BEGIN EC PRIVATE KEY") {
146 extract_pem_content(
148 pem_str,
149 "-----BEGIN EC PRIVATE KEY-----",
150 "-----END EC PRIVATE KEY-----",
151 )?
152 } else if pem_str.contains("BEGIN PRIVATE KEY") {
153 extract_pem_content(
155 pem_str,
156 "-----BEGIN PRIVATE KEY-----",
157 "-----END PRIVATE KEY-----",
158 )?
159 } else {
160 return Err("Unsupported PEM format: expected EC PRIVATE KEY or PRIVATE KEY".to_string());
161 };
162
163 if let Ok(key) = p256::SecretKey::from_sec1_der(&der_bytes) {
165 return Ok(p256::ecdsa::SigningKey::from(key));
166 }
167
168 use p256::pkcs8::DecodePrivateKey;
170 p256::SecretKey::from_pkcs8_der(&der_bytes)
171 .map(p256::ecdsa::SigningKey::from)
172 .map_err(|e| format!("Failed to parse P-256 private key: {}", e))
173}
174
175pub(super) fn extract_pem_content(
177 pem_str: &str,
178 begin_marker: &str,
179 end_marker: &str,
180) -> std::result::Result<Vec<u8>, String> {
181 let start = pem_str
182 .find(begin_marker)
183 .ok_or("Missing PEM begin marker")?
184 + begin_marker.len();
185 let end = pem_str.find(end_marker).ok_or("Missing PEM end marker")?;
186
187 let b64: String = pem_str[start..end]
188 .chars()
189 .filter(|c| !c.is_whitespace())
190 .collect();
191
192 base64_decode(&b64).map_err(|e| format!("Failed to decode PEM base64: {}", e))
193}