igc_net/identity/
did_key.rs1use 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}