1use sha2::{Digest, Sha256};
8
9use crate::errors::WebVHDIDError;
10
11use super::jcs;
12
13const MULTIHASH_SHA256_HEADER: [u8; 2] = [0x12, 0x20];
15
16const BASE58BTC_ALPHABET: &str = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
18
19const SCID_MIN_LENGTH: usize = 45;
21
22const SCID_MAX_LENGTH: usize = 48;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum HashAlgorithm {
28 Sha256,
30}
31
32pub fn detect_hash_algorithm(scid: &str) -> Result<HashAlgorithm, WebVHDIDError> {
37 let (_, decoded) = multibase::decode(scid).map_err(|e| WebVHDIDError::InvalidSCIDFormat {
38 details: format!("failed to decode SCID multibase: {}", e),
39 })?;
40
41 if decoded.len() < 2 {
42 return Err(WebVHDIDError::InvalidSCIDFormat {
43 details: "SCID multihash too short".to_string(),
44 });
45 }
46
47 match (decoded[0], decoded[1]) {
48 (0x12, 0x20) => Ok(HashAlgorithm::Sha256),
49 (code, length) => Err(WebVHDIDError::UnsupportedHashAlgorithm {
50 details: format!(
51 "multihash code {:#04x} with length {:#04x} is not supported",
52 code, length
53 ),
54 }),
55 }
56}
57
58pub fn validate_hash_algorithm(
63 scid: &str,
64 method_version: &str,
65) -> Result<HashAlgorithm, WebVHDIDError> {
66 let algorithm = detect_hash_algorithm(scid)?;
67
68 match method_version {
69 "did:webvh:1.0" | "did:webvh:0.5" => match algorithm {
70 HashAlgorithm::Sha256 => Ok(algorithm),
71 },
72 _ => Err(WebVHDIDError::UnsupportedHashAlgorithm {
73 details: format!("unknown method version: {}", method_version),
74 }),
75 }
76}
77
78pub fn validate_scid_format(scid: &str) -> Result<(), WebVHDIDError> {
85 if !scid.starts_with('z') {
86 return Err(WebVHDIDError::InvalidSCIDFormat {
87 details: format!(
88 "SCID must start with 'z' (base58btc prefix), got '{}'",
89 scid.chars().next().unwrap_or(' ')
90 ),
91 });
92 }
93
94 if scid.len() < SCID_MIN_LENGTH || scid.len() > SCID_MAX_LENGTH {
95 return Err(WebVHDIDError::InvalidSCIDFormat {
96 details: format!(
97 "SCID must be {}-{} characters, got {}",
98 SCID_MIN_LENGTH,
99 SCID_MAX_LENGTH,
100 scid.len()
101 ),
102 });
103 }
104
105 for ch in scid[1..].chars() {
107 if !BASE58BTC_ALPHABET.contains(ch) {
108 return Err(WebVHDIDError::InvalidSCIDFormat {
109 details: format!("invalid base58btc character in SCID: '{}'", ch),
110 });
111 }
112 }
113
114 Ok(())
115}
116
117pub fn compute_multihash_sha256(data: &[u8]) -> Vec<u8> {
121 let hash = Sha256::digest(data);
122 let mut result = Vec::with_capacity(2 + hash.len());
123 result.extend_from_slice(&MULTIHASH_SHA256_HEADER);
124 result.extend_from_slice(&hash);
125 result
126}
127
128pub fn compute_multihash_base58btc(data: &[u8]) -> String {
132 let multihash = compute_multihash_sha256(data);
133 multibase::encode(multibase::Base::Base58Btc, &multihash)
134}
135
136pub fn compute_entry_hash(entry: &serde_json::Value) -> Result<String, WebVHDIDError> {
147 let mut hashable = entry.clone();
148 if let Some(obj) = hashable.as_object_mut() {
149 obj.remove("proof");
150 obj.remove("versionId");
151 } else {
152 return Err(WebVHDIDError::InvalidVersionId {
153 entry: 0,
154 details: "log entry is not a JSON object".to_string(),
155 });
156 }
157
158 let canonical = jcs::canonicalize(&hashable);
159 Ok(compute_multihash_base58btc(canonical.as_bytes()))
160}
161
162pub fn parse_version_id(version_id: &str) -> Result<(u64, &str), WebVHDIDError> {
166 let dash_pos = version_id
167 .find('-')
168 .ok_or_else(|| WebVHDIDError::InvalidVersionId {
169 entry: 0,
170 details: format!("missing dash separator in version ID: {}", version_id),
171 })?;
172
173 let number_str = &version_id[..dash_pos];
174 let hash = &version_id[dash_pos + 1..];
175
176 let number: u64 = number_str
177 .parse()
178 .map_err(|_| WebVHDIDError::InvalidVersionId {
179 entry: 0,
180 details: format!("invalid version number: {}", number_str),
181 })?;
182
183 if hash.is_empty() {
184 return Err(WebVHDIDError::InvalidVersionId {
185 entry: 0,
186 details: "empty hash in version ID".to_string(),
187 });
188 }
189
190 Ok((number, hash))
191}
192
193pub fn verify_version_id(
197 entry: &serde_json::Value,
198 expected_number: u64,
199 entry_index: usize,
200) -> Result<u64, WebVHDIDError> {
201 let version_id = entry
202 .get("versionId")
203 .and_then(|v| v.as_str())
204 .ok_or_else(|| WebVHDIDError::InvalidVersionId {
205 entry: entry_index,
206 details: "missing or non-string versionId".to_string(),
207 })?;
208
209 let (number, expected_hash) = parse_version_id(version_id).map_err(|e| {
210 if let WebVHDIDError::InvalidVersionId { details, .. } = e {
211 WebVHDIDError::InvalidVersionId {
212 entry: entry_index,
213 details,
214 }
215 } else {
216 e
217 }
218 })?;
219
220 if number != expected_number {
221 return Err(WebVHDIDError::InvalidVersionId {
222 entry: entry_index,
223 details: format!("expected version {}, got {}", expected_number, number),
224 });
225 }
226
227 let computed_hash = compute_entry_hash(entry)?;
228
229 if computed_hash != expected_hash {
230 return Err(WebVHDIDError::EntryHashVerificationFailed { entry: entry_index });
231 }
232
233 Ok(number)
234}
235
236pub fn verify_scid(scid: &str, genesis_entry: &serde_json::Value) -> Result<(), WebVHDIDError> {
245 let mut entry = genesis_entry.clone();
246
247 if let Some(obj) = entry.as_object_mut() {
249 obj.remove("proof");
250 }
251
252 if let Some(obj) = entry.as_object_mut() {
254 obj.insert(
255 "versionId".to_string(),
256 serde_json::Value::String("{SCID}".to_string()),
257 );
258 }
259
260 let json_str = serde_json::to_string(&entry).map_err(|e| WebVHDIDError::InvalidSCIDFormat {
262 details: format!("failed to serialize genesis entry: {}", e),
263 })?;
264
265 let replaced = json_str.replace(scid, "{SCID}");
267
268 let replaced_value: serde_json::Value =
270 serde_json::from_str(&replaced).map_err(|e| WebVHDIDError::InvalidSCIDFormat {
271 details: format!("failed to parse replaced entry: {}", e),
272 })?;
273
274 let canonical = jcs::canonicalize(&replaced_value);
275 let computed_scid = compute_multihash_base58btc(canonical.as_bytes());
276
277 if computed_scid != scid {
278 return Err(WebVHDIDError::SCIDVerificationFailed);
279 }
280
281 Ok(())
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287 use serde_json::json;
288
289 #[test]
290 fn test_compute_multihash_sha256() {
291 let data = b"hello";
292 let result = compute_multihash_sha256(data);
293 assert_eq!(result[0], 0x12); assert_eq!(result[1], 0x20); assert_eq!(result.len(), 34); let result2 = compute_multihash_sha256(data);
300 assert_eq!(result, result2);
301 }
302
303 #[test]
304 fn test_compute_multihash_base58btc() {
305 let data = b"hello";
306 let result = compute_multihash_base58btc(data);
307 assert!(result.starts_with('z'));
309
310 let result2 = compute_multihash_base58btc(data);
312 assert_eq!(result, result2);
313 }
314
315 #[test]
316 fn test_compute_entry_hash() {
317 let entry = json!({
318 "versionId": "1-test",
319 "versionTime": "2025-04-29T17:15:59Z",
320 "parameters": {"method": "did:webvh:1.0"},
321 "state": {"id": "did:webvh:test:example.com"},
322 "proof": [{"type": "DataIntegrityProof"}]
323 });
324
325 let hash = compute_entry_hash(&entry).unwrap();
326 assert!(hash.starts_with('z'));
327
328 let mut entry2 = entry.clone();
330 entry2["proof"] = json!([{"type": "DataIntegrityProof", "extra": "data"}]);
331 let hash2 = compute_entry_hash(&entry2).unwrap();
332 assert_eq!(hash, hash2);
333 }
334
335 #[test]
336 fn test_compute_entry_hash_not_object() {
337 let entry = json!("not an object");
338 assert!(compute_entry_hash(&entry).is_err());
339 }
340
341 #[test]
342 fn test_parse_version_id() {
343 let (num, hash) = parse_version_id("1-QmTest").unwrap();
344 assert_eq!(num, 1);
345 assert_eq!(hash, "QmTest");
346
347 let (num, hash) = parse_version_id("42-zAbcdef").unwrap();
348 assert_eq!(num, 42);
349 assert_eq!(hash, "zAbcdef");
350 }
351
352 #[test]
353 fn test_parse_version_id_invalid() {
354 assert!(parse_version_id("1QmTest").is_err());
356 assert!(parse_version_id("1-").is_err());
358 assert!(parse_version_id("abc-QmTest").is_err());
360 }
361
362 #[test]
363 fn test_verify_version_id_valid() {
364 let mut entry = json!({
366 "versionTime": "2025-04-29T17:15:59Z",
367 "parameters": {"method": "did:webvh:1.0"},
368 "state": {"id": "did:webvh:test:example.com"}
369 });
370
371 let hash = compute_entry_hash(&entry).unwrap();
373 entry["versionId"] = serde_json::Value::String(format!("1-{}", hash));
374
375 let result = verify_version_id(&entry, 1, 1);
377 assert!(result.is_ok());
378 assert_eq!(result.unwrap(), 1);
379 }
380
381 #[test]
382 fn test_verify_version_id_wrong_number() {
383 let entry = json!({
384 "versionId": "2-zSomeHash",
385 "parameters": {},
386 "state": {}
387 });
388
389 let result = verify_version_id(&entry, 1, 1);
390 assert!(matches!(
391 result,
392 Err(WebVHDIDError::InvalidVersionId { .. })
393 ));
394 }
395
396 #[test]
397 fn test_verify_version_id_hash_mismatch() {
398 let entry = json!({
399 "versionId": "1-zWrongHash",
400 "versionTime": "2025-04-29T17:15:59Z",
401 "parameters": {},
402 "state": {}
403 });
404
405 let result = verify_version_id(&entry, 1, 1);
406 assert!(matches!(
407 result,
408 Err(WebVHDIDError::EntryHashVerificationFailed { .. })
409 ));
410 }
411
412 #[test]
413 fn test_verify_scid_roundtrip() {
414 let preliminary = json!({
416 "versionId": "{SCID}",
417 "versionTime": "2025-04-29T17:15:59Z",
418 "parameters": {
419 "method": "did:webvh:1.0",
420 "scid": "{SCID}",
421 "updateKeys": ["z6MkTestKey"]
422 },
423 "state": {
424 "@context": ["https://www.w3.org/ns/did/v1"],
425 "id": "did:webvh:{SCID}:example.com"
426 }
427 });
428
429 let canonical = jcs::canonicalize(&preliminary);
431 let scid = compute_multihash_base58btc(canonical.as_bytes());
432
433 let json_str = serde_json::to_string(&preliminary).unwrap();
435 let replaced = json_str.replace("{SCID}", &scid);
436 let genesis: serde_json::Value = serde_json::from_str(&replaced).unwrap();
437
438 assert!(verify_scid(&scid, &genesis).is_ok());
440 }
441
442 #[test]
443 fn test_verify_scid_mismatch() {
444 let entry = json!({
445 "versionId": "zWrongSCID",
446 "versionTime": "2025-04-29T17:15:59Z",
447 "parameters": {
448 "method": "did:webvh:1.0",
449 "scid": "zWrongSCID",
450 "updateKeys": ["z6MkTestKey"]
451 },
452 "state": {
453 "@context": ["https://www.w3.org/ns/did/v1"],
454 "id": "did:webvh:zWrongSCID:example.com"
455 }
456 });
457
458 assert!(matches!(
459 verify_scid("zWrongSCID", &entry),
460 Err(WebVHDIDError::SCIDVerificationFailed)
461 ));
462 }
463
464 #[test]
465 fn test_entry_hash_deterministic() {
466 let entry = json!({
467 "versionId": "1-test",
468 "versionTime": "2025-01-01T00:00:00Z",
469 "parameters": {"method": "did:webvh:1.0"},
470 "state": {"id": "test"}
471 });
472
473 let hash1 = compute_entry_hash(&entry).unwrap();
474 let hash2 = compute_entry_hash(&entry).unwrap();
475 assert_eq!(hash1, hash2);
476 }
477}