Skip to main content

affinidi_did_common/did_method/
peer.rs

1//! did:peer specific types and validation
2//!
3//! Implements validation for did:peer method per the spec:
4//! https://identity.foundation/peer-did-method-spec/
5
6use std::collections::HashMap;
7
8use base64::{Engine, prelude::BASE64_URL_SAFE_NO_PAD};
9use serde::{Deserialize, Serialize};
10use thiserror::Error;
11
12use crate::one_or_many::OneOrMany;
13
14/// Peer DID algorithm number (numalgo)
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
16pub enum PeerNumAlgo {
17    /// Type 0: Inception key (wraps a did:key)
18    InceptionKey = 0,
19    /// Type 1: Genesis document (not widely supported)
20    GenesisDoc = 1,
21    /// Type 2: Multiple inline keys
22    MultipleKeys = 2,
23}
24
25impl PeerNumAlgo {
26    /// Parse numalgo from the first character of method-specific-id
27    pub fn from_char(c: char) -> Option<Self> {
28        match c {
29            '0' => Some(PeerNumAlgo::InceptionKey),
30            '1' => Some(PeerNumAlgo::GenesisDoc),
31            '2' => Some(PeerNumAlgo::MultipleKeys),
32            _ => None,
33        }
34    }
35
36    /// Convert to character representation
37    pub fn to_char(self) -> char {
38        match self {
39            PeerNumAlgo::InceptionKey => '0',
40            PeerNumAlgo::GenesisDoc => '1',
41            PeerNumAlgo::MultipleKeys => '2',
42        }
43    }
44}
45
46/// Purpose codes for did:peer type 2 key entries
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum PeerPurpose {
49    /// Assertion method
50    Assertion,
51    /// Capability delegation
52    Delegation,
53    /// Key agreement (encryption)
54    Encryption,
55    /// Capability invocation
56    Invocation,
57    /// Authentication (verification)
58    Verification,
59    /// Service endpoint
60    Service,
61}
62
63impl PeerPurpose {
64    /// Parse purpose from character
65    pub fn from_char(c: char) -> Option<Self> {
66        match c {
67            'A' => Some(PeerPurpose::Assertion),
68            'D' => Some(PeerPurpose::Delegation),
69            'E' => Some(PeerPurpose::Encryption),
70            'I' => Some(PeerPurpose::Invocation),
71            'V' => Some(PeerPurpose::Verification),
72            'S' => Some(PeerPurpose::Service),
73            _ => None,
74        }
75    }
76
77    /// Convert to character representation
78    pub fn to_char(self) -> char {
79        match self {
80            PeerPurpose::Assertion => 'A',
81            PeerPurpose::Delegation => 'D',
82            PeerPurpose::Encryption => 'E',
83            PeerPurpose::Invocation => 'I',
84            PeerPurpose::Verification => 'V',
85            PeerPurpose::Service => 'S',
86        }
87    }
88
89    /// Returns true if this purpose represents a key (not a service)
90    pub fn is_key(&self) -> bool {
91        !matches!(self, PeerPurpose::Service)
92    }
93}
94
95// ============================================================================
96// Error Types
97// ============================================================================
98
99/// Errors specific to did:peer operations
100#[derive(Error, Debug)]
101pub enum PeerError {
102    #[error("Unsupported key type")]
103    UnsupportedKeyType,
104
105    #[error("Unsupported curve: {0}")]
106    UnsupportedCurve(String),
107
108    #[error("Syntax error in service definition: {0}")]
109    ServiceSyntaxError(String),
110
111    #[error("Unsupported numalgo. Only 0 and 2 are supported")]
112    UnsupportedNumalgo,
113
114    #[error("Key parsing error: {0}")]
115    KeyParsingError(String),
116
117    #[error("Encoding error: {0}")]
118    EncodingError(String),
119
120    #[error("Internal error: {0}")]
121    InternalError(String),
122}
123
124// ============================================================================
125// Key Types for Generation
126// ============================================================================
127
128/// Purpose of a key when creating a did:peer
129#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
130pub enum PeerKeyPurpose {
131    /// Keys for authentication and assertions (V prefix in DID)
132    Verification,
133    /// Keys for key agreement/encryption (E prefix in DID)
134    Encryption,
135}
136
137impl PeerKeyPurpose {
138    /// Get the DID peer purpose code character
139    pub fn to_char(self) -> char {
140        match self {
141            PeerKeyPurpose::Verification => 'V',
142            PeerKeyPurpose::Encryption => 'E',
143        }
144    }
145}
146
147/// Supported key types for did:peer creation
148#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
149pub enum PeerKeyType {
150    Ed25519,
151    Secp256k1,
152    P256,
153}
154
155impl PeerKeyType {
156    /// Convert to affinidi_crypto::KeyType
157    pub fn to_crypto_key_type(self) -> affinidi_crypto::KeyType {
158        match self {
159            PeerKeyType::Ed25519 => affinidi_crypto::KeyType::Ed25519,
160            PeerKeyType::Secp256k1 => affinidi_crypto::KeyType::Secp256k1,
161            PeerKeyType::P256 => affinidi_crypto::KeyType::P256,
162        }
163    }
164}
165
166/// Key specification for creating a did:peer
167#[derive(Debug, Clone)]
168pub struct PeerCreateKey {
169    /// Purpose of this key (Verification or Encryption)
170    pub purpose: PeerKeyPurpose,
171    /// Key type to generate (required if public_key_multibase is None)
172    pub key_type: Option<PeerKeyType>,
173    /// Pre-existing public key in multibase format (z6Mk...)
174    /// If None, a new key will be generated
175    pub public_key_multibase: Option<String>,
176}
177
178impl PeerCreateKey {
179    /// Create a new key spec for generation
180    pub fn new(purpose: PeerKeyPurpose, key_type: PeerKeyType) -> Self {
181        Self {
182            purpose,
183            key_type: Some(key_type),
184            public_key_multibase: None,
185        }
186    }
187
188    /// Create a key spec from an existing multibase key
189    pub fn from_multibase(purpose: PeerKeyPurpose, multibase: String) -> Self {
190        Self {
191            purpose,
192            key_type: None,
193            public_key_multibase: Some(multibase),
194        }
195    }
196}
197
198/// Result of key generation during did:peer creation
199#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct PeerCreatedKey {
201    /// The multibase-encoded public key (z6Mk...)
202    pub key_multibase: String,
203    /// The elliptic curve used
204    pub curve: String,
205    /// Private key value in Base64URL (no padding)
206    pub d: String,
207    /// Public key X coordinate in Base64URL (no padding)
208    pub x: String,
209    /// Public key Y coordinate for EC keys (None for Ed25519)
210    pub y: Option<String>,
211}
212
213// ============================================================================
214// Service Types
215// ============================================================================
216
217/// Service definition for did:peer
218///
219/// Uses abbreviated format for encoding in the DID string
220#[derive(Debug, Clone, Serialize, Deserialize)]
221pub struct PeerService {
222    /// Service type (e.g., "dm" for DIDCommMessaging)
223    #[serde(rename = "t")]
224    pub type_: String,
225
226    /// Service endpoint
227    #[serde(rename = "s")]
228    pub endpoint: PeerServiceEndpoint,
229
230    /// Optional service ID fragment (e.g., "#my-service")
231    #[serde(skip_serializing_if = "Option::is_none")]
232    pub id: Option<String>,
233}
234
235/// Service endpoint - can be a simple URI or a structured map
236#[derive(Debug, Clone, Serialize, Deserialize)]
237#[serde(untagged)]
238pub enum PeerServiceEndpoint {
239    /// Simple URI endpoint
240    Uri(String),
241    /// Structured endpoint with routing info (long format)
242    Long(OneOrMany<PeerServiceEndpointLong>),
243    /// Structured endpoint with routing info (short format)
244    Short(OneOrMany<PeerServiceEndpointShort>),
245}
246
247/// Short format service endpoint map (for DID encoding)
248#[derive(Debug, Clone, Serialize, Deserialize)]
249pub struct PeerServiceEndpointShort {
250    /// Service URI
251    pub uri: String,
252    /// Accepted message types (abbreviated)
253    #[serde(default, skip_serializing_if = "Vec::is_empty")]
254    pub a: Vec<String>,
255    /// Routing keys (abbreviated)
256    #[serde(default, skip_serializing_if = "Vec::is_empty")]
257    pub r: Vec<String>,
258}
259
260/// Long format service endpoint map (standard DID Document format)
261#[derive(Debug, Clone, Serialize, Deserialize)]
262pub struct PeerServiceEndpointLong {
263    /// Service URI
264    pub uri: String,
265    /// Accepted message types
266    #[serde(default, skip_serializing_if = "Vec::is_empty")]
267    pub accept: Vec<String>,
268    /// Routing keys
269    #[serde(default, skip_serializing_if = "Vec::is_empty")]
270    pub routing_keys: Vec<String>,
271}
272
273impl PeerServiceEndpointShort {
274    /// Convert to long format
275    pub fn to_long(&self) -> PeerServiceEndpointLong {
276        PeerServiceEndpointLong {
277            uri: self.uri.clone(),
278            accept: self.a.clone(),
279            routing_keys: self.r.clone(),
280        }
281    }
282}
283
284impl PeerServiceEndpointLong {
285    /// Convert to short format (for encoding in DID)
286    pub fn to_short(&self) -> PeerServiceEndpointShort {
287        PeerServiceEndpointShort {
288            uri: self.uri.clone(),
289            a: self.accept.clone(),
290            r: self.routing_keys.clone(),
291        }
292    }
293}
294
295// ============================================================================
296// Service Encoding/Decoding
297// ============================================================================
298
299impl PeerService {
300    /// Encode this service for inclusion in a did:peer string
301    pub fn encode(&self) -> Result<String, PeerError> {
302        let json = serde_json::to_string(self).map_err(|e| {
303            PeerError::ServiceSyntaxError(format!("Failed to serialize service: {e}"))
304        })?;
305        Ok(format!(
306            "S{}",
307            BASE64_URL_SAFE_NO_PAD.encode(json.as_bytes())
308        ))
309    }
310
311    /// Decode a service from a did:peer encoded string (including S prefix)
312    pub fn decode(encoded: &str) -> Result<Self, PeerError> {
313        let encoded = encoded.strip_prefix('S').unwrap_or(encoded);
314        let bytes = BASE64_URL_SAFE_NO_PAD
315            .decode(encoded)
316            .map_err(|e| PeerError::ServiceSyntaxError(format!("Base64 decode failed: {e}")))?;
317
318        serde_json::from_slice(&bytes)
319            .map_err(|e| PeerError::ServiceSyntaxError(format!("JSON parse failed: {e}")))
320    }
321
322    /// Convert to standard DID Document Service format
323    pub fn to_did_service(
324        &self,
325        did: &str,
326        index: u32,
327    ) -> Result<crate::service::Service, PeerError> {
328        use std::str::FromStr;
329        use url::Url;
330
331        // Build service ID
332        let id_fragment = if let Some(id) = &self.id {
333            id.clone()
334        } else if index == 0 {
335            "#service".to_string()
336        } else {
337            format!("#service-{index}")
338        };
339
340        let id = Url::from_str(&format!("{did}{id_fragment}"))
341            .map_err(|e| PeerError::ServiceSyntaxError(format!("Invalid service ID: {e}")))?;
342
343        // Convert endpoint to standard format
344        let service_endpoint = match &self.endpoint {
345            PeerServiceEndpoint::Uri(uri) => {
346                let url = Url::from_str(uri)
347                    .map_err(|e| PeerError::ServiceSyntaxError(format!("Invalid URI: {e}")))?;
348                crate::service::Endpoint::Url(url)
349            }
350            PeerServiceEndpoint::Short(endpoints) => {
351                let value = match endpoints {
352                    OneOrMany::One(ep) => serde_json::to_value(ep.to_long())
353                        .map_err(|e| PeerError::ServiceSyntaxError(e.to_string()))?,
354                    OneOrMany::Many(eps) => {
355                        let long: Vec<_> = eps.iter().map(|e| e.to_long()).collect();
356                        serde_json::to_value(long)
357                            .map_err(|e| PeerError::ServiceSyntaxError(e.to_string()))?
358                    }
359                };
360                crate::service::Endpoint::Map(value)
361            }
362            PeerServiceEndpoint::Long(endpoints) => {
363                let value = serde_json::to_value(endpoints)
364                    .map_err(|e| PeerError::ServiceSyntaxError(e.to_string()))?;
365                crate::service::Endpoint::Map(value)
366            }
367        };
368
369        Ok(crate::service::Service {
370            id: Some(id),
371            type_: vec!["DIDCommMessaging".to_string()],
372            service_endpoint,
373            property_set: HashMap::new(),
374        })
375    }
376}
377
378#[cfg(test)]
379mod tests {
380    use super::*;
381
382    // --- PeerNumAlgo ---
383
384    #[test]
385    fn numalgo_from_char_valid() {
386        assert_eq!(PeerNumAlgo::from_char('0'), Some(PeerNumAlgo::InceptionKey));
387        assert_eq!(PeerNumAlgo::from_char('1'), Some(PeerNumAlgo::GenesisDoc));
388        assert_eq!(PeerNumAlgo::from_char('2'), Some(PeerNumAlgo::MultipleKeys));
389    }
390
391    #[test]
392    fn numalgo_from_char_invalid() {
393        assert_eq!(PeerNumAlgo::from_char('3'), None);
394        assert_eq!(PeerNumAlgo::from_char('x'), None);
395    }
396
397    #[test]
398    fn numalgo_to_char() {
399        assert_eq!(PeerNumAlgo::InceptionKey.to_char(), '0');
400        assert_eq!(PeerNumAlgo::GenesisDoc.to_char(), '1');
401        assert_eq!(PeerNumAlgo::MultipleKeys.to_char(), '2');
402    }
403
404    #[test]
405    fn numalgo_roundtrip() {
406        for c in ['0', '1', '2'] {
407            let algo = PeerNumAlgo::from_char(c).unwrap();
408            assert_eq!(algo.to_char(), c);
409        }
410    }
411
412    // --- PeerPurpose ---
413
414    #[test]
415    fn purpose_from_char_valid() {
416        assert_eq!(PeerPurpose::from_char('A'), Some(PeerPurpose::Assertion));
417        assert_eq!(PeerPurpose::from_char('D'), Some(PeerPurpose::Delegation));
418        assert_eq!(PeerPurpose::from_char('E'), Some(PeerPurpose::Encryption));
419        assert_eq!(PeerPurpose::from_char('I'), Some(PeerPurpose::Invocation));
420        assert_eq!(PeerPurpose::from_char('V'), Some(PeerPurpose::Verification));
421        assert_eq!(PeerPurpose::from_char('S'), Some(PeerPurpose::Service));
422    }
423
424    #[test]
425    fn purpose_from_char_invalid() {
426        assert_eq!(PeerPurpose::from_char('X'), None);
427        assert_eq!(PeerPurpose::from_char('a'), None);
428    }
429
430    #[test]
431    fn purpose_to_char() {
432        assert_eq!(PeerPurpose::Assertion.to_char(), 'A');
433        assert_eq!(PeerPurpose::Delegation.to_char(), 'D');
434        assert_eq!(PeerPurpose::Encryption.to_char(), 'E');
435        assert_eq!(PeerPurpose::Invocation.to_char(), 'I');
436        assert_eq!(PeerPurpose::Verification.to_char(), 'V');
437        assert_eq!(PeerPurpose::Service.to_char(), 'S');
438    }
439
440    #[test]
441    fn purpose_roundtrip() {
442        for c in ['A', 'D', 'E', 'I', 'V', 'S'] {
443            let purpose = PeerPurpose::from_char(c).unwrap();
444            assert_eq!(purpose.to_char(), c);
445        }
446    }
447
448    #[test]
449    fn purpose_is_key() {
450        assert!(PeerPurpose::Assertion.is_key());
451        assert!(PeerPurpose::Delegation.is_key());
452        assert!(PeerPurpose::Encryption.is_key());
453        assert!(PeerPurpose::Invocation.is_key());
454        assert!(PeerPurpose::Verification.is_key());
455        assert!(!PeerPurpose::Service.is_key());
456    }
457
458    // --- PeerKeyPurpose ---
459
460    #[test]
461    fn key_purpose_to_char() {
462        assert_eq!(PeerKeyPurpose::Verification.to_char(), 'V');
463        assert_eq!(PeerKeyPurpose::Encryption.to_char(), 'E');
464    }
465
466    // --- PeerKeyType ---
467
468    #[test]
469    fn key_type_to_crypto_key_type() {
470        assert_eq!(
471            PeerKeyType::Ed25519.to_crypto_key_type(),
472            affinidi_crypto::KeyType::Ed25519
473        );
474        assert_eq!(
475            PeerKeyType::Secp256k1.to_crypto_key_type(),
476            affinidi_crypto::KeyType::Secp256k1
477        );
478        assert_eq!(
479            PeerKeyType::P256.to_crypto_key_type(),
480            affinidi_crypto::KeyType::P256
481        );
482    }
483
484    // --- PeerCreateKey ---
485
486    #[test]
487    fn peer_create_key_new() {
488        let k = PeerCreateKey::new(PeerKeyPurpose::Verification, PeerKeyType::Ed25519);
489        assert_eq!(k.purpose, PeerKeyPurpose::Verification);
490        assert_eq!(k.key_type, Some(PeerKeyType::Ed25519));
491        assert!(k.public_key_multibase.is_none());
492    }
493
494    #[test]
495    fn peer_create_key_from_multibase() {
496        let k = PeerCreateKey::from_multibase(
497            PeerKeyPurpose::Encryption,
498            "z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK".to_string(),
499        );
500        assert_eq!(k.purpose, PeerKeyPurpose::Encryption);
501        assert!(k.key_type.is_none());
502        assert_eq!(
503            k.public_key_multibase.as_deref(),
504            Some("z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK")
505        );
506    }
507
508    // --- PeerServiceEndpoint conversion ---
509
510    #[test]
511    fn short_to_long_conversion() {
512        let short = PeerServiceEndpointShort {
513            uri: "https://example.com/didcomm".to_string(),
514            a: vec!["didcomm/v2".to_string()],
515            r: vec!["did:example:123#key-1".to_string()],
516        };
517        let long = short.to_long();
518        assert_eq!(long.uri, "https://example.com/didcomm");
519        assert_eq!(long.accept, vec!["didcomm/v2"]);
520        assert_eq!(long.routing_keys, vec!["did:example:123#key-1"]);
521    }
522
523    #[test]
524    fn long_to_short_conversion() {
525        let long = PeerServiceEndpointLong {
526            uri: "https://example.com/didcomm".to_string(),
527            accept: vec!["didcomm/v2".to_string()],
528            routing_keys: vec!["did:example:123#key-1".to_string()],
529        };
530        let short = long.to_short();
531        assert_eq!(short.uri, "https://example.com/didcomm");
532        assert_eq!(short.a, vec!["didcomm/v2"]);
533        assert_eq!(short.r, vec!["did:example:123#key-1"]);
534    }
535
536    #[test]
537    fn short_long_roundtrip() {
538        let short = PeerServiceEndpointShort {
539            uri: "https://example.com".to_string(),
540            a: vec!["a".to_string(), "b".to_string()],
541            r: vec![],
542        };
543        let roundtripped = short.to_long().to_short();
544        assert_eq!(roundtripped.uri, short.uri);
545        assert_eq!(roundtripped.a, short.a);
546        assert_eq!(roundtripped.r, short.r);
547    }
548
549    // --- PeerService encode/decode ---
550
551    #[test]
552    fn service_encode_decode_roundtrip() {
553        let svc = PeerService {
554            type_: "dm".to_string(),
555            endpoint: PeerServiceEndpoint::Uri("https://example.com/didcomm".to_string()),
556            id: None,
557        };
558        let encoded = svc.encode().unwrap();
559        assert!(encoded.starts_with('S'));
560
561        let decoded = PeerService::decode(&encoded).unwrap();
562        assert_eq!(decoded.type_, "dm");
563        if let PeerServiceEndpoint::Uri(uri) = &decoded.endpoint {
564            assert_eq!(uri, "https://example.com/didcomm");
565        } else {
566            panic!("expected Uri endpoint");
567        }
568    }
569
570    #[test]
571    fn service_decode_without_s_prefix() {
572        let svc = PeerService {
573            type_: "dm".to_string(),
574            endpoint: PeerServiceEndpoint::Uri("https://example.com".to_string()),
575            id: None,
576        };
577        let encoded = svc.encode().unwrap();
578        // Strip the S prefix manually
579        let without_prefix = &encoded[1..];
580        let decoded = PeerService::decode(without_prefix).unwrap();
581        assert_eq!(decoded.type_, "dm");
582    }
583
584    #[test]
585    fn service_decode_invalid_base64() {
586        assert!(PeerService::decode("S!!!invalid!!!").is_err());
587    }
588
589    #[test]
590    fn service_decode_invalid_json() {
591        let encoded = format!("S{}", BASE64_URL_SAFE_NO_PAD.encode(b"not json"));
592        assert!(PeerService::decode(&encoded).is_err());
593    }
594
595    // --- PeerService::to_did_service ---
596
597    #[test]
598    fn to_did_service_with_uri_endpoint() {
599        let svc = PeerService {
600            type_: "dm".to_string(),
601            endpoint: PeerServiceEndpoint::Uri("https://example.com/didcomm".to_string()),
602            id: None,
603        };
604        let did_svc = svc.to_did_service("did:peer:2abc", 0).unwrap();
605        assert_eq!(did_svc.id.unwrap().as_str(), "did:peer:2abc#service");
606        assert_eq!(did_svc.type_, vec!["DIDCommMessaging"]);
607    }
608
609    #[test]
610    fn to_did_service_with_index() {
611        let svc = PeerService {
612            type_: "dm".to_string(),
613            endpoint: PeerServiceEndpoint::Uri("https://example.com".to_string()),
614            id: None,
615        };
616        let did_svc = svc.to_did_service("did:peer:2abc", 3).unwrap();
617        assert_eq!(did_svc.id.unwrap().as_str(), "did:peer:2abc#service-3");
618    }
619
620    #[test]
621    fn to_did_service_with_custom_id() {
622        let svc = PeerService {
623            type_: "dm".to_string(),
624            endpoint: PeerServiceEndpoint::Uri("https://example.com".to_string()),
625            id: Some("#my-svc".to_string()),
626        };
627        let did_svc = svc.to_did_service("did:peer:2abc", 0).unwrap();
628        assert_eq!(did_svc.id.unwrap().as_str(), "did:peer:2abc#my-svc");
629    }
630
631    #[test]
632    fn to_did_service_with_short_endpoint() {
633        let short = PeerServiceEndpointShort {
634            uri: "https://example.com/didcomm".to_string(),
635            a: vec!["didcomm/v2".to_string()],
636            r: vec![],
637        };
638        let svc = PeerService {
639            type_: "dm".to_string(),
640            endpoint: PeerServiceEndpoint::Short(OneOrMany::One(short)),
641            id: None,
642        };
643        let did_svc = svc.to_did_service("did:peer:2abc", 0).unwrap();
644        assert!(matches!(
645            did_svc.service_endpoint,
646            crate::service::Endpoint::Map(_)
647        ));
648    }
649}