Skip to main content

agent_id_core/
did.rs

1//! Decentralized Identifier (DID) handling.
2//!
3//! Format: `did:key:z<base58btc(multicodec_prefix + ed25519_public_key)>`
4//!
5//! Uses the did:key method (W3C standard) with Ed25519 keys.
6//! - Multicodec prefix: 0xed01 (Ed25519 public key)
7//! - Multibase prefix: z (base58btc)
8
9use crate::{Error, Result};
10use ed25519_dalek::VerifyingKey;
11use std::fmt;
12use std::str::FromStr;
13
14/// Ed25519 public key multicodec prefix (varint-encoded 0xed).
15/// See: https://github.com/multiformats/multicodec
16const ED25519_MULTICODEC: [u8; 2] = [0xed, 0x01];
17
18/// Multibase prefix for base58btc.
19const BASE58BTC_PREFIX: char = 'z';
20
21/// A parsed DID (did:key method).
22#[derive(Debug, Clone, PartialEq, Eq, Hash)]
23pub struct Did {
24    public_key: [u8; 32],
25}
26
27impl Did {
28    /// Create a new DID from a public key.
29    pub fn new(public_key: VerifyingKey) -> Self {
30        Self {
31            public_key: public_key.to_bytes(),
32        }
33    }
34
35    /// Get the public key bytes.
36    pub fn public_key_bytes(&self) -> &[u8; 32] {
37        &self.public_key
38    }
39
40    /// Get the public key.
41    pub fn public_key(&self) -> Result<VerifyingKey> {
42        VerifyingKey::from_bytes(&self.public_key).map_err(|e| Error::InvalidDid(e.to_string()))
43    }
44
45    /// Get the multibase-encoded key portion (without did:key: prefix).
46    pub fn key_id(&self) -> String {
47        let mut bytes = Vec::with_capacity(34);
48        bytes.extend_from_slice(&ED25519_MULTICODEC);
49        bytes.extend_from_slice(&self.public_key);
50        format!("{}{}", BASE58BTC_PREFIX, bs58::encode(&bytes).into_string())
51    }
52}
53
54impl fmt::Display for Did {
55    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56        write!(f, "did:key:{}", self.key_id())
57    }
58}
59
60impl FromStr for Did {
61    type Err = Error;
62
63    fn from_str(s: &str) -> Result<Self> {
64        // Parse did:key:z... format
65        let key_part = s
66            .strip_prefix("did:key:")
67            .ok_or_else(|| Error::InvalidDid("must start with 'did:key:'".into()))?;
68
69        // Check multibase prefix (z = base58btc)
70        let encoded = key_part
71            .strip_prefix(BASE58BTC_PREFIX)
72            .ok_or_else(|| Error::InvalidDid("must use base58btc encoding (z prefix)".into()))?;
73
74        // Decode base58
75        let bytes = bs58::decode(encoded)
76            .into_vec()
77            .map_err(|e| Error::Base58(e.to_string()))?;
78
79        // Check length: 2 byte prefix + 32 byte key
80        if bytes.len() != 34 {
81            return Err(Error::InvalidDid(format!(
82                "expected 34 bytes, got {}",
83                bytes.len()
84            )));
85        }
86
87        // Check multicodec prefix
88        if bytes[0..2] != ED25519_MULTICODEC {
89            return Err(Error::InvalidDid(
90                "unsupported key type (expected Ed25519 multicodec 0xed01)".into(),
91            ));
92        }
93
94        // Extract public key
95        let mut public_key = [0u8; 32];
96        public_key.copy_from_slice(&bytes[2..34]);
97
98        // Validate it's a valid Ed25519 point
99        VerifyingKey::from_bytes(&public_key)
100            .map_err(|e| Error::InvalidDid(format!("invalid Ed25519 key: {}", e)))?;
101
102        Ok(Self { public_key })
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109    use ed25519_dalek::SigningKey;
110    use rand::rngs::OsRng;
111
112    #[test]
113    fn test_did_roundtrip() {
114        let signing_key = SigningKey::generate(&mut OsRng);
115        let did = Did::new(signing_key.verifying_key());
116
117        let did_str = did.to_string();
118        assert!(did_str.starts_with("did:key:z6Mk"), "got: {}", did_str);
119
120        let parsed: Did = did_str.parse().unwrap();
121        assert_eq!(did, parsed);
122    }
123
124    #[test]
125    fn test_known_vector() {
126        // Test vector from did:key spec
127        // https://w3c-ccg.github.io/did-method-key/#test-vectors
128        let did_str = "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK";
129        let parsed: Did = did_str.parse().unwrap();
130        assert_eq!(parsed.to_string(), did_str);
131    }
132
133    #[test]
134    fn test_invalid_prefix() {
135        let result: std::result::Result<Did, _> = "did:aip:1:abc".parse();
136        assert!(result.is_err());
137    }
138
139    #[test]
140    fn test_invalid_multibase() {
141        // Wrong multibase prefix (not z)
142        let result: std::result::Result<Did, _> =
143            "did:key:f6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK".parse();
144        assert!(result.is_err());
145    }
146}