1use std::borrow::Borrow;
2use std::collections::{BTreeMap, BTreeSet};
3use std::fmt;
4use std::ops::Deref;
5use std::str::FromStr;
6
7use base64::Engine;
8use base64::engine::general_purpose::URL_SAFE_NO_PAD;
9use serde::{Deserialize, Deserializer, Serialize, Serializer};
10
11use super::DidKey;
12
13const DID_WEB_PREFIX: &str = "did:web:";
14
15#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
16pub enum DidWebError {
17 #[error("invalid did:web: {0}")]
18 InvalidFormat(String),
19 #[error("did:web contains invalid percent-encoding: {0}")]
20 InvalidPercentEncoding(String),
21 #[error("did:web host is invalid: {0}")]
22 InvalidHost(String),
23 #[error("did:web path segment is invalid: {0}")]
24 InvalidPathSegment(String),
25}
26
27#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
28pub enum DidWebResolutionError {
29 #[error("no did:web resolver was configured")]
30 ResolverUnavailable,
31 #[error("did:web fetch failed: {0}")]
32 Fetch(String),
33 #[error("did:web document was invalid: {0}")]
34 InvalidDocument(String),
35 #[error("did:web verification method not found: {0}")]
36 VerificationMethodNotFound(String),
37}
38
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct ResolvedDidWebVerificationMethod {
41 pub did: String,
42 pub kid: Option<String>,
43 pub public_key: iroh::PublicKey,
44}
45
46pub trait DidWebResolver {
47 fn resolve_verification_method(
48 &self,
49 did: &str,
50 kid: Option<&str>,
51 ) -> Result<ResolvedDidWebVerificationMethod, DidWebResolutionError>;
52}
53
54#[derive(Debug, Clone, Copy, Default)]
55pub struct NoDidWebResolver;
56
57impl DidWebResolver for NoDidWebResolver {
58 fn resolve_verification_method(
59 &self,
60 _did: &str,
61 _kid: Option<&str>,
62 ) -> Result<ResolvedDidWebVerificationMethod, DidWebResolutionError> {
63 Err(DidWebResolutionError::ResolverUnavailable)
64 }
65}
66
67#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
68pub struct DidWeb(String);
69
70impl DidWeb {
71 pub fn parse(value: impl Into<String>) -> Result<Self, DidWebError> {
72 let value = value.into();
73 parse_did_web_components(&value)?;
74 Ok(Self(value))
75 }
76
77 pub fn as_str(&self) -> &str {
78 &self.0
79 }
80
81 pub fn into_string(self) -> String {
82 self.0
83 }
84
85 pub fn document_url(&self) -> String {
86 let components = parse_did_web_components(self.as_str()).expect("validated did:web");
87 let mut url = format!("https://{}", components.host);
88 if components.path.is_empty() {
89 url.push_str("/.well-known/did.json");
90 } else {
91 for segment in components.path {
92 url.push('/');
93 url.push_str(&segment);
94 }
95 url.push_str("/did.json");
96 }
97 url
98 }
99}
100
101impl fmt::Display for DidWeb {
102 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103 self.0.fmt(f)
104 }
105}
106
107impl Deref for DidWeb {
108 type Target = str;
109
110 fn deref(&self) -> &Self::Target {
111 self.as_str()
112 }
113}
114
115impl Borrow<str> for DidWeb {
116 fn borrow(&self) -> &str {
117 self.as_str()
118 }
119}
120
121impl FromStr for DidWeb {
122 type Err = DidWebError;
123
124 fn from_str(s: &str) -> Result<Self, Self::Err> {
125 Self::parse(s)
126 }
127}
128
129impl TryFrom<String> for DidWeb {
130 type Error = DidWebError;
131
132 fn try_from(value: String) -> Result<Self, Self::Error> {
133 Self::parse(value)
134 }
135}
136
137impl TryFrom<&str> for DidWeb {
138 type Error = DidWebError;
139
140 fn try_from(value: &str) -> Result<Self, Self::Error> {
141 Self::parse(value)
142 }
143}
144
145impl From<DidWeb> for String {
146 fn from(value: DidWeb) -> Self {
147 value.into_string()
148 }
149}
150
151impl Serialize for DidWeb {
152 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
153 where
154 S: Serializer,
155 {
156 serializer.serialize_str(self.as_str())
157 }
158}
159
160impl<'de> Deserialize<'de> for DidWeb {
161 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
162 where
163 D: Deserializer<'de>,
164 {
165 let value = String::deserialize(deserializer)?;
166 Self::parse(value).map_err(serde::de::Error::custom)
167 }
168}
169
170#[derive(Debug, Clone, Deserialize)]
171pub struct DidWebDocument {
172 pub id: DidWeb,
173 #[serde(default, rename = "verificationMethod")]
174 verification_methods: Vec<DidWebVerificationMethodEntry>,
175 #[serde(default, rename = "assertionMethod")]
176 assertion_methods: Vec<DidWebVerificationMethodReference>,
177}
178
179impl DidWebDocument {
180 pub fn from_slice(bytes: &[u8]) -> Result<Self, DidWebResolutionError> {
181 serde_json::from_slice(bytes)
182 .map_err(|error| DidWebResolutionError::InvalidDocument(error.to_string()))
183 }
184
185 pub fn resolve_verification_method(
186 &self,
187 did: &DidWeb,
188 kid: Option<&str>,
189 ) -> Result<ResolvedDidWebVerificationMethod, DidWebResolutionError> {
190 if self.id != *did {
191 return Err(DidWebResolutionError::InvalidDocument(format!(
192 "document id {found} does not match requested DID {expected}",
193 found = self.id,
194 expected = did,
195 )));
196 }
197
198 let mut methods = BTreeMap::<String, DidWebVerificationMethodEntry>::new();
199 for method in &self.verification_methods {
200 let method_id = normalize_method_id(did, &method.id)?;
201 if methods.insert(method_id, method.clone()).is_some() {
202 return Err(DidWebResolutionError::InvalidDocument(
203 "duplicate verificationMethod id".to_string(),
204 ));
205 }
206 }
207
208 let mut assertion_method_ids = BTreeSet::<String>::new();
209 for reference in &self.assertion_methods {
210 match reference {
211 DidWebVerificationMethodReference::Reference(id) => {
212 assertion_method_ids.insert(normalize_method_id(did, id)?);
213 }
214 DidWebVerificationMethodReference::Embedded(method) => {
215 let method_id = normalize_method_id(did, &method.id)?;
216 if methods.insert(method_id.clone(), method.clone()).is_some() {
217 return Err(DidWebResolutionError::InvalidDocument(
218 "duplicate assertionMethod id".to_string(),
219 ));
220 }
221 assertion_method_ids.insert(method_id);
222 }
223 }
224 }
225
226 if assertion_method_ids.is_empty() {
227 return Err(DidWebResolutionError::InvalidDocument(
228 "did:web document does not declare assertionMethod".to_string(),
229 ));
230 }
231
232 let selected_id = match kid {
233 Some(kid) => {
234 let normalized = normalize_method_id(did, kid)?;
235 if !assertion_method_ids.contains(&normalized) {
236 return Err(DidWebResolutionError::VerificationMethodNotFound(
237 normalized,
238 ));
239 }
240 normalized
241 }
242 None if assertion_method_ids.len() == 1 => assertion_method_ids
243 .iter()
244 .next()
245 .expect("non-empty assertionMethod set")
246 .clone(),
247 None => {
248 return Err(DidWebResolutionError::VerificationMethodNotFound(
249 "no kid provided and did:web document has multiple assertion methods"
250 .to_string(),
251 ));
252 }
253 };
254
255 let method = methods.get(&selected_id).ok_or_else(|| {
256 DidWebResolutionError::VerificationMethodNotFound(selected_id.clone())
257 })?;
258 let public_key = method.public_key(did)?;
259
260 Ok(ResolvedDidWebVerificationMethod {
261 did: did.to_string(),
262 kid: Some(selected_id),
263 public_key,
264 })
265 }
266}
267
268#[derive(Debug, Clone, Deserialize)]
269#[serde(untagged)]
270enum DidWebVerificationMethodReference {
271 Reference(String),
272 Embedded(DidWebVerificationMethodEntry),
273}
274
275#[derive(Debug, Clone, Deserialize)]
276struct DidWebVerificationMethodEntry {
277 id: String,
278 #[serde(default, rename = "publicKeyJwk")]
279 public_key_jwk: Option<DidWebPublicKeyJwk>,
280 #[serde(default, rename = "publicKeyMultibase")]
281 public_key_multibase: Option<String>,
282}
283
284impl DidWebVerificationMethodEntry {
285 fn public_key(&self, did: &DidWeb) -> Result<iroh::PublicKey, DidWebResolutionError> {
286 match (&self.public_key_jwk, &self.public_key_multibase) {
287 (Some(_), Some(_)) => Err(DidWebResolutionError::InvalidDocument(
288 "verification method must contain exactly one public key encoding".to_string(),
289 )),
290 (Some(jwk), None) => jwk.public_key(),
291 (None, Some(multibase)) => public_key_from_multibase(multibase, did),
292 (None, None) => Err(DidWebResolutionError::InvalidDocument(
293 "verification method does not contain supported public key material".to_string(),
294 )),
295 }
296 }
297}
298
299#[derive(Debug, Clone, Deserialize)]
300struct DidWebPublicKeyJwk {
301 kty: String,
302 crv: String,
303 x: String,
304 #[serde(default)]
305 d: Option<String>,
306}
307
308impl DidWebPublicKeyJwk {
309 fn public_key(&self) -> Result<iroh::PublicKey, DidWebResolutionError> {
310 if self.kty != "OKP" || self.crv != "Ed25519" {
311 return Err(DidWebResolutionError::InvalidDocument(
312 "publicKeyJwk must be an OKP Ed25519 key".to_string(),
313 ));
314 }
315 if self.d.is_some() {
316 return Err(DidWebResolutionError::InvalidDocument(
317 "publicKeyJwk must not include private key material".to_string(),
318 ));
319 }
320
321 let decoded = URL_SAFE_NO_PAD
322 .decode(self.x.as_bytes())
323 .map_err(|error| DidWebResolutionError::InvalidDocument(error.to_string()))?;
324 let public_key_bytes: [u8; 32] = decoded.try_into().map_err(|_| {
325 DidWebResolutionError::InvalidDocument(
326 "publicKeyJwk.x must decode to 32 bytes".to_string(),
327 )
328 })?;
329 iroh::PublicKey::from_bytes(&public_key_bytes).map_err(|_| {
330 DidWebResolutionError::InvalidDocument(
331 "publicKeyJwk.x does not contain a valid Ed25519 key".to_string(),
332 )
333 })
334 }
335}
336
337#[cfg(feature = "did-web")]
338#[derive(Debug, Clone)]
339pub struct ReqwestDidWebResolver {
340 client: reqwest::blocking::Client,
341}
342
343#[cfg(feature = "did-web")]
344impl Default for ReqwestDidWebResolver {
345 fn default() -> Self {
346 Self::new()
347 }
348}
349
350#[cfg(feature = "did-web")]
351impl ReqwestDidWebResolver {
352 pub fn new() -> Self {
353 let client = reqwest::blocking::Client::builder()
354 .https_only(true)
355 .build()
356 .expect("valid reqwest did:web client");
357 Self { client }
358 }
359}
360
361#[cfg(feature = "did-web")]
362impl DidWebResolver for ReqwestDidWebResolver {
363 fn resolve_verification_method(
364 &self,
365 did: &str,
366 kid: Option<&str>,
367 ) -> Result<ResolvedDidWebVerificationMethod, DidWebResolutionError> {
368 resolve_verification_method_with_fetch(did, kid, |document_url| {
369 let response = self
370 .client
371 .get(document_url)
372 .send()
373 .map_err(|error| DidWebResolutionError::Fetch(error.to_string()))?;
374 let response = response
375 .error_for_status()
376 .map_err(|error| DidWebResolutionError::Fetch(error.to_string()))?;
377 let bytes = response
378 .bytes()
379 .map_err(|error| DidWebResolutionError::Fetch(error.to_string()))?;
380 Ok(bytes.to_vec())
381 })
382 }
383}
384
385fn resolve_verification_method_with_fetch<F>(
386 did: &str,
387 kid: Option<&str>,
388 fetch: F,
389) -> Result<ResolvedDidWebVerificationMethod, DidWebResolutionError>
390where
391 F: FnOnce(&str) -> Result<Vec<u8>, DidWebResolutionError>,
392{
393 let did = DidWeb::parse(did)
394 .map_err(|error| DidWebResolutionError::InvalidDocument(error.to_string()))?;
395 let document_bytes = fetch(&did.document_url())?;
396 let document = DidWebDocument::from_slice(&document_bytes)?;
397 document.resolve_verification_method(&did, kid)
398}
399
400fn public_key_from_multibase(
401 multibase: &str,
402 did: &DidWeb,
403) -> Result<iroh::PublicKey, DidWebResolutionError> {
404 let did_key = DidKey::parse(format!("did:key:{multibase}")).map_err(|error| {
405 DidWebResolutionError::InvalidDocument(format!(
406 "did:web verification method for {did} contains invalid publicKeyMultibase: {error}",
407 ))
408 })?;
409 Ok(did_key.public_key())
410}
411
412fn normalize_method_id(did: &DidWeb, value: &str) -> Result<String, DidWebResolutionError> {
413 if value.starts_with('#') {
414 return Ok(format!("{did}{value}"));
415 }
416 if value.starts_with("did:") {
417 return Ok(value.to_string());
418 }
419 Err(DidWebResolutionError::InvalidDocument(format!(
420 "verification method id {value:?} is not a DID URL or fragment"
421 )))
422}
423
424#[derive(Debug, Clone, PartialEq, Eq)]
425struct DidWebComponents {
426 host: String,
427 path: Vec<String>,
428}
429
430fn parse_did_web_components(value: &str) -> Result<DidWebComponents, DidWebError> {
431 let encoded = value
432 .strip_prefix(DID_WEB_PREFIX)
433 .ok_or_else(|| DidWebError::InvalidFormat(value.to_string()))?;
434 if encoded.is_empty() {
435 return Err(DidWebError::InvalidFormat(value.to_string()));
436 }
437
438 let mut segments = encoded.split(':');
439 let host = decode_component(
440 segments
441 .next()
442 .ok_or_else(|| DidWebError::InvalidFormat(value.to_string()))?,
443 )?;
444 validate_host(&host)?;
445
446 let mut path = Vec::new();
447 for segment in segments {
448 let decoded = decode_component(segment)?;
449 validate_path_segment(&decoded)?;
450 path.push(decoded);
451 }
452
453 Ok(DidWebComponents { host, path })
454}
455
456fn decode_component(value: &str) -> Result<String, DidWebError> {
457 let mut decoded = String::with_capacity(value.len());
458 let bytes = value.as_bytes();
459 let mut index = 0;
460 while index < bytes.len() {
461 if bytes[index] == b'%' {
462 if index + 2 >= bytes.len() {
463 return Err(DidWebError::InvalidPercentEncoding(value.to_string()));
464 }
465 let hi = decode_hex_digit(bytes[index + 1])
466 .ok_or_else(|| DidWebError::InvalidPercentEncoding(value.to_string()))?;
467 let lo = decode_hex_digit(bytes[index + 2])
468 .ok_or_else(|| DidWebError::InvalidPercentEncoding(value.to_string()))?;
469 decoded.push(((hi << 4) | lo) as char);
470 index += 3;
471 } else {
472 decoded.push(bytes[index] as char);
473 index += 1;
474 }
475 }
476 Ok(decoded)
477}
478
479fn decode_hex_digit(byte: u8) -> Option<u8> {
480 match byte {
481 b'0'..=b'9' => Some(byte - b'0'),
482 b'a'..=b'f' => Some(byte - b'a' + 10),
483 b'A'..=b'F' => Some(byte - b'A' + 10),
484 _ => None,
485 }
486}
487
488fn validate_host(host: &str) -> Result<(), DidWebError> {
489 if host.is_empty()
490 || host.starts_with('.')
491 || host.ends_with('.')
492 || host.contains('/')
493 || host.contains('?')
494 || host.contains('#')
495 || host.contains(' ')
496 {
497 return Err(DidWebError::InvalidHost(host.to_string()));
498 }
499 Ok(())
500}
501
502fn validate_path_segment(segment: &str) -> Result<(), DidWebError> {
503 if segment.is_empty()
504 || segment == "."
505 || segment == ".."
506 || segment.contains('/')
507 || segment.contains('?')
508 || segment.contains('#')
509 || segment.contains('\\')
510 {
511 return Err(DidWebError::InvalidPathSegment(segment.to_string()));
512 }
513 Ok(())
514}
515
516#[cfg(test)]
517mod tests {
518 use super::*;
519
520 fn sample_did_document(id: &str, methods: &[(&str, iroh::PublicKey)]) -> serde_json::Value {
521 let verification_methods = methods
522 .iter()
523 .map(|(method_id, public_key)| {
524 serde_json::json!({
525 "id": method_id,
526 "type": "JsonWebKey2020",
527 "controller": id,
528 "publicKeyJwk": {
529 "kty": "OKP",
530 "crv": "Ed25519",
531 "x": URL_SAFE_NO_PAD.encode(public_key.as_bytes()),
532 }
533 })
534 })
535 .collect::<Vec<_>>();
536 serde_json::json!({
537 "id": id,
538 "verificationMethod": verification_methods,
539 "assertionMethod": methods.iter().map(|(method_id, _)| serde_json::Value::String((*method_id).to_string())).collect::<Vec<_>>(),
540 })
541 }
542
543 #[test]
544 fn root_did_maps_to_well_known_url() {
545 let did = DidWeb::parse("did:web:portal.example.org").unwrap();
546 assert_eq!(
547 did.document_url(),
548 "https://portal.example.org/.well-known/did.json"
549 );
550 }
551
552 #[test]
553 fn path_did_maps_to_path_scoped_url() {
554 let did = DidWeb::parse("did:web:portal.example.org:pilots:alice").unwrap();
555 assert_eq!(
556 did.document_url(),
557 "https://portal.example.org/pilots/alice/did.json"
558 );
559 }
560
561 #[test]
562 fn percent_encoded_host_port_is_decoded_in_document_url() {
563 let did = DidWeb::parse("did:web:localhost%3A8443:issuer").unwrap();
564 assert_eq!(did.document_url(), "https://localhost:8443/issuer/did.json");
565 }
566
567 #[test]
568 fn invalid_path_traversal_segment_is_rejected() {
569 let err = DidWeb::parse("did:web:portal.example.org:%2E%2E").unwrap_err();
570 assert!(matches!(err, DidWebError::InvalidPathSegment(segment) if segment == ".."));
571 }
572
573 #[test]
574 fn resolves_assertion_method_from_public_key_jwk() {
575 let secret_key = iroh::SecretKey::from_bytes(&[21u8; 32]);
576 let did = DidWeb::parse("did:web:portal.example.org").unwrap();
577 let document = DidWebDocument::from_slice(
578 serde_json::to_vec(&sample_did_document(
579 did.as_str(),
580 &[("did:web:portal.example.org#issuer-1", secret_key.public())],
581 ))
582 .unwrap()
583 .as_slice(),
584 )
585 .unwrap();
586
587 let resolved = document
588 .resolve_verification_method(&did, Some("did:web:portal.example.org#issuer-1"))
589 .unwrap();
590 assert_eq!(resolved.did, did.as_str());
591 assert_eq!(
592 resolved.kid.as_deref(),
593 Some("did:web:portal.example.org#issuer-1")
594 );
595 assert_eq!(resolved.public_key, secret_key.public());
596 }
597
598 #[test]
599 fn resolves_assertion_method_from_public_key_multibase() {
600 let secret_key = iroh::SecretKey::from_bytes(&[22u8; 32]);
601 let did = DidWeb::parse("did:web:portal.example.org").unwrap();
602 let public_key_multibase = DidKey::from_public_key(secret_key.public())
603 .method_specific_id()
604 .to_string();
605 let document = serde_json::json!({
606 "id": did,
607 "verificationMethod": [{
608 "id": "did:web:portal.example.org#issuer-1",
609 "type": "Ed25519VerificationKey2020",
610 "controller": "did:web:portal.example.org",
611 "publicKeyMultibase": public_key_multibase,
612 }],
613 "assertionMethod": ["#issuer-1"],
614 });
615 let document = DidWebDocument::from_slice(&serde_json::to_vec(&document).unwrap()).unwrap();
616
617 let resolved = document
618 .resolve_verification_method(&did, Some("#issuer-1"))
619 .unwrap();
620 assert_eq!(resolved.public_key, secret_key.public());
621 assert_eq!(
622 resolved.kid.as_deref(),
623 Some("did:web:portal.example.org#issuer-1")
624 );
625 }
626
627 #[test]
628 fn multiple_assertion_methods_require_kid() {
629 let first = iroh::SecretKey::from_bytes(&[31u8; 32]).public();
630 let second = iroh::SecretKey::from_bytes(&[32u8; 32]).public();
631 let did = DidWeb::parse("did:web:portal.example.org").unwrap();
632 let document = DidWebDocument::from_slice(
633 &serde_json::to_vec(&sample_did_document(
634 did.as_str(),
635 &[
636 ("did:web:portal.example.org#old", first),
637 ("did:web:portal.example.org#new", second),
638 ],
639 ))
640 .unwrap(),
641 )
642 .unwrap();
643
644 let err = document
645 .resolve_verification_method(&did, None)
646 .unwrap_err();
647 assert!(matches!(
648 err,
649 DidWebResolutionError::VerificationMethodNotFound(reason)
650 if reason.contains("no kid provided")
651 ));
652 }
653
654 #[test]
655 fn removed_rotation_key_is_no_longer_resolvable() {
656 let retained = iroh::SecretKey::from_bytes(&[41u8; 32]).public();
657 let removed = iroh::SecretKey::from_bytes(&[42u8; 32]).public();
658 let did = DidWeb::parse("did:web:portal.example.org").unwrap();
659
660 let initial = DidWebDocument::from_slice(
661 &serde_json::to_vec(&sample_did_document(
662 did.as_str(),
663 &[
664 ("did:web:portal.example.org#old", removed),
665 ("did:web:portal.example.org#new", retained),
666 ],
667 ))
668 .unwrap(),
669 )
670 .unwrap();
671 let rotated = DidWebDocument::from_slice(
672 &serde_json::to_vec(&sample_did_document(
673 did.as_str(),
674 &[("did:web:portal.example.org#new", retained)],
675 ))
676 .unwrap(),
677 )
678 .unwrap();
679
680 assert_eq!(
681 initial
682 .resolve_verification_method(&did, Some("did:web:portal.example.org#old"))
683 .unwrap()
684 .public_key,
685 removed
686 );
687 let err = rotated
688 .resolve_verification_method(&did, Some("did:web:portal.example.org#old"))
689 .unwrap_err();
690 assert!(matches!(
691 err,
692 DidWebResolutionError::VerificationMethodNotFound(kid)
693 if kid == "did:web:portal.example.org#old"
694 ));
695 }
696
697 #[test]
698 fn fetch_failure_is_reported() {
699 let err = resolve_verification_method_with_fetch(
700 "did:web:portal.example.org",
701 Some("#issuer-1"),
702 |_document_url| Err(DidWebResolutionError::Fetch("network down".to_string())),
703 )
704 .unwrap_err();
705 assert!(matches!(err, DidWebResolutionError::Fetch(reason) if reason == "network down"));
706 }
707
708 #[test]
709 fn malformed_document_is_rejected_during_fetch_resolution() {
710 let err = resolve_verification_method_with_fetch(
711 "did:web:portal.example.org",
712 Some("#issuer-1"),
713 |_document_url| {
714 Ok(br#"{"id":"did:web:portal.example.org","assertionMethod":42}"#.to_vec())
715 },
716 )
717 .unwrap_err();
718 assert!(matches!(err, DidWebResolutionError::InvalidDocument(_)));
719 }
720
721 #[test]
722 fn did_web_alias_is_non_authoritative_relative_to_governance() {
723 let fixture: serde_json::Value = serde_json::from_str(include_str!(
724 "../../../../specs/fixtures/v0.3/pilot_did_mirror_precedence.json"
725 ))
726 .unwrap();
727 let case = fixture.get("case").unwrap();
728 let authoritative = case
729 .get("authoritative_pilot_auth_did")
730 .unwrap()
731 .as_str()
732 .unwrap();
733 let alias = case.get("public_alias_did_web").unwrap().as_str().unwrap();
734
735 assert!(DidKey::parse(authoritative).is_ok());
736 assert!(DidWeb::parse(alias).is_ok());
737 assert_ne!(authoritative, alias);
738 }
739
740 #[test]
741 fn did_key_multibase_must_be_ed25519() {
742 let did = DidWeb::parse("did:web:portal.example.org").unwrap();
743 let document = serde_json::json!({
744 "id": did,
745 "verificationMethod": [{
746 "id": "did:web:portal.example.org#issuer-1",
747 "type": "UnknownKey",
748 "controller": "did:web:portal.example.org",
749 "publicKeyMultibase": "z3weFAN",
750 }],
751 "assertionMethod": ["#issuer-1"],
752 });
753 let document = DidWebDocument::from_slice(&serde_json::to_vec(&document).unwrap()).unwrap();
754 let err = document
755 .resolve_verification_method(&did, Some("#issuer-1"))
756 .unwrap_err();
757 assert!(
758 matches!(err, DidWebResolutionError::InvalidDocument(message) if message.contains("invalid publicKeyMultibase"))
759 );
760 }
761}