baseid_did/methods/
peer.rs

1//! `did:peer` method implementation.
2//!
3//! Supports numalgo 0 (inception key), 2 (multiple keys + services),
4//! 3 (short form hash), and 4 (long form + short form).
5//!
6//! Reference: <https://identity.foundation/peer-did-method-spec/>
7
8use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
9use sha2::{Digest, Sha256};
10
11use baseid_core::error::DidError;
12use baseid_crypto::PublicKey;
13
14use crate::document::{
15    DidDocument, Service, ServiceEndpoint, VerificationMethod, VerificationRelationship,
16};
17use crate::resolution::{DidResolver, ResolutionMetadata, ResolutionResult};
18
19const DID_CONTEXT: &str = "https://www.w3.org/ns/did/v1";
20const MULTIKEY_CONTEXT: &str = "https://w3id.org/security/multikey/v1";
21
22/// Resolver for `did:peer` method.
23pub struct DidPeerResolver;
24
25impl DidResolver for DidPeerResolver {
26    fn method(&self) -> &str {
27        "peer"
28    }
29
30    async fn resolve(&self, did: &str) -> baseid_core::Result<ResolutionResult> {
31        if !did.starts_with("did:peer:") {
32            return Err(DidError::UnsupportedMethod.into());
33        }
34
35        let method_id = &did["did:peer:".len()..];
36        if method_id.is_empty() {
37            return Err(DidError::InvalidDid.into());
38        }
39
40        let numalgo = method_id.chars().next().unwrap();
41        let document = match numalgo {
42            '0' => resolve_numalgo_0(did, &method_id[1..])?,
43            '2' => resolve_numalgo_2(did, &method_id[1..])?,
44            '3' => {
45                // Numalgo 3 requires prior exchange — return minimal doc
46                return Err(DidError::NotFound.into());
47            }
48            '4' => resolve_numalgo_4(did, &method_id[1..])?,
49            _ => return Err(DidError::InvalidDid.into()),
50        };
51
52        Ok(ResolutionResult {
53            document: Some(document),
54            metadata: ResolutionMetadata {
55                content_type: Some("application/did+ld+json".to_string()),
56                error: None,
57            },
58        })
59    }
60}
61
62impl DidPeerResolver {
63    /// Create a did:peer:0 from a single public key (like did:key).
64    pub fn create_peer_0(public_key: &PublicKey) -> baseid_core::Result<DidDocument> {
65        let multibase = public_key.to_multibase();
66        let did = format!("did:peer:0{multibase}");
67        resolve_numalgo_0(&did, &multibase)
68    }
69
70    /// Create a did:peer:2 from keys and optional services.
71    ///
72    /// Each key entry is (purpose_char, public_key). Purpose chars:
73    /// - 'V' = authentication
74    /// - 'E' = keyAgreement
75    /// - 'A' = assertionMethod
76    ///
77    /// Services are JSON values with abbreviated keys.
78    pub fn create_peer_2(
79        keys: &[(char, &PublicKey)],
80        services: &[serde_json::Value],
81    ) -> baseid_core::Result<(String, DidDocument)> {
82        let mut did = "did:peer:2".to_string();
83
84        for (purpose, key) in keys {
85            let multibase = key.to_multibase();
86            did.push('.');
87            did.push(*purpose);
88            did.push_str(&multibase);
89        }
90
91        for svc in services {
92            let json_str = serde_json::to_string(&abbreviate_service(svc))
93                .map_err(|_| DidError::InvalidDid)?;
94            let encoded = URL_SAFE_NO_PAD.encode(json_str.as_bytes());
95            did.push_str(".S");
96            did.push_str(&encoded);
97        }
98
99        let method_id = &did["did:peer:2".len()..];
100        let doc = resolve_numalgo_2(&did, method_id)?;
101        Ok((did, doc))
102    }
103
104    /// Create a did:peer:3 from a did:peer:2 (short hash form).
105    pub fn create_peer_3(peer2_did: &str) -> baseid_core::Result<String> {
106        if !peer2_did.starts_with("did:peer:2") {
107            return Err(DidError::InvalidDid.into());
108        }
109        let method_id = &peer2_did["did:peer:2".len()..];
110        let hash = Sha256::digest(method_id.as_bytes());
111        // Multihash: SHA-256 = 0x12, length = 0x20
112        let mut multihash = vec![0x12, 0x20];
113        multihash.extend_from_slice(&hash);
114        let multibase = format!("z{}", bs58::encode(&multihash).into_string());
115        Ok(format!("did:peer:3{multibase}"))
116    }
117
118    /// Create a did:peer:4 from a DID document (long form + short form).
119    pub fn create_peer_4(doc: &DidDocument) -> baseid_core::Result<(String, String)> {
120        // Serialize doc without id
121        let mut doc_json = serde_json::to_value(doc).map_err(|_| DidError::InvalidDid)?;
122        if let Some(obj) = doc_json.as_object_mut() {
123            obj.remove("id");
124        }
125        let doc_bytes = serde_json::to_vec(&doc_json).map_err(|_| DidError::InvalidDid)?;
126
127        // Multicodec for JSON: 0x0200
128        let mut encoded = vec![0x02, 0x00];
129        encoded.extend_from_slice(&doc_bytes);
130        let encoded_multibase = format!("z{}", bs58::encode(&encoded).into_string());
131
132        // Hash for short form
133        let hash = Sha256::digest(&encoded);
134        let mut multihash = vec![0x12, 0x20];
135        multihash.extend_from_slice(&hash);
136        let hash_multibase = format!("z{}", bs58::encode(&multihash).into_string());
137
138        let long_form = format!("did:peer:4{hash_multibase}:{encoded_multibase}");
139        let short_form = format!("did:peer:4{hash_multibase}");
140
141        Ok((long_form, short_form))
142    }
143}
144
145// ── Numalgo 0: Inception Key ─────────────────────────────────
146
147fn resolve_numalgo_0(did: &str, multibase_key: &str) -> baseid_core::Result<DidDocument> {
148    let _public_key =
149        PublicKey::from_multibase(multibase_key).map_err(|_| DidError::ResolutionFailed)?;
150
151    let vm_id = format!("{did}#{multibase_key}");
152    let vm = VerificationMethod {
153        id: vm_id.clone(),
154        r#type: "Multikey".to_string(),
155        controller: did.to_string(),
156        public_key_jwk: None,
157        public_key_multibase: Some(multibase_key.to_string()),
158    };
159
160    Ok(DidDocument {
161        id: did.to_string(),
162        context: vec![DID_CONTEXT.to_string(), MULTIKEY_CONTEXT.to_string()],
163        verification_method: vec![vm],
164        authentication: vec![VerificationRelationship::Reference(vm_id.clone())],
165        assertion_method: vec![VerificationRelationship::Reference(vm_id)],
166        key_agreement: vec![],
167        service: vec![],
168    })
169}
170
171// ── Numalgo 2: Multiple Keys + Services ──────────────────────
172
173fn resolve_numalgo_2(did: &str, elements_str: &str) -> baseid_core::Result<DidDocument> {
174    let mut verification_methods = Vec::new();
175    let mut authentication = Vec::new();
176    let mut assertion_method = Vec::new();
177    let mut key_agreement = Vec::new();
178    let mut services = Vec::new();
179    let mut key_index = 1u32;
180    let mut svc_index = 0u32;
181
182    // Split on '.' (skip empty elements from leading '.')
183    let elements: Vec<&str> = elements_str.split('.').filter(|s| !s.is_empty()).collect();
184
185    for element in &elements {
186        if element.is_empty() {
187            continue;
188        }
189
190        let purpose = element.chars().next().unwrap();
191        let value = &element[1..];
192
193        if purpose == 'S' {
194            // Service: base64url-decoded JSON
195            let decoded = URL_SAFE_NO_PAD
196                .decode(value.as_bytes())
197                .map_err(|_| DidError::ResolutionFailed)?;
198            let svc_json: serde_json::Value =
199                serde_json::from_slice(&decoded).map_err(|_| DidError::ResolutionFailed)?;
200            let expanded = expand_service(&svc_json);
201
202            let svc_id = if svc_index == 0 {
203                format!("{did}#service")
204            } else {
205                format!("{did}#service-{svc_index}")
206            };
207            svc_index += 1;
208
209            let svc_type = expanded
210                .get("type")
211                .and_then(|v| v.as_str())
212                .unwrap_or("DIDCommMessaging")
213                .to_string();
214            let endpoint = expanded
215                .get("serviceEndpoint")
216                .and_then(|v| v.as_str())
217                .unwrap_or("")
218                .to_string();
219
220            services.push(Service {
221                id: svc_id,
222                r#type: svc_type,
223                service_endpoint: ServiceEndpoint::Uri(endpoint),
224            });
225        } else {
226            // Key: purpose char + multibase-encoded key
227            let vm_id = format!("{did}#key-{key_index}");
228            key_index += 1;
229
230            let vm = VerificationMethod {
231                id: vm_id.clone(),
232                r#type: "Multikey".to_string(),
233                controller: did.to_string(),
234                public_key_jwk: None,
235                public_key_multibase: Some(value.to_string()),
236            };
237            verification_methods.push(vm);
238
239            let rel = VerificationRelationship::Reference(vm_id);
240            match purpose {
241                'V' => authentication.push(rel),
242                'E' => key_agreement.push(rel),
243                'A' => assertion_method.push(rel),
244                'I' | 'D' => { /* capabilityInvocation / capabilityDelegation */ }
245                _ => {}
246            }
247        }
248    }
249
250    Ok(DidDocument {
251        id: did.to_string(),
252        context: vec![DID_CONTEXT.to_string(), MULTIKEY_CONTEXT.to_string()],
253        verification_method: verification_methods,
254        authentication,
255        assertion_method,
256        key_agreement,
257        service: services,
258    })
259}
260
261// ── Numalgo 4: Long Form + Short Form ────────────────────────
262
263fn resolve_numalgo_4(did: &str, rest: &str) -> baseid_core::Result<DidDocument> {
264    // Split on ':' — first part is hash, second is encoded document
265    let parts: Vec<&str> = rest.splitn(2, ':').collect();
266    if parts.len() != 2 {
267        // Short form — requires stored long form
268        return Err(DidError::NotFound.into());
269    }
270
271    let hash_multibase = parts[0];
272    let encoded_multibase = parts[1];
273
274    // Decode the encoded document
275    if !encoded_multibase.starts_with('z') {
276        return Err(DidError::InvalidDid.into());
277    }
278    let encoded_bytes = bs58::decode(&encoded_multibase[1..])
279        .into_vec()
280        .map_err(|_| DidError::InvalidDid)?;
281
282    // Strip multicodec JSON prefix (0x02, 0x00)
283    if encoded_bytes.len() < 2 || encoded_bytes[0] != 0x02 || encoded_bytes[1] != 0x00 {
284        return Err(DidError::InvalidDid.into());
285    }
286    let doc_bytes = &encoded_bytes[2..];
287
288    // Verify hash
289    let computed_hash = Sha256::digest(&encoded_bytes);
290    let mut expected_multihash = vec![0x12, 0x20];
291    expected_multihash.extend_from_slice(&computed_hash);
292    let expected_multibase = format!("z{}", bs58::encode(&expected_multihash).into_string());
293
294    if hash_multibase != expected_multibase {
295        return Err(DidError::ResolutionFailed.into());
296    }
297
298    // Parse as JSON value first (the stored doc has `id` removed), inject `id`,
299    // then deserialize to DidDocument.
300    let mut doc_json: serde_json::Value =
301        serde_json::from_slice(doc_bytes).map_err(|_| DidError::ResolutionFailed)?;
302
303    if let Some(obj) = doc_json.as_object_mut() {
304        obj.insert("id".to_string(), serde_json::Value::String(did.to_string()));
305    }
306
307    let doc: DidDocument =
308        serde_json::from_value(doc_json).map_err(|_| DidError::ResolutionFailed)?;
309
310    Ok(doc)
311}
312
313// ── Service Abbreviation ─────────────────────────────────────
314
315fn abbreviate_service(svc: &serde_json::Value) -> serde_json::Value {
316    let mut result = serde_json::Map::new();
317    if let Some(obj) = svc.as_object() {
318        for (k, v) in obj {
319            let key = match k.as_str() {
320                "type" => "t",
321                "serviceEndpoint" => "s",
322                "routingKeys" => "r",
323                "accept" => "a",
324                other => other,
325            };
326            let val = if k == "type" {
327                match v.as_str() {
328                    Some("DIDCommMessaging") => serde_json::Value::String("dm".to_string()),
329                    _ => v.clone(),
330                }
331            } else {
332                v.clone()
333            };
334            result.insert(key.to_string(), val);
335        }
336    }
337    serde_json::Value::Object(result)
338}
339
340fn expand_service(svc: &serde_json::Value) -> serde_json::Value {
341    let mut result = serde_json::Map::new();
342    if let Some(obj) = svc.as_object() {
343        for (k, v) in obj {
344            let key = match k.as_str() {
345                "t" => "type",
346                "s" => "serviceEndpoint",
347                "r" => "routingKeys",
348                "a" => "accept",
349                other => other,
350            };
351            let val = if key == "type" {
352                match v.as_str() {
353                    Some("dm") => serde_json::Value::String("DIDCommMessaging".to_string()),
354                    _ => v.clone(),
355                }
356            } else {
357                v.clone()
358            };
359            result.insert(key.to_string(), val);
360        }
361    }
362    serde_json::Value::Object(result)
363}
364
365// ── bs58 encoding (inline, no external dep) ──────────────────
366
367mod bs58 {
368    const ALPHABET: &[u8; 58] = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
369
370    pub struct Encoder(Vec<u8>);
371
372    pub fn encode(data: &[u8]) -> Encoder {
373        Encoder(data.to_vec())
374    }
375
376    impl Encoder {
377        pub fn into_string(self) -> String {
378            let mut result = Vec::new();
379            let mut data = self.0;
380
381            // Count leading zeros
382            let leading_zeros = data.iter().take_while(|&&b| b == 0).count();
383
384            // Convert to base58
385            while !data.is_empty() {
386                let mut remainder = 0u32;
387                let mut new_data = Vec::new();
388                for &byte in &data {
389                    let acc = (remainder << 8) | byte as u32;
390                    let digit = acc / 58;
391                    remainder = acc % 58;
392                    if !new_data.is_empty() || digit > 0 {
393                        new_data.push(digit as u8);
394                    }
395                }
396                result.push(ALPHABET[remainder as usize]);
397                data = new_data;
398            }
399
400            // Add leading '1's for leading zeros
401            result.extend(std::iter::repeat_n(b'1', leading_zeros));
402
403            result.reverse();
404            String::from_utf8(result).unwrap_or_default()
405        }
406    }
407
408    pub struct Decoder(String);
409
410    pub fn decode(s: &str) -> Decoder {
411        Decoder(s.to_string())
412    }
413
414    impl Decoder {
415        pub fn into_vec(self) -> Result<Vec<u8>, String> {
416            let mut result: Vec<u8> = Vec::new();
417            let leading_ones = self.0.bytes().take_while(|&b| b == b'1').count();
418
419            for ch in self.0.bytes() {
420                let pos = ALPHABET
421                    .iter()
422                    .position(|&a| a == ch)
423                    .ok_or_else(|| format!("invalid base58 char: {}", ch as char))?;
424                let mut carry = pos as u32;
425                for byte in result.iter_mut().rev() {
426                    carry += *byte as u32 * 58;
427                    *byte = (carry & 0xff) as u8;
428                    carry >>= 8;
429                }
430                while carry > 0 {
431                    result.insert(0, (carry & 0xff) as u8);
432                    carry >>= 8;
433                }
434            }
435
436            // Prepend zeros for leading '1's
437            let mut final_result = vec![0u8; leading_ones];
438            final_result.extend(result);
439            Ok(final_result)
440        }
441    }
442}
443
444#[cfg(test)]
445mod tests {
446    use super::*;
447    use baseid_core::types::KeyType;
448    use baseid_crypto::KeyPair;
449    use serde_json::json;
450
451    #[test]
452    fn peer_0_create_and_resolve() {
453        let kp = KeyPair::generate(KeyType::Ed25519).unwrap();
454        let doc = DidPeerResolver::create_peer_0(&kp.public).unwrap();
455        assert!(doc.id.starts_with("did:peer:0z6Mk"));
456        assert_eq!(doc.verification_method.len(), 1);
457        assert_eq!(doc.authentication.len(), 1);
458    }
459
460    #[tokio::test]
461    async fn peer_0_resolve_roundtrip() {
462        let kp = KeyPair::generate(KeyType::Ed25519).unwrap();
463        let doc = DidPeerResolver::create_peer_0(&kp.public).unwrap();
464        let resolver = DidPeerResolver;
465        let result = resolver.resolve(&doc.id).await.unwrap();
466        let resolved = result.document.unwrap();
467        assert_eq!(resolved.id, doc.id);
468        assert_eq!(resolved.verification_method.len(), 1);
469    }
470
471    #[test]
472    fn peer_2_create_with_keys_and_service() {
473        let auth_kp = KeyPair::generate(KeyType::Ed25519).unwrap();
474        let enc_kp = KeyPair::generate(KeyType::P256).unwrap();
475
476        let service = json!({
477            "type": "DIDCommMessaging",
478            "serviceEndpoint": "https://example.com/didcomm",
479            "routingKeys": [],
480            "accept": ["didcomm/v2"]
481        });
482
483        let (did, doc) = DidPeerResolver::create_peer_2(
484            &[('V', &auth_kp.public), ('E', &enc_kp.public)],
485            &[service],
486        )
487        .unwrap();
488
489        assert!(did.starts_with("did:peer:2.V"));
490        assert!(did.contains(".E"));
491        assert!(did.contains(".S"));
492        assert_eq!(doc.verification_method.len(), 2);
493        assert_eq!(doc.authentication.len(), 1);
494        assert_eq!(doc.key_agreement.len(), 1);
495        assert_eq!(doc.service.len(), 1);
496        assert_eq!(doc.service[0].r#type, "DIDCommMessaging");
497    }
498
499    #[tokio::test]
500    async fn peer_2_resolve_roundtrip() {
501        let kp = KeyPair::generate(KeyType::Ed25519).unwrap();
502        let svc = json!({"type": "DIDCommMessaging", "serviceEndpoint": "https://example.com"});
503        let (did, _) = DidPeerResolver::create_peer_2(&[('V', &kp.public)], &[svc]).unwrap();
504
505        let resolver = DidPeerResolver;
506        let result = resolver.resolve(&did).await.unwrap();
507        let doc = result.document.unwrap();
508        assert_eq!(doc.verification_method.len(), 1);
509        assert_eq!(doc.service.len(), 1);
510    }
511
512    #[test]
513    fn peer_2_service_abbreviation() {
514        let svc = json!({"type": "DIDCommMessaging", "serviceEndpoint": "https://x.com", "accept": ["didcomm/v2"]});
515        let abbreviated = abbreviate_service(&svc);
516        assert_eq!(abbreviated["t"], "dm");
517        assert_eq!(abbreviated["s"], "https://x.com");
518        assert_eq!(abbreviated["a"], json!(["didcomm/v2"]));
519
520        let expanded = expand_service(&abbreviated);
521        assert_eq!(expanded["type"], "DIDCommMessaging");
522        assert_eq!(expanded["serviceEndpoint"], "https://x.com");
523    }
524
525    #[test]
526    fn peer_3_from_peer_2() {
527        let kp = KeyPair::generate(KeyType::Ed25519).unwrap();
528        let (peer2, _) = DidPeerResolver::create_peer_2(&[('V', &kp.public)], &[]).unwrap();
529        let peer3 = DidPeerResolver::create_peer_3(&peer2).unwrap();
530        assert!(peer3.starts_with("did:peer:3z"));
531        // Different peer:2 should produce different peer:3
532        let kp2 = KeyPair::generate(KeyType::Ed25519).unwrap();
533        let (peer2b, _) = DidPeerResolver::create_peer_2(&[('V', &kp2.public)], &[]).unwrap();
534        let peer3b = DidPeerResolver::create_peer_3(&peer2b).unwrap();
535        assert_ne!(peer3, peer3b);
536    }
537
538    #[test]
539    fn peer_4_create_and_resolve() {
540        let kp = KeyPair::generate(KeyType::Ed25519).unwrap();
541        let multibase = kp.public.to_multibase();
542        let doc = DidDocument {
543            id: String::new(),
544            context: vec![DID_CONTEXT.to_string()],
545            verification_method: vec![VerificationMethod {
546                id: "#key-1".to_string(),
547                r#type: "Multikey".to_string(),
548                controller: String::new(),
549                public_key_jwk: None,
550                public_key_multibase: Some(multibase),
551            }],
552            authentication: vec![VerificationRelationship::Reference("#key-1".to_string())],
553            assertion_method: vec![],
554            key_agreement: vec![],
555            service: vec![],
556        };
557
558        let (long_form, short_form) = DidPeerResolver::create_peer_4(&doc).unwrap();
559        assert!(long_form.starts_with("did:peer:4z"));
560        assert!(long_form.contains(':'));
561        assert!(short_form.starts_with("did:peer:4z"));
562        // Short form has no colon after 4z...
563        assert!(!short_form[10..].contains(':'));
564    }
565
566    #[tokio::test]
567    async fn peer_4_resolve_long_form() {
568        let kp = KeyPair::generate(KeyType::Ed25519).unwrap();
569        let multibase = kp.public.to_multibase();
570        let doc = DidDocument {
571            id: String::new(),
572            context: vec![DID_CONTEXT.to_string()],
573            verification_method: vec![VerificationMethod {
574                id: "#key-1".to_string(),
575                r#type: "Multikey".to_string(),
576                controller: String::new(),
577                public_key_jwk: None,
578                public_key_multibase: Some(multibase),
579            }],
580            authentication: vec![],
581            assertion_method: vec![],
582            key_agreement: vec![],
583            service: vec![],
584        };
585
586        let (long_form, _) = DidPeerResolver::create_peer_4(&doc).unwrap();
587        let resolver = DidPeerResolver;
588        let result = resolver.resolve(&long_form).await.unwrap();
589        let resolved = result.document.unwrap();
590        assert_eq!(resolved.verification_method.len(), 1);
591    }
592
593    #[tokio::test]
594    async fn peer_4_short_form_requires_stored() {
595        let resolver = DidPeerResolver;
596        let result = resolver.resolve("did:peer:4zQmSomeShortFormHash").await;
597        // Short form without stored long form should return NotFound
598        assert!(result.is_err());
599    }
600
601    #[tokio::test]
602    async fn reject_invalid_peer_did() {
603        let resolver = DidPeerResolver;
604        assert!(resolver.resolve("did:peer:9invalid").await.is_err());
605        assert!(resolver.resolve("did:peer:").await.is_err());
606        assert!(resolver.resolve("did:key:z6Mk123").await.is_err());
607    }
608
609    #[test]
610    fn peer_2_multiple_services() {
611        let kp = KeyPair::generate(KeyType::Ed25519).unwrap();
612        let svc1 = json!({"type": "DIDCommMessaging", "serviceEndpoint": "https://a.com"});
613        let svc2 = json!({"type": "LinkedDomains", "serviceEndpoint": "https://b.com"});
614        let (_, doc) = DidPeerResolver::create_peer_2(&[('V', &kp.public)], &[svc1, svc2]).unwrap();
615        assert_eq!(doc.service.len(), 2);
616        assert_eq!(doc.service[0].id, format!("{}#service", doc.id));
617        assert_eq!(doc.service[1].id, format!("{}#service-1", doc.id));
618    }
619}