Skip to main content

igc_net/identity/
did_web.rs

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}