1use baseid_core::error::DidError;
9use sha2::{Digest, Sha256};
10
11use crate::document::DidDocument;
12use crate::resolution::{DidResolver, ResolutionMetadata, ResolutionResult};
13
14#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
16#[serde(rename_all = "camelCase")]
17pub struct WebvhLogEntry {
18 pub version_id: String,
19 pub version_time: String,
20 #[serde(default)]
21 pub parameters: WebvhParameters,
22 pub state: DidDocument,
23}
24
25#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
27#[serde(rename_all = "camelCase")]
28pub struct WebvhParameters {
29 #[serde(default)]
30 pub method: String,
31 #[serde(default)]
32 pub scid: String,
33 #[serde(default)]
34 pub update_keys: Vec<String>,
35 #[serde(default)]
36 pub portable: bool,
37 #[serde(default)]
38 pub deactivated: bool,
39 #[serde(default)]
40 pub ttl: u64,
41}
42
43pub struct DidWebvhResolver {
47 client: reqwest::Client,
48}
49
50impl Default for DidWebvhResolver {
51 fn default() -> Self {
52 Self::new()
53 }
54}
55
56impl DidWebvhResolver {
57 pub fn new() -> Self {
58 Self {
59 client: reqwest::Client::new(),
60 }
61 }
62
63 pub fn did_to_url(did: &str) -> baseid_core::Result<String> {
65 if !did.starts_with("did:webvh:") {
66 return Err(DidError::UnsupportedMethod.into());
67 }
68
69 let rest = &did["did:webvh:".len()..];
70 let parts: Vec<&str> = rest.splitn(2, ':').collect();
72 if parts.len() < 2 {
73 return Err(DidError::InvalidDid.into());
74 }
75
76 let _scid = parts[0];
77 let domain_path = parts[1];
78
79 let segments: Vec<&str> = domain_path.split(':').collect();
81 let domain = segments[0].replace("%3A", ":");
82 let path = if segments.len() > 1 {
83 format!("/{}", segments[1..].join("/"))
84 } else {
85 "/.well-known".to_string()
86 };
87
88 Ok(format!("https://{domain}{path}/did.jsonl"))
89 }
90
91 pub fn process_log(did: &str, jsonl: &str) -> baseid_core::Result<DidDocument> {
93 let mut last_doc: Option<DidDocument> = None;
94 let mut expected_version = 1u64;
95
96 for line in jsonl.lines() {
97 let line = line.trim();
98 if line.is_empty() {
99 continue;
100 }
101
102 let entry: WebvhLogEntry =
103 serde_json::from_str(line).map_err(|_| DidError::ResolutionFailed)?;
104
105 let version_num = entry
107 .version_id
108 .split('-')
109 .next()
110 .and_then(|s| s.parse::<u64>().ok())
111 .ok_or(DidError::ResolutionFailed)?;
112
113 if version_num != expected_version {
114 return Err(DidError::ResolutionFailed.into());
115 }
116 expected_version += 1;
117
118 if entry.parameters.deactivated {
120 return Err(DidError::NotFound.into());
121 }
122
123 let mut doc = entry.state;
124 if doc.id.is_empty() {
125 doc.id = did.to_string();
126 }
127 last_doc = Some(doc);
128 }
129
130 last_doc.ok_or_else(|| DidError::NotFound.into())
131 }
132
133 pub fn compute_scid(preliminary_entry: &str) -> String {
135 let hash = Sha256::digest(preliminary_entry.as_bytes());
136 let mut multihash = vec![0x12, 0x20];
137 multihash.extend_from_slice(&hash);
138 format!("z{}", base58_encode(&multihash))
139 }
140}
141
142impl DidResolver for DidWebvhResolver {
143 fn method(&self) -> &str {
144 "webvh"
145 }
146
147 async fn resolve(&self, did: &str) -> baseid_core::Result<ResolutionResult> {
148 let url = Self::did_to_url(did)?;
149
150 let response = self
151 .client
152 .get(&url)
153 .send()
154 .await
155 .map_err(|_| DidError::ResolutionFailed)?;
156
157 if !response.status().is_success() {
158 return Err(DidError::NotFound.into());
159 }
160
161 let body = response
162 .text()
163 .await
164 .map_err(|_| DidError::ResolutionFailed)?;
165
166 let document = Self::process_log(did, &body)?;
167
168 Ok(ResolutionResult {
169 document: Some(document),
170 metadata: ResolutionMetadata {
171 content_type: Some("application/did+ld+json".to_string()),
172 error: None,
173 },
174 })
175 }
176}
177
178fn base58_encode(data: &[u8]) -> String {
179 const ALPHABET: &[u8; 58] = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
180 let mut result = Vec::new();
181 let mut data = data.to_vec();
182 let leading_zeros = data.iter().take_while(|&&b| b == 0).count();
183 while !data.is_empty() {
184 let mut remainder = 0u32;
185 let mut new_data = Vec::new();
186 for &byte in &data {
187 let acc = (remainder << 8) | byte as u32;
188 let digit = acc / 58;
189 remainder = acc % 58;
190 if !new_data.is_empty() || digit > 0 {
191 new_data.push(digit as u8);
192 }
193 }
194 result.push(ALPHABET[remainder as usize]);
195 data = new_data;
196 }
197 result.extend(std::iter::repeat_n(b'1', leading_zeros));
198 result.reverse();
199 String::from_utf8(result).unwrap_or_default()
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205 use crate::document::{VerificationMethod, VerificationRelationship};
206
207 #[test]
208 fn did_to_url_simple_domain() {
209 let url = DidWebvhResolver::did_to_url("did:webvh:zSCID123:example.com").unwrap();
210 assert_eq!(url, "https://example.com/.well-known/did.jsonl");
211 }
212
213 #[test]
214 fn did_to_url_with_path() {
215 let url =
216 DidWebvhResolver::did_to_url("did:webvh:zSCID123:example.com:dids:issuer").unwrap();
217 assert_eq!(url, "https://example.com/dids/issuer/did.jsonl");
218 }
219
220 #[test]
221 fn did_to_url_with_port() {
222 let url = DidWebvhResolver::did_to_url("did:webvh:zSCID123:example.com%3A3000").unwrap();
223 assert_eq!(url, "https://example.com:3000/.well-known/did.jsonl");
224 }
225
226 #[test]
227 fn process_single_entry_log() {
228 let doc = DidDocument {
229 id: "did:webvh:zSCID:example.com".to_string(),
230 context: vec!["https://www.w3.org/ns/did/v1".to_string()],
231 verification_method: vec![VerificationMethod {
232 id: "#key-1".to_string(),
233 r#type: "Multikey".to_string(),
234 controller: "did:webvh:zSCID:example.com".to_string(),
235 public_key_jwk: None,
236 public_key_multibase: Some("z6MkTest".to_string()),
237 }],
238 authentication: vec![VerificationRelationship::Reference("#key-1".to_string())],
239 assertion_method: vec![],
240 key_agreement: vec![],
241 service: vec![],
242 };
243
244 let entry = WebvhLogEntry {
245 version_id: "1-hash123".to_string(),
246 version_time: "2024-01-01T00:00:00Z".to_string(),
247 parameters: WebvhParameters {
248 method: "did:webvh:1.0".to_string(),
249 scid: "zSCID".to_string(),
250 ..Default::default()
251 },
252 state: doc,
253 };
254
255 let jsonl = serde_json::to_string(&entry).unwrap();
256 let result = DidWebvhResolver::process_log("did:webvh:zSCID:example.com", &jsonl).unwrap();
257 assert_eq!(result.verification_method.len(), 1);
258 }
259
260 #[test]
261 fn process_multi_version_log() {
262 let make_entry = |version: u64, key: &str| -> String {
263 serde_json::to_string(&WebvhLogEntry {
264 version_id: format!("{version}-hash{version}"),
265 version_time: format!("2024-01-0{version}T00:00:00Z"),
266 parameters: WebvhParameters::default(),
267 state: DidDocument {
268 id: "did:webvh:zSCID:example.com".to_string(),
269 context: vec!["https://www.w3.org/ns/did/v1".to_string()],
270 verification_method: vec![VerificationMethod {
271 id: "#key-1".to_string(),
272 r#type: "Multikey".to_string(),
273 controller: String::new(),
274 public_key_jwk: None,
275 public_key_multibase: Some(key.to_string()),
276 }],
277 authentication: vec![],
278 assertion_method: vec![],
279 key_agreement: vec![],
280 service: vec![],
281 },
282 })
283 .unwrap()
284 };
285
286 let jsonl = format!(
287 "{}\n{}\n{}",
288 make_entry(1, "zKey1"),
289 make_entry(2, "zKey2"),
290 make_entry(3, "zKey3")
291 );
292 let doc = DidWebvhResolver::process_log("did:webvh:zSCID:example.com", &jsonl).unwrap();
293 assert_eq!(
295 doc.verification_method[0].public_key_multibase.as_deref(),
296 Some("zKey3")
297 );
298 }
299
300 #[test]
301 fn process_deactivated_did() {
302 let entry = WebvhLogEntry {
303 version_id: "1-hash".to_string(),
304 version_time: "2024-01-01T00:00:00Z".to_string(),
305 parameters: WebvhParameters {
306 deactivated: true,
307 ..Default::default()
308 },
309 state: DidDocument {
310 id: String::new(),
311 context: vec![],
312 verification_method: vec![],
313 authentication: vec![],
314 assertion_method: vec![],
315 key_agreement: vec![],
316 service: vec![],
317 },
318 };
319 let jsonl = serde_json::to_string(&entry).unwrap();
320 let result = DidWebvhResolver::process_log("did:webvh:zSCID:example.com", &jsonl);
321 assert!(result.is_err());
322 }
323
324 #[test]
325 fn reject_invalid_version_order() {
326 let entry1 = serde_json::to_string(&WebvhLogEntry {
327 version_id: "1-hash1".to_string(),
328 version_time: "2024-01-01T00:00:00Z".to_string(),
329 parameters: WebvhParameters::default(),
330 state: DidDocument {
331 id: String::new(),
332 context: vec![],
333 verification_method: vec![],
334 authentication: vec![],
335 assertion_method: vec![],
336 key_agreement: vec![],
337 service: vec![],
338 },
339 })
340 .unwrap();
341 let entry3 = serde_json::to_string(&WebvhLogEntry {
342 version_id: "3-hash3".to_string(), version_time: "2024-01-03T00:00:00Z".to_string(),
344 parameters: WebvhParameters::default(),
345 state: DidDocument {
346 id: String::new(),
347 context: vec![],
348 verification_method: vec![],
349 authentication: vec![],
350 assertion_method: vec![],
351 key_agreement: vec![],
352 service: vec![],
353 },
354 })
355 .unwrap();
356 let jsonl = format!("{}\n{}", entry1, entry3);
357 let result = DidWebvhResolver::process_log("did:webvh:zSCID:example.com", &jsonl);
358 assert!(result.is_err());
359 }
360
361 #[test]
362 fn compute_scid_deterministic() {
363 let scid1 = DidWebvhResolver::compute_scid("test entry 1");
364 let scid2 = DidWebvhResolver::compute_scid("test entry 1");
365 let scid3 = DidWebvhResolver::compute_scid("test entry 2");
366 assert_eq!(scid1, scid2); assert_ne!(scid1, scid3); assert!(scid1.starts_with('z')); }
370
371 #[test]
372 fn reject_invalid_did() {
373 assert!(DidWebvhResolver::did_to_url("did:web:example.com").is_err());
374 assert!(DidWebvhResolver::did_to_url("did:webvh:").is_err());
375 }
376}