1use anyhow::{anyhow, Result};
5use ed25519_dalek::{Signer, SigningKey, Verifier, VerifyingKey};
6use serde::{Deserialize, Serialize};
7use sha2::{Digest, Sha256};
8
9use super::did::did_key_from_ed25519;
10
11#[derive(Clone, Debug, Serialize, Deserialize)]
13#[serde(rename_all = "camelCase")]
14pub struct FactCredential {
15 #[serde(rename = "@context")]
16 pub context: Vec<String>,
17 #[serde(rename = "type")]
18 pub credential_type: Vec<String>,
19 pub id: String,
20 pub issuer: String,
21 pub valid_from: String,
22 pub credential_subject: FactSubject,
23 #[serde(skip_serializing_if = "Option::is_none")]
24 pub credential_status: Option<CredentialStatus>,
25 pub proof: Option<DataIntegrityProof>,
26}
27
28#[derive(Clone, Debug, Serialize, Deserialize)]
29#[serde(rename_all = "camelCase")]
30pub struct CredentialStatus {
31 pub id: String,
32 #[serde(rename = "type")]
33 pub status_type: String,
34 pub status_purpose: String,
35 pub status_list_index: String,
36 pub status_list_credential: String,
37}
38
39#[derive(Clone, Debug, Serialize, Deserialize)]
40#[serde(rename_all = "camelCase")]
41pub struct FactSubject {
42 pub id: String,
43 #[serde(skip_serializing_if = "Option::is_none")]
44 pub source_uri: Option<String>,
45 pub content_hash: String,
46 pub encoding_method: String,
47 pub dimensions: u32,
48 #[serde(skip_serializing_if = "Option::is_none")]
49 pub subject_id: Option<String>,
50 #[serde(skip_serializing_if = "Option::is_none")]
51 pub relation_id: Option<String>,
52 #[serde(skip_serializing_if = "Option::is_none")]
53 pub object_id: Option<String>,
54}
55
56#[derive(Clone, Debug, Serialize, Deserialize)]
57#[serde(rename_all = "camelCase")]
58pub struct DataIntegrityProof {
59 #[serde(rename = "type")]
60 pub proof_type: String,
61 pub cryptosuite: String,
62 pub created: String,
63 pub verification_method: String,
64 pub proof_purpose: String,
65 pub proof_value: String,
66}
67
68pub fn create_fact_credential(
70 issuer_did: &str,
71 fact_id: &str,
72 content_hash: &[u8; 32],
73 dimensions: u32,
74 source_uri: Option<&str>,
75 triple: Option<(&str, &str, &str)>,
76 status_index: u64,
77) -> FactCredential {
78 let timestamp = chrono_iso8601_now();
79 let hash_hex = hex_encode(content_hash);
80
81 let mut subject = FactSubject {
82 id: format!("urn:hms:fact:{fact_id}"),
83 source_uri: source_uri.map(String::from),
84 content_hash: hash_hex,
85 encoding_method: "holographic-reduced-representation".to_string(),
86 dimensions,
87 subject_id: None,
88 relation_id: None,
89 object_id: None,
90 };
91
92 if let Some((s, r, o)) = triple {
93 subject.subject_id = Some(s.to_string());
94 subject.relation_id = Some(r.to_string());
95 subject.object_id = Some(o.to_string());
96 }
97
98 let vc_id = format!("urn:uuid:{}", simple_uuid());
99 let status = CredentialStatus {
100 id: format!("{vc_id}#status"),
101 status_type: "BitstringStatusListEntry".to_string(),
102 status_purpose: "revocation".to_string(),
103 status_list_index: status_index.to_string(),
104 status_list_credential: format!("{issuer_did}/status/1"),
105 };
106
107 FactCredential {
108 context: vec![
109 "https://www.w3.org/ns/credentials/v2".to_string(),
110 "https://writerslogic.com/ns/hms/v1".to_string(),
111 ],
112 credential_type: vec![
113 "VerifiableCredential".to_string(),
114 "HMSFactCredential".to_string(),
115 ],
116 id: vc_id,
117 issuer: issuer_did.to_string(),
118 valid_from: timestamp,
119 credential_subject: subject,
120 credential_status: Some(status),
121 proof: None,
122 }
123}
124
125fn jcs_serialize<T: serde::Serialize>(value: &T) -> Result<Vec<u8>> {
129 let json_value =
130 serde_json::to_value(value).map_err(|e| anyhow!("JCS serialization failed: {e}"))?;
131 serde_json::to_vec(&json_value).map_err(|e| anyhow!("JCS serialization failed: {e}"))
132}
133
134const PROOF_TYPE: &str = "DataIntegrityProof";
135const CRYPTOSUITE: &str = "eddsa-jcs-2022";
136const PROOF_PURPOSE: &str = "assertionMethod";
137
138fn proof_hash_data<T: Serialize>(
139 unsigned: &T,
140 proof_options: &serde_json::Value,
141) -> Result<[u8; 64]> {
142 let mut hash_data = [0u8; 64];
143 hash_data[..32].copy_from_slice(&Sha256::digest(jcs_serialize(proof_options)?));
144 hash_data[32..].copy_from_slice(&Sha256::digest(jcs_serialize(unsigned)?));
145 Ok(hash_data)
146}
147
148pub(crate) fn sign_document<T: Serialize>(
153 signing_key: &SigningKey,
154 unsigned: &T,
155) -> Result<DataIntegrityProof> {
156 let issuer_did = did_key_from_ed25519(&signing_key.verifying_key().to_bytes());
157 let created = chrono_iso8601_now();
158 let verification_method = format!("{issuer_did}#key-0");
159 let proof_options = serde_json::json!({
160 "type": PROOF_TYPE,
161 "cryptosuite": CRYPTOSUITE,
162 "created": &created,
163 "verificationMethod": &verification_method,
164 "proofPurpose": PROOF_PURPOSE
165 });
166 let signature = signing_key.sign(&proof_hash_data(unsigned, &proof_options)?);
167
168 Ok(DataIntegrityProof {
169 proof_type: PROOF_TYPE.to_string(),
170 cryptosuite: CRYPTOSUITE.to_string(),
171 created,
172 verification_method,
173 proof_purpose: PROOF_PURPOSE.to_string(),
174 proof_value: multibase::encode(multibase::Base::Base58Btc, signature.to_bytes()),
175 })
176}
177
178pub(crate) fn verify_document<T: Serialize>(
183 unsigned: &T,
184 issuer: &str,
185 proof: &DataIntegrityProof,
186) -> Result<VerifyingKey> {
187 if proof.proof_type != PROOF_TYPE
188 || proof.cryptosuite != CRYPTOSUITE
189 || proof.proof_purpose != PROOF_PURPOSE
190 {
191 return Err(anyhow!("unsupported proof type, cryptosuite, or purpose"));
192 }
193
194 let did_part = proof
195 .verification_method
196 .split('#')
197 .next()
198 .unwrap_or(&proof.verification_method);
199 let issuer_did = issuer.split('#').next().unwrap_or(issuer);
200 if did_part != issuer_did {
201 return Err(anyhow!(
202 "verification method DID ({did_part}) does not match credential issuer ({issuer_did})"
203 ));
204 }
205
206 let pk_bytes = super::did::ed25519_from_did_key(did_part)?;
207 let verifying_key =
208 VerifyingKey::from_bytes(&pk_bytes).map_err(|e| anyhow!("invalid public key: {e}"))?;
209
210 let (_, sig_bytes) = multibase::decode(&proof.proof_value)
211 .map_err(|e| anyhow!("multibase decode failed: {e}"))?;
212 let signature = ed25519_dalek::Signature::from_slice(&sig_bytes)
213 .map_err(|e| anyhow!("invalid signature: {e}"))?;
214
215 let proof_options = serde_json::json!({
216 "type": &proof.proof_type,
217 "cryptosuite": &proof.cryptosuite,
218 "created": &proof.created,
219 "verificationMethod": &proof.verification_method,
220 "proofPurpose": &proof.proof_purpose
221 });
222 verifying_key
223 .verify(&proof_hash_data(unsigned, &proof_options)?, &signature)
224 .map_err(|e| anyhow!("VC signature verification failed: {e}"))?;
225 Ok(verifying_key)
226}
227
228pub fn sign_credential(
230 signing_key: &SigningKey,
231 mut credential: FactCredential,
232) -> Result<FactCredential> {
233 credential.proof = None;
234 credential.proof = Some(sign_document(signing_key, &credential)?);
235 Ok(credential)
236}
237
238pub fn verify_credential(credential: &FactCredential) -> Result<()> {
240 let proof = credential
241 .proof
242 .as_ref()
243 .ok_or_else(|| anyhow!("credential has no proof"))?;
244 let mut unsigned = credential.clone();
245 unsigned.proof = None;
246 verify_document(&unsigned, &credential.issuer, proof).map(|_| ())
247}
248
249pub(crate) fn parse_iso8601_utc(value: &str) -> Result<u64> {
251 let b = value.as_bytes();
252 let well_formed = b.len() == 20
253 && b[4] == b'-'
254 && b[7] == b'-'
255 && b[10] == b'T'
256 && b[13] == b':'
257 && b[16] == b':'
258 && b[19] == b'Z';
259 let field = |range: std::ops::Range<usize>| -> Option<u64> {
260 let part = value.get(range)?;
261 part.bytes()
262 .all(|c| c.is_ascii_digit())
263 .then(|| part.parse().ok())
264 .flatten()
265 };
266 let parsed = well_formed
267 .then(|| {
268 Some((
269 field(0..4)?,
270 field(5..7)?,
271 field(8..10)?,
272 field(11..13)?,
273 field(14..16)?,
274 field(17..19)?,
275 ))
276 })
277 .flatten();
278 let Some((year, month, day, hour, minute, second)) = parsed else {
279 return Err(anyhow!("timestamp must be YYYY-MM-DDTHH:MM:SSZ"));
280 };
281 if year < 1970 || !(1..=12).contains(&month) || hour > 23 || minute > 59 || second > 59 {
282 return Err(anyhow!("timestamp field out of range"));
283 }
284 let y = if month <= 2 { year - 1 } else { year };
286 let era = y / 400;
287 let yoe = y - era * 400;
288 let mp = (month + 9) % 12;
289 let doy = (153 * mp + 2) / 5 + day - 1;
290 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
291 let days = era * 146097 + doe - 719468;
292 if day == 0 || days_to_ymd(days) != (year, month, day) {
293 return Err(anyhow!("timestamp is not a valid calendar date"));
294 }
295 Ok(days * 86400 + hour * 3600 + minute * 60 + second)
296}
297
298pub(crate) fn chrono_iso8601_now() -> String {
299 let dur = std::time::SystemTime::now()
300 .duration_since(std::time::UNIX_EPOCH)
301 .unwrap_or_default();
302 let secs = dur.as_secs();
303 let days = secs / 86400;
304 let rem = secs % 86400;
305 let hours = rem / 3600;
306 let minutes = (rem % 3600) / 60;
307 let seconds = rem % 60;
308
309 let (year, month, day) = days_to_ymd(days);
310 format!("{year:04}-{month:02}-{day:02}T{hours:02}:{minutes:02}:{seconds:02}Z")
311}
312
313fn days_to_ymd(days_since_epoch: u64) -> (u64, u64, u64) {
314 let z = days_since_epoch + 719468;
315 let era = z / 146097;
316 let doe = z - era * 146097;
317 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
318 let y = yoe + era * 400;
319 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
320 let mp = (5 * doy + 2) / 153;
321 let d = doy - (153 * mp + 2) / 5 + 1;
322 let m = if mp < 10 { mp + 3 } else { mp - 9 };
323 let y = if m <= 2 { y + 1 } else { y };
324 (y, m, d)
325}
326
327fn simple_uuid() -> String {
328 use rand::Rng;
329 let mut bytes = [0u8; 16];
330 rand::rng().fill_bytes(&mut bytes);
331 bytes[6] = (bytes[6] & 0x0f) | 0x40;
332 bytes[8] = (bytes[8] & 0x3f) | 0x80;
333 format!(
334 "{:08x}-{:04x}-{:04x}-{:04x}-{:012x}",
335 u32::from_be_bytes(bytes[0..4].try_into().unwrap()),
336 u16::from_be_bytes(bytes[4..6].try_into().unwrap()),
337 u16::from_be_bytes(bytes[6..8].try_into().unwrap()),
338 u16::from_be_bytes(bytes[8..10].try_into().unwrap()),
339 u64::from_be_bytes({
340 let mut buf = [0u8; 8];
341 buf[2..8].copy_from_slice(&bytes[10..16]);
342 buf
343 }),
344 )
345}
346
347fn hex_encode(bytes: &[u8]) -> String {
348 bytes.iter().map(|b| format!("{b:02x}")).collect()
349}
350
351#[cfg(test)]
352mod tests {
353 use super::*;
354
355 fn test_keypair() -> SigningKey {
356 SigningKey::from_bytes(&rand::random())
357 }
358
359 #[test]
360 fn create_and_sign_vc() {
361 let key = test_keypair();
362 let hash = [0xABu8; 32];
363 let did = did_key_from_ed25519(&key.verifying_key().to_bytes());
364
365 let vc = create_fact_credential(&did, "fact-001", &hash, 16384, None, None, 0);
366 assert_eq!(vc.credential_type[1], "HMSFactCredential");
367
368 let signed = sign_credential(&key, vc).unwrap();
369 assert!(signed.proof.is_some());
370
371 verify_credential(&signed).unwrap();
372 }
373
374 #[test]
375 fn vc_with_triple() {
376 let key = test_keypair();
377 let hash = [0xCDu8; 32];
378 let did = did_key_from_ed25519(&key.verifying_key().to_bytes());
379
380 let vc = create_fact_credential(
381 &did,
382 "triple-001",
383 &hash,
384 16384,
385 Some("https://example.com/source"),
386 Some(("paris", "capital_of", "france")),
387 1,
388 );
389 assert_eq!(vc.credential_subject.subject_id.as_deref(), Some("paris"));
390 assert_eq!(
391 vc.credential_subject.relation_id.as_deref(),
392 Some("capital_of")
393 );
394
395 let signed = sign_credential(&key, vc).unwrap();
396 verify_credential(&signed).unwrap();
397 }
398
399 #[test]
400 fn issuer_mismatch_rejected() {
401 let victim = test_keypair();
405 let attacker = test_keypair();
406 let victim_did = did_key_from_ed25519(&victim.verifying_key().to_bytes());
407 let hash = [0x11u8; 32];
408
409 let vc = create_fact_credential(&victim_did, "forge-001", &hash, 16384, None, None, 0);
410 let signed = sign_credential(&attacker, vc).unwrap();
411
412 assert!(verify_credential(&signed).is_err());
413 }
414
415 #[test]
416 fn tampered_vc_rejected() {
417 let key = test_keypair();
418 let hash = [0xABu8; 32];
419 let did = did_key_from_ed25519(&key.verifying_key().to_bytes());
420
421 let vc = create_fact_credential(&did, "fact-001", &hash, 16384, None, None, 0);
422 let mut signed = sign_credential(&key, vc).unwrap();
423 signed.credential_subject.content_hash = "tampered".to_string();
424
425 assert!(verify_credential(&signed).is_err());
426 }
427
428 #[test]
429 fn vc_json_structure() {
430 let key = test_keypair();
431 let hash = [0u8; 32];
432 let did = did_key_from_ed25519(&key.verifying_key().to_bytes());
433
434 let vc = create_fact_credential(&did, "f1", &hash, 10000, None, None, 0);
435 let json_str = serde_json::to_string_pretty(&vc).unwrap();
436 assert!(json_str.contains("@context"));
437 assert!(json_str.contains("VerifiableCredential"));
438 assert!(json_str.contains("holographic-reduced-representation"));
439 }
440}