Skip to main content

igc_net/identity/
did_key.rs

1use std::borrow::Borrow;
2use std::fmt;
3use std::ops::Deref;
4use std::str::FromStr;
5
6use serde::{Deserialize, Deserializer, Serialize, Serializer};
7
8const DID_KEY_PREFIX: &str = "did:key:z";
9const ED25519_MULTICODEC_PREFIX: [u8; 2] = [0xed, 0x01];
10const BASE58_ALPHABET: &[u8; 58] = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
11
12#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
13pub enum DidKeyError {
14    #[error("invalid did:key: {0:?}")]
15    InvalidFormat(String),
16    #[error("did:key is not an Ed25519 multicodec")]
17    UnsupportedCodec,
18    #[error("did:key does not contain a valid Ed25519 public key")]
19    InvalidPublicKey,
20}
21
22#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
23pub struct DidKey(String);
24
25impl DidKey {
26    pub fn parse(value: impl Into<String>) -> Result<Self, DidKeyError> {
27        let value = value.into();
28        parse_did_key_public_key(&value)?;
29        Ok(Self(value))
30    }
31
32    pub fn from_public_key(public_key: iroh::PublicKey) -> Self {
33        let mut multicodec_bytes = Vec::with_capacity(34);
34        multicodec_bytes.extend_from_slice(&ED25519_MULTICODEC_PREFIX);
35        multicodec_bytes.extend_from_slice(public_key.as_bytes());
36        Self(format!(
37            "{DID_KEY_PREFIX}{}",
38            encode_base58btc(&multicodec_bytes)
39        ))
40    }
41
42    pub fn as_str(&self) -> &str {
43        &self.0
44    }
45
46    pub fn into_string(self) -> String {
47        self.0
48    }
49
50    pub fn public_key(&self) -> iroh::PublicKey {
51        parse_did_key_public_key(self.as_str()).expect("validated did:key")
52    }
53
54    pub fn method_specific_id(&self) -> &str {
55        self.0
56            .strip_prefix("did:key:")
57            .expect("validated did:key prefix")
58    }
59
60    pub fn key_id_fragment(&self) -> &str {
61        self.method_specific_id()
62    }
63
64    pub fn key_id(&self) -> String {
65        format!("{}#{}", self.as_str(), self.key_id_fragment())
66    }
67}
68
69impl fmt::Display for DidKey {
70    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71        self.0.fmt(f)
72    }
73}
74
75impl Deref for DidKey {
76    type Target = str;
77
78    fn deref(&self) -> &Self::Target {
79        self.as_str()
80    }
81}
82
83impl Borrow<str> for DidKey {
84    fn borrow(&self) -> &str {
85        self.as_str()
86    }
87}
88
89impl FromStr for DidKey {
90    type Err = DidKeyError;
91
92    fn from_str(s: &str) -> Result<Self, Self::Err> {
93        Self::parse(s)
94    }
95}
96
97impl TryFrom<String> for DidKey {
98    type Error = DidKeyError;
99
100    fn try_from(value: String) -> Result<Self, Self::Error> {
101        Self::parse(value)
102    }
103}
104
105impl TryFrom<&str> for DidKey {
106    type Error = DidKeyError;
107
108    fn try_from(value: &str) -> Result<Self, Self::Error> {
109        Self::parse(value)
110    }
111}
112
113impl From<DidKey> for String {
114    fn from(value: DidKey) -> Self {
115        value.into_string()
116    }
117}
118
119impl Serialize for DidKey {
120    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
121    where
122        S: Serializer,
123    {
124        serializer.serialize_str(self.as_str())
125    }
126}
127
128impl<'de> Deserialize<'de> for DidKey {
129    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
130    where
131        D: Deserializer<'de>,
132    {
133        let value = String::deserialize(deserializer)?;
134        Self::parse(value).map_err(serde::de::Error::custom)
135    }
136}
137
138fn parse_did_key_public_key(value: &str) -> Result<iroh::PublicKey, DidKeyError> {
139    let encoded = value
140        .strip_prefix(DID_KEY_PREFIX)
141        .ok_or_else(|| DidKeyError::InvalidFormat(value.to_string()))?;
142    if encoded.is_empty() {
143        return Err(DidKeyError::InvalidFormat(value.to_string()));
144    }
145
146    let decoded =
147        decode_base58btc(encoded).map_err(|_| DidKeyError::InvalidFormat(value.to_string()))?;
148    if decoded.len() != 34 || decoded[..2] != ED25519_MULTICODEC_PREFIX {
149        return Err(DidKeyError::UnsupportedCodec);
150    }
151
152    let public_key_bytes: [u8; 32] = decoded[2..]
153        .try_into()
154        .map_err(|_| DidKeyError::InvalidPublicKey)?;
155    iroh::PublicKey::from_bytes(&public_key_bytes).map_err(|_| DidKeyError::InvalidPublicKey)
156}
157
158fn decode_base58btc(input: &str) -> Result<Vec<u8>, ()> {
159    let mut bytes = Vec::<u8>::new();
160    for ch in input.bytes() {
161        let value = base58_value(ch).ok_or(())? as u32;
162        let mut carry = value;
163        for byte in bytes.iter_mut().rev() {
164            let accum = (*byte as u32) * 58 + carry;
165            *byte = (accum & 0xff) as u8;
166            carry = accum >> 8;
167        }
168        while carry > 0 {
169            bytes.insert(0, (carry & 0xff) as u8);
170            carry >>= 8;
171        }
172    }
173
174    let leading_zero_count = input.bytes().take_while(|byte| *byte == b'1').count();
175    let mut decoded = vec![0u8; leading_zero_count];
176    decoded.extend(bytes);
177    Ok(decoded)
178}
179
180fn encode_base58btc(bytes: &[u8]) -> String {
181    if bytes.is_empty() {
182        return String::new();
183    }
184
185    let mut digits = Vec::<u8>::new();
186    for byte in bytes {
187        let mut carry = *byte as u32;
188        for digit in digits.iter_mut().rev() {
189            let accum = (*digit as u32) * 256 + carry;
190            *digit = (accum % 58) as u8;
191            carry = accum / 58;
192        }
193        while carry > 0 {
194            digits.insert(0, (carry % 58) as u8);
195            carry /= 58;
196        }
197    }
198
199    let mut encoded = String::with_capacity(bytes.len() * 2);
200    for _ in bytes.iter().take_while(|byte| **byte == 0) {
201        encoded.push('1');
202    }
203    for digit in digits {
204        encoded.push(BASE58_ALPHABET[digit as usize] as char);
205    }
206    encoded
207}
208
209fn base58_value(ch: u8) -> Option<u8> {
210    BASE58_ALPHABET
211        .iter()
212        .position(|candidate| *candidate == ch)
213        .map(|value| value as u8)
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    #[test]
221    fn round_trip_ed25519_did_key() {
222        let secret_key = iroh::SecretKey::from_bytes(&[7u8; 32]);
223        let did_key = DidKey::from_public_key(secret_key.public());
224
225        let parsed = DidKey::parse(did_key.as_str()).unwrap();
226        assert_eq!(parsed, did_key);
227        assert_eq!(parsed.public_key(), secret_key.public());
228    }
229
230    #[test]
231    fn rejects_non_base58_input() {
232        let err = DidKey::parse("did:key:z0OIl").unwrap_err();
233        assert!(matches!(err, DidKeyError::InvalidFormat(_)));
234    }
235
236    #[test]
237    fn rejects_non_ed25519_multicodec() {
238        let bad = format!("did:key:z{}", encode_base58btc(&[0x12, 0x20, 0u8]));
239        let err = DidKey::parse(bad).unwrap_err();
240        assert!(matches!(err, DidKeyError::UnsupportedCodec));
241    }
242
243    #[test]
244    fn key_id_uses_did_url_fragment_form() {
245        let did_key = DidKey::from_public_key(iroh::SecretKey::from_bytes(&[8u8; 32]).public());
246        assert_eq!(
247            did_key.key_id(),
248            format!("{}#{}", did_key.as_str(), did_key.key_id_fragment())
249        );
250        assert_eq!(did_key.key_id_fragment(), did_key.method_specific_id());
251    }
252}