1use std::process::Command;
2
3use base64::Engine;
4use chrono::Utc;
5use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
6
7use crate::crypto::compute_sha256;
8use crate::error::{CrablockError, Result};
9use crate::format::Package;
10
11pub const SIGNATURE_ALGORITHM_ED25519: &str = "ed25519";
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum SignatureVerificationStatus {
15 NotSigned,
16 Verified,
17}
18
19pub fn is_signature_required(package: &Package, require_signature_flag: bool) -> bool {
20 require_signature_flag || package.manifest.require_signature.unwrap_or(false)
22}
23
24pub fn read_signing_key_from_source(source: &str) -> Result<SigningKey> {
25 let raw = read_key_source(source)?;
26
27 match raw.len() {
28 32 => {
29 let mut key = [0u8; 32];
30 key.copy_from_slice(&raw);
31 Ok(SigningKey::from_bytes(&key))
32 }
33 64 => {
34 let mut key = [0u8; 64];
35 key.copy_from_slice(&raw);
36 SigningKey::from_keypair_bytes(&key).map_err(|e| {
37 CrablockError::InvalidKey(format!("Invalid Ed25519 keypair bytes: {e}"))
38 })
39 }
40 len => Err(CrablockError::InvalidKey(format!(
41 "Signing key must decode to 32-byte seed or 64-byte keypair, got {len} bytes"
42 ))),
43 }
44}
45
46pub fn read_verifying_key_from_source(source: &str) -> Result<VerifyingKey> {
47 let raw = read_key_source(source)?;
48
49 match raw.len() {
50 32 => {
51 let mut key = [0u8; 32];
52 key.copy_from_slice(&raw);
53 VerifyingKey::from_bytes(&key)
54 .map_err(|e| CrablockError::InvalidKey(format!("Invalid Ed25519 pubkey: {e}")))
55 }
56 64 => {
57 let mut key = [0u8; 64];
58 key.copy_from_slice(&raw);
59 let signing_key = SigningKey::from_keypair_bytes(&key).map_err(|e| {
60 CrablockError::InvalidKey(format!("Invalid Ed25519 keypair bytes: {e}"))
61 })?;
62 Ok(signing_key.verifying_key())
63 }
64 len => Err(CrablockError::InvalidKey(format!(
65 "Public key must decode to 32-byte Ed25519 pubkey or 64-byte keypair, got {len} bytes"
66 ))),
67 }
68}
69
70pub fn public_key_fingerprint(verifying_key: &VerifyingKey) -> String {
71 compute_sha256(verifying_key.as_bytes())
72}
73
74pub fn sign_package(
75 package: &mut Package,
76 signing_key: &SigningKey,
77 pubkey_id_override: Option<&str>,
78) -> Result<()> {
79 let verifying_key = signing_key.verifying_key();
82 let fingerprint = pubkey_id_override
83 .map(str::to_string)
84 .unwrap_or_else(|| public_key_fingerprint(&verifying_key));
85 let signed_at = Utc::now().to_rfc3339();
86
87 package.signature_algorithm = Some(SIGNATURE_ALGORITHM_ED25519.to_string());
88 package.signing_pubkey_fingerprint = Some(fingerprint.clone());
89 package.manifest.signature_algorithm = Some(SIGNATURE_ALGORITHM_ED25519.to_string());
90 package.manifest.signing_pubkey_fingerprint = Some(fingerprint);
91 package.manifest.signature_created_at = Some(signed_at);
92
93 let canonical_bytes = package.canonical_signing_bytes()?;
94 let signature = signing_key.sign(&canonical_bytes);
95 package.signature = Some(signature.to_bytes().to_vec());
96
97 Ok(())
98}
99
100pub fn verify_package_signature(
101 package: &Package,
102 verifying_key: &VerifyingKey,
103 require_signature: bool,
104) -> Result<SignatureVerificationStatus> {
105 let signature_bytes = match package.signature.as_ref() {
106 Some(signature) => signature,
107 None => {
108 if require_signature {
109 return Err(CrablockError::SignatureMissing);
110 }
111 return Ok(SignatureVerificationStatus::NotSigned);
112 }
113 };
114
115 let algorithm = package
116 .signature_algorithm
117 .as_deref()
118 .or(package.manifest.signature_algorithm.as_deref())
119 .unwrap_or(SIGNATURE_ALGORITHM_ED25519);
120 if algorithm != SIGNATURE_ALGORITHM_ED25519 {
121 return Err(CrablockError::UnsupportedSignatureAlgorithm(
122 algorithm.to_string(),
123 ));
124 }
125
126 if signature_bytes.len() != 64 {
127 return Err(CrablockError::SignatureInvalid);
128 }
129 let signature =
130 Signature::from_slice(signature_bytes).map_err(|_| CrablockError::SignatureInvalid)?;
131
132 let canonical_bytes = package.canonical_signing_bytes()?;
134 verifying_key
135 .verify(&canonical_bytes, &signature)
136 .map_err(|_| CrablockError::SignatureInvalid)?;
137
138 let expected_fingerprint = package
139 .signing_pubkey_fingerprint
140 .as_deref()
141 .or(package.manifest.signing_pubkey_fingerprint.as_deref());
142 if let Some(expected) = expected_fingerprint {
143 let actual = public_key_fingerprint(verifying_key);
144 if expected != actual {
145 return Err(CrablockError::SignatureInvalid);
146 }
147 }
148
149 Ok(SignatureVerificationStatus::Verified)
150}
151
152fn read_key_source(source: &str) -> Result<Vec<u8>> {
153 if let Some(var) = source.strip_prefix("env:") {
155 let value = std::env::var(var)
156 .map_err(|_| CrablockError::KeySource(format!("Environment variable {var} not set")))?;
157 return decode_key_text(&value);
158 }
159
160 if let Some(path) = source.strip_prefix("file:") {
161 let bytes = std::fs::read(path).map_err(|e| {
162 CrablockError::KeySource(format!("Failed to read key file {path}: {e}"))
163 })?;
164 return decode_key_bytes(bytes);
165 }
166
167 if let Some(cmd) = source.strip_prefix("cmd:") {
168 let output = Command::new("sh")
169 .arg("-c")
170 .arg(cmd)
171 .output()
172 .map_err(|e| CrablockError::KeySource(format!("Failed to execute key command: {e}")))?;
173
174 if !output.status.success() {
175 return Err(CrablockError::KeySource(format!(
176 "Key command failed with exit code: {:?}",
177 output.status.code()
178 )));
179 }
180 return decode_key_bytes(output.stdout);
181 }
182
183 decode_key_text(source)
184}
185
186fn decode_key_bytes(bytes: Vec<u8>) -> Result<Vec<u8>> {
187 match String::from_utf8(bytes.clone()) {
188 Ok(value) => decode_key_text(&value),
189 Err(_) => Ok(bytes),
190 }
191}
192
193fn decode_key_text(value: &str) -> Result<Vec<u8>> {
194 let trimmed = value.trim();
196 if trimmed.is_empty() {
197 return Err(CrablockError::InvalidKey(
198 "Key material cannot be empty".to_string(),
199 ));
200 }
201
202 if let Some(raw) = trimmed.strip_prefix("hex:") {
203 return hex::decode(raw)
204 .map_err(|e| CrablockError::InvalidKey(format!("Invalid hex: {e}")));
205 }
206
207 if let Some(raw) = trimmed.strip_prefix("base64:") {
208 return base64::engine::general_purpose::STANDARD
209 .decode(raw)
210 .map_err(|e| CrablockError::InvalidKey(format!("Invalid base64: {e}")));
211 }
212
213 if trimmed.len().is_multiple_of(2) && trimmed.chars().all(|ch| ch.is_ascii_hexdigit()) {
214 if let Ok(decoded) = hex::decode(trimmed) {
215 return Ok(decoded);
216 }
217 }
218
219 if let Ok(decoded) = base64::engine::general_purpose::STANDARD.decode(trimmed) {
220 return Ok(decoded);
221 }
222
223 Ok(trimmed.as_bytes().to_vec())
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229 use crate::crypto::EncryptionAlgorithm;
230 use crate::manifest::Manifest;
231
232 #[test]
233 fn test_sign_and_verify_roundtrip() {
234 let signing_key = SigningKey::from_bytes(&[42u8; 32]);
235 let mut package = Package::new(
236 Manifest::new(
237 "test".to_string(),
238 4,
239 EncryptionAlgorithm::Aes256Gcm,
240 &[0u8; 12],
241 "artifact_hash",
242 "payload_hash",
243 ),
244 vec![1, 2, 3, 4],
245 None,
246 );
247
248 sign_package(&mut package, &signing_key, None).unwrap();
249 let status =
250 verify_package_signature(&package, &signing_key.verifying_key(), true).unwrap();
251
252 assert_eq!(status, SignatureVerificationStatus::Verified);
253 }
254
255 #[test]
256 fn test_tampered_payload_invalidates_signature() {
257 let signing_key = SigningKey::from_bytes(&[11u8; 32]);
258 let mut package = Package::new(
259 Manifest::new(
260 "test".to_string(),
261 4,
262 EncryptionAlgorithm::Aes256Gcm,
263 &[0u8; 12],
264 "artifact_hash",
265 "payload_hash",
266 ),
267 vec![1, 2, 3, 4],
268 None,
269 );
270
271 sign_package(&mut package, &signing_key, None).unwrap();
272 package.payload[0] ^= 0xFF;
273
274 let result = verify_package_signature(&package, &signing_key.verifying_key(), true);
275 assert!(matches!(result, Err(CrablockError::SignatureInvalid)));
276 }
277
278 #[test]
279 fn test_unsigned_package_require_signature_behavior() {
280 let signing_key = SigningKey::from_bytes(&[7u8; 32]);
281 let package = Package::new(
282 Manifest::new(
283 "test".to_string(),
284 4,
285 EncryptionAlgorithm::Aes256Gcm,
286 &[0u8; 12],
287 "artifact_hash",
288 "payload_hash",
289 ),
290 vec![1, 2, 3, 4],
291 None,
292 );
293
294 let optional =
295 verify_package_signature(&package, &signing_key.verifying_key(), false).unwrap();
296 assert_eq!(optional, SignatureVerificationStatus::NotSigned);
297
298 let required = verify_package_signature(&package, &signing_key.verifying_key(), true);
299 assert!(matches!(required, Err(CrablockError::SignatureMissing)));
300 }
301
302 #[test]
303 fn test_read_signing_key_from_base64_prefix() {
304 let source = "base64:KioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKio=";
305 let key = read_signing_key_from_source(source).unwrap();
306 assert_eq!(key.to_bytes(), [42u8; 32]);
307 }
308}