1use baseid_core::error::CryptoError;
20use baseid_core::types::SignatureAlgorithm;
21use baseid_crypto::signer::{Signer, Verifier};
22use serde_json::Value;
23use sha2::{Digest, Sha256};
24
25use crate::credential::VerifiableCredential;
26use crate::proof::{Cryptosuite, DataIntegrityProof};
27
28pub fn sign_credential_di(
32 vc: &VerifiableCredential,
33 signer: &dyn Signer,
34 verification_method: &str,
35) -> baseid_core::Result<VerifiableCredential> {
36 if signer.algorithm() != SignatureAlgorithm::EdDsa {
38 return Err(CryptoError::UnsupportedAlgorithm.into());
39 }
40
41 let mut vc_value = serde_json::to_value(vc).map_err(|_| CryptoError::SigningFailed)?;
43 if let Some(obj) = vc_value.as_object_mut() {
44 obj.remove("proof");
45 }
46
47 let created = current_timestamp();
49 let proof_options = serde_json::json!({
50 "type": "DataIntegrityProof",
51 "cryptosuite": Cryptosuite::EddsaRdfc2022.as_str(),
52 "created": created,
53 "verificationMethod": verification_method,
54 "proofPurpose": "assertionMethod",
55 });
56
57 let canonical_doc = canonicalize_document(&vc_value);
59 let canonical_opts = canonicalize_document(&proof_options);
60
61 let opts_hash = Sha256::digest(canonical_opts.as_bytes());
63 let doc_hash = Sha256::digest(canonical_doc.as_bytes());
64 let mut to_sign = Vec::with_capacity(64);
65 to_sign.extend_from_slice(&opts_hash);
66 to_sign.extend_from_slice(&doc_hash);
67
68 let signature = signer
70 .sign(&to_sign)
71 .map_err(|_| CryptoError::SigningFailed)?;
72
73 let proof_value = multibase::encode(multibase::Base::Base58Btc, &signature);
75
76 let proof = DataIntegrityProof {
78 r#type: "DataIntegrityProof".to_string(),
79 cryptosuite: Cryptosuite::EddsaRdfc2022.as_str().to_string(),
80 created,
81 verification_method: verification_method.to_string(),
82 proof_purpose: "assertionMethod".to_string(),
83 proof_value,
84 };
85
86 let mut signed_vc = vc.clone();
87 signed_vc.proof = Some(serde_json::to_value(&proof).map_err(|_| CryptoError::SigningFailed)?);
88
89 Ok(signed_vc)
90}
91
92pub fn verify_credential_di(
96 vc: &VerifiableCredential,
97 verifier: &dyn Verifier,
98) -> baseid_core::Result<()> {
99 let proof_value = vc.proof.as_ref().ok_or(CryptoError::VerificationFailed)?;
101 let proof: DataIntegrityProof =
102 serde_json::from_value(proof_value.clone()).map_err(|_| CryptoError::VerificationFailed)?;
103
104 if Cryptosuite::parse_str(&proof.cryptosuite).is_none() {
106 return Err(CryptoError::UnsupportedAlgorithm.into());
107 }
108
109 let (_, signature_bytes) =
111 multibase::decode(&proof.proof_value).map_err(|_| CryptoError::VerificationFailed)?;
112
113 let proof_options = serde_json::json!({
115 "type": proof.r#type,
116 "cryptosuite": proof.cryptosuite,
117 "created": proof.created,
118 "verificationMethod": proof.verification_method,
119 "proofPurpose": proof.proof_purpose,
120 });
121
122 let mut vc_value = serde_json::to_value(vc).map_err(|_| CryptoError::VerificationFailed)?;
124 if let Some(obj) = vc_value.as_object_mut() {
125 obj.remove("proof");
126 }
127
128 let canonical_doc = canonicalize_document(&vc_value);
130 let canonical_opts = canonicalize_document(&proof_options);
131 let opts_hash = Sha256::digest(canonical_opts.as_bytes());
132 let doc_hash = Sha256::digest(canonical_doc.as_bytes());
133 let mut to_verify = Vec::with_capacity(64);
134 to_verify.extend_from_slice(&opts_hash);
135 to_verify.extend_from_slice(&doc_hash);
136
137 let valid = verifier
139 .verify(&to_verify, &signature_bytes)
140 .map_err(|_| CryptoError::VerificationFailed)?;
141 if !valid {
142 return Err(CryptoError::VerificationFailed.into());
143 }
144
145 Ok(())
146}
147
148fn canonicalize_document(value: &Value) -> String {
153 baseid_jsonld::canonicalize_vc(value)
154}
155
156#[cfg(test)]
164fn canonicalize_jcs(value: &Value) -> String {
165 match value {
166 Value::Object(map) => {
167 let mut entries: Vec<(&String, &Value)> = map.iter().collect();
168 entries.sort_by_key(|(k, _)| *k);
169 let parts: Vec<String> = entries
170 .iter()
171 .map(|(k, v)| {
172 format!(
173 "{}:{}",
174 canonicalize_jcs(&Value::String((*k).clone())),
175 canonicalize_jcs(v)
176 )
177 })
178 .collect();
179 format!("{{{}}}", parts.join(","))
180 }
181 Value::Array(arr) => {
182 let parts: Vec<String> = arr.iter().map(canonicalize_jcs).collect();
183 format!("[{}]", parts.join(","))
184 }
185 Value::String(_) | Value::Number(_) | Value::Bool(_) | Value::Null => {
186 serde_json::to_string(value).unwrap_or_else(|_| "null".to_string())
187 }
188 }
189}
190
191fn current_timestamp() -> String {
193 let secs = std::time::SystemTime::now()
194 .duration_since(std::time::UNIX_EPOCH)
195 .unwrap_or_default()
196 .as_secs();
197 let days = secs / 86400;
199 let rem = secs % 86400;
200 let hours = rem / 3600;
201 let minutes = (rem % 3600) / 60;
202 let seconds = rem % 60;
203 let (year, month, day) = epoch_days_to_ymd(days);
205 format!(
206 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
207 year, month, day, hours, minutes, seconds
208 )
209}
210
211fn epoch_days_to_ymd(days: u64) -> (u64, u64, u64) {
212 let y = 1970 + days / 365;
214 let remaining = days - (y - 1970) * 365 - ((y - 1969) / 4);
215 let m = remaining / 30 + 1;
216 let d = remaining % 30 + 1;
217 (y, m.min(12), d.min(28))
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223 use baseid_core::types::KeyType;
224 use baseid_crypto::KeyPair;
225
226 use crate::credential::{Issuer, VerifiableCredential};
227
228 fn sample_vc() -> VerifiableCredential {
229 VerifiableCredential {
230 context: vec!["https://www.w3.org/ns/credentials/v2".to_string()],
231 id: Some("urn:uuid:test-123".to_string()),
232 r#type: vec![
233 "VerifiableCredential".to_string(),
234 "TestCredential".to_string(),
235 ],
236 issuer: Issuer::Uri("did:key:z6MkIssuer".to_string()),
237 valid_from: Some("2024-01-01T00:00:00Z".to_string()),
238 valid_until: None,
239 credential_subject: serde_json::json!({"id": "did:key:z6MkHolder", "name": "Alice", "score": 95}),
240 credential_status: None,
241 proof: None,
242 }
243 }
244
245 fn test_keypair() -> KeyPair {
246 KeyPair::generate(KeyType::Ed25519).unwrap()
247 }
248
249 #[test]
250 fn sign_verify_roundtrip_ed25519() {
251 let kp = test_keypair();
252 let vc = sample_vc();
253 let signed = sign_credential_di(&vc, &kp, "did:key:z6MkIssuer#key-1").unwrap();
254 assert!(signed.proof.is_some());
255 verify_credential_di(&signed, &kp.public).unwrap();
256 }
257
258 #[test]
259 fn proof_structure() {
260 let kp = test_keypair();
261 let vc = sample_vc();
262 let signed = sign_credential_di(&vc, &kp, "did:key:z6MkIssuer#key-1").unwrap();
263 let proof: DataIntegrityProof = serde_json::from_value(signed.proof.unwrap()).unwrap();
264 assert_eq!(proof.r#type, "DataIntegrityProof");
265 assert_eq!(proof.cryptosuite, "eddsa-rdfc-2022");
266 assert_eq!(proof.proof_purpose, "assertionMethod");
267 assert_eq!(proof.verification_method, "did:key:z6MkIssuer#key-1");
268 }
269
270 #[test]
271 fn proof_value_is_multibase() {
272 let kp = test_keypair();
273 let vc = sample_vc();
274 let signed = sign_credential_di(&vc, &kp, "did:key:z6MkIssuer#key-1").unwrap();
275 let proof: DataIntegrityProof = serde_json::from_value(signed.proof.unwrap()).unwrap();
276 assert!(
277 proof.proof_value.starts_with('z'),
278 "Should be base58btc multibase (z prefix)"
279 );
280 let (base, _) = multibase::decode(&proof.proof_value).unwrap();
282 assert_eq!(base, multibase::Base::Base58Btc);
283 }
284
285 #[test]
286 fn wrong_key_rejected() {
287 let signer = test_keypair();
288 let wrong_verifier = test_keypair(); let vc = sample_vc();
290 let signed = sign_credential_di(&vc, &signer, "did:key:z6MkIssuer#key-1").unwrap();
291 let result = verify_credential_di(&signed, &wrong_verifier.public);
292 assert!(result.is_err());
293 }
294
295 #[test]
296 fn tampered_credential_rejected() {
297 let kp = test_keypair();
298 let vc = sample_vc();
299 let mut signed = sign_credential_di(&vc, &kp, "did:key:z6MkIssuer#key-1").unwrap();
300 signed.credential_subject = serde_json::json!({"id": "did:key:z6MkEvil", "name": "Eve"});
302 let result = verify_credential_di(&signed, &kp.public);
303 assert!(result.is_err());
304 }
305
306 #[test]
307 fn tampered_proof_rejected() {
308 let kp = test_keypair();
309 let vc = sample_vc();
310 let mut signed = sign_credential_di(&vc, &kp, "did:key:z6MkIssuer#key-1").unwrap();
311 if let Some(proof) = signed.proof.as_mut() {
313 proof["proofValue"] = serde_json::Value::String("zINVALID".to_string());
314 }
315 let result = verify_credential_di(&signed, &kp.public);
316 assert!(result.is_err());
317 }
318
319 #[test]
320 fn canonicalization_determinism() {
321 let a = serde_json::json!({"z": 1, "a": 2, "m": 3});
322 let b = serde_json::json!({"a": 2, "m": 3, "z": 1});
323 assert_eq!(canonicalize_jcs(&a), canonicalize_jcs(&b));
324 }
325
326 #[test]
327 fn canonicalization_nested() {
328 let val = serde_json::json!({"b": {"z": 1, "a": 2}, "a": [3, 1, 2]});
329 let canonical = canonicalize_jcs(&val);
330 assert_eq!(canonical, "{\"a\":[3,1,2],\"b\":{\"a\":2,\"z\":1}}");
331 }
332
333 #[test]
334 fn proof_created_timestamp() {
335 let kp = test_keypair();
336 let vc = sample_vc();
337 let signed = sign_credential_di(&vc, &kp, "did:key:z6MkIssuer#key-1").unwrap();
338 let proof: DataIntegrityProof = serde_json::from_value(signed.proof.unwrap()).unwrap();
339 assert!(proof.created.contains('T'));
341 assert!(proof.created.ends_with('Z'));
342 assert!(proof.created.starts_with("20"));
343 }
344
345 #[test]
346 fn no_proof_verification_fails() {
347 let kp = test_keypair();
348 let vc = sample_vc(); let result = verify_credential_di(&vc, &kp.public);
350 assert!(result.is_err());
351 }
352
353 #[test]
354 fn credential_unchanged_after_signing() {
355 let kp = test_keypair();
356 let vc = sample_vc();
357 let signed = sign_credential_di(&vc, &kp, "did:key:z6MkIssuer#key-1").unwrap();
358 assert_eq!(signed.context, vc.context);
360 assert_eq!(signed.r#type, vc.r#type);
361 assert_eq!(signed.credential_subject, vc.credential_subject);
362 assert_eq!(signed.valid_from, vc.valid_from);
363 }
364}