1use base64::Engine;
7use ed25519_dalek::{Signature as Ed25519Signature, Signer, SigningKey, Verifier, VerifyingKey};
8use thiserror::Error;
9
10use crate::hash::compute_sha256;
11use crate::types::{Signature, SpanCommitment};
12
13#[derive(Debug, Error, PartialEq, Eq)]
15pub enum SignatureError {
16 #[error("invalid signature format: {0}")]
18 InvalidFormat(String),
19
20 #[error("signature verification failed: {0}")]
22 VerificationFailed(String),
23
24 #[error("decoding error: {0}")]
26 Decoding(String),
27}
28
29pub fn validate_signature_format(sig: &Signature) -> Result<(), SignatureError> {
33 if sig.alg != "ed25519" {
34 return Err(SignatureError::InvalidFormat(format!(
35 "unsupported algorithm: {}",
36 sig.alg
37 )));
38 }
39
40 if sig.kid.is_empty() {
41 return Err(SignatureError::InvalidFormat(
42 "kid cannot be empty".to_string(),
43 ));
44 }
45
46 if sig.sig.is_empty() {
47 return Err(SignatureError::InvalidFormat(
48 "sig cannot be empty".to_string(),
49 ));
50 }
51
52 base64::engine::general_purpose::URL_SAFE_NO_PAD
54 .decode(&sig.sig)
55 .map_err(|e| SignatureError::InvalidFormat(format!("invalid base64url sig: {}", e)))?;
56
57 Ok(())
58}
59
60pub fn verify_signature(
65 message: &[u8],
66 sig: &Signature,
67 verifying_key_bytes: &[u8],
68) -> Result<bool, SignatureError> {
69 validate_signature_format(sig)?;
70
71 let sig_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
73 .decode(&sig.sig)
74 .map_err(|e| SignatureError::Decoding(format!("failed to decode sig: {}", e)))?;
75
76 let ed25519_sig = Ed25519Signature::from_bytes(
78 sig_bytes
79 .as_slice()
80 .try_into()
81 .map_err(|_| SignatureError::Decoding("signature must be 64 bytes".to_string()))?,
82 );
83
84 let verifying_key = VerifyingKey::from_bytes(
86 verifying_key_bytes
87 .try_into()
88 .map_err(|_| SignatureError::Decoding("key must be 32 bytes".to_string()))?,
89 )
90 .map_err(|e| SignatureError::Decoding(format!("invalid verifying key: {}", e)))?;
91
92 match verifying_key.verify(message, &ed25519_sig) {
94 Ok(_) => Ok(true),
95 Err(_) => Ok(false), }
97}
98
99pub fn verify_span_commitment_signature(
106 commitment: &SpanCommitment,
107 sig: &Signature,
108 verifying_key_bytes: &[u8],
109) -> Result<bool, SignatureError> {
110 let canonical = crate::canonical::canonicalize_span_commitment(commitment)
112 .map_err(|e| SignatureError::Decoding(format!("canonicalization failed: {}", e)))?;
113 let message_hash = compute_sha256(&canonical);
114 let message_bytes = message_hash.as_bytes();
115
116 verify_signature(message_bytes, sig, verifying_key_bytes)
117}
118
119pub fn sign_message(
123 message: &[u8],
124 signing_key: &SigningKey,
125 kid: String,
126) -> Result<Signature, SignatureError> {
127 let sig = signing_key.sign(message);
128 let sig_bytes = sig.to_bytes();
129
130 let sig_base64 = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(sig_bytes);
132
133 Ok(Signature {
134 alg: "ed25519".to_string(),
135 kid,
136 sig: sig_base64,
137 })
138}
139
140pub fn sign_span_commitment(
147 commitment: &SpanCommitment,
148 signing_key: &SigningKey,
149 kid: String,
150) -> Result<Signature, SignatureError> {
151 let canonical = crate::canonical::canonicalize_span_commitment(commitment)
153 .map_err(|e| SignatureError::Decoding(format!("canonicalization failed: {}", e)))?;
154 let message_hash = compute_sha256(&canonical);
155 let message_bytes = message_hash.as_bytes();
156
157 sign_message(message_bytes, signing_key, kid)
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163 use rand::rngs::OsRng;
164
165 #[test]
166 fn test_validate_signature_format_valid() {
167 let sig = Signature {
168 alg: "ed25519".to_string(),
169 kid: "did:key:zTest".to_string(),
170 sig: base64::engine::general_purpose::URL_SAFE_NO_PAD.encode([0u8; 64]),
171 };
172 assert!(validate_signature_format(&sig).is_ok());
173 }
174
175 #[test]
176 fn test_validate_signature_format_invalid_alg() {
177 let sig = Signature {
178 alg: "rsa".to_string(),
179 kid: "did:key:zTest".to_string(),
180 sig: base64::engine::general_purpose::URL_SAFE_NO_PAD.encode([0u8; 64]),
181 };
182 assert!(validate_signature_format(&sig).is_err());
183 }
184
185 #[test]
186 fn test_sign_and_verify() {
187 let mut csprng = OsRng;
188 let signing_key = SigningKey::generate(&mut csprng);
189 let verifying_key = signing_key.verifying_key();
190
191 let message = b"test message";
192 let kid = "did:key:zTest".to_string();
193
194 let sig = sign_message(message, &signing_key, kid.clone()).unwrap();
196
197 let verified = verify_signature(message, &sig, verifying_key.as_bytes()).unwrap();
199 assert!(verified, "Signature should verify");
200
201 let wrong_message = b"wrong message";
203 let verified_wrong =
204 verify_signature(wrong_message, &sig, verifying_key.as_bytes()).unwrap();
205 assert!(
206 !verified_wrong,
207 "Signature should not verify for wrong message"
208 );
209 }
210
211 #[test]
212 fn test_sign_and_verify_span_commitment() {
213 use crate::types::{ArtifactRef, MethodRef, SpanCommitment, SpanStatus};
214
215 let mut csprng = OsRng;
216 let signing_key = SigningKey::generate(&mut csprng);
217 let verifying_key = signing_key.verifying_key();
218 let kid = "did:key:zExecutor".to_string();
219
220 let commitment = SpanCommitment {
221 schema_version: "proof.v2".to_string(),
222 trace_id: "tr_01H1234567890".to_string(),
223 span_id: "sp-0001".to_string(),
224 method_ref: MethodRef {
225 method_id: "com.test/method".to_string(),
226 version: "1.0.0".to_string(),
227 method_shape_root:
228 "sha256:0000000000000000000000000000000000000000000000000000000000000000"
229 .to_string(),
230 image_digest:
231 "sha256:0000000000000000000000000000000000000000000000000000000000000000"
232 .to_string(),
233 method_fingerprint: "git:deadbeef".to_string(),
234 },
235 data_shape_hash:
236 "sha256:0000000000000000000000000000000000000000000000000000000000000000"
237 .to_string(),
238 span_shape_hash:
239 "sha256:0000000000000000000000000000000000000000000000000000000000000000"
240 .to_string(),
241 inputs: vec![ArtifactRef {
242 name: "input.csv".to_string(),
243 content_hash:
244 "sha256:1111111111111111111111111111111111111111111111111111111111111111"
245 .to_string(),
246 uri: None,
247 }],
248 outputs: vec![ArtifactRef {
249 name: "output.json".to_string(),
250 content_hash:
251 "sha256:2222222222222222222222222222222222222222222222222222222222222222"
252 .to_string(),
253 uri: None,
254 }],
255 start_time: "2025-11-06T12:00:00.000Z".to_string(),
256 end_time: "2025-11-06T12:00:00.042Z".to_string(),
257 status: SpanStatus::OK,
258 meta: None,
259 };
260
261 let sig = sign_span_commitment(&commitment, &signing_key, kid.clone()).unwrap();
263
264 let verified =
266 verify_span_commitment_signature(&commitment, &sig, verifying_key.as_bytes()).unwrap();
267 assert!(verified, "Signature should verify");
268 }
269}