1use acdp_primitives::error::AcdpError;
29
30const MULTICODEC_ED25519: [u8; 2] = [0xed, 0x01];
32const MULTICODEC_P256: [u8; 2] = [0x80, 0x24];
34
35#[derive(Debug, Clone, PartialEq, Eq)]
37pub enum DidKeyMaterial {
38 Ed25519([u8; 32]),
40 EcdsaP256([u8; 33]),
42}
43
44impl DidKeyMaterial {
45 pub fn algorithm(&self) -> &'static str {
49 match self {
50 Self::Ed25519(_) => "ed25519",
51 Self::EcdsaP256(_) => "ecdsa-p256",
52 }
53 }
54}
55
56pub fn resolve_did_key(did: &str) -> Result<DidKeyMaterial, AcdpError> {
63 let msi = did
64 .strip_prefix("did:key:")
65 .ok_or_else(|| AcdpError::KeyResolution(format!("not a did:key DID: {did}")))?;
66 decode_multibase_key(msi)
67}
68
69fn decode_multibase_key(msi: &str) -> Result<DidKeyMaterial, AcdpError> {
72 let rest = msi.strip_prefix('z').ok_or_else(|| {
73 AcdpError::KeyResolution(format!(
74 "did:key requires the 'z' (base58btc) multibase prefix, got '{msi}'"
75 ))
76 })?;
77 let decoded = bs58::decode(rest)
78 .into_vec()
79 .map_err(|e| AcdpError::KeyResolution(format!("did:key base58 decode: {e}")))?;
80
81 match decoded.get(0..2) {
82 Some(p) if p == MULTICODEC_ED25519 => {
83 let key: [u8; 32] = decoded[2..].try_into().map_err(|_| {
84 AcdpError::KeyResolution(format!(
85 "did:key ed25519 key must be 32 bytes after the multicodec prefix, got {}",
86 decoded.len().saturating_sub(2)
87 ))
88 })?;
89 Ok(DidKeyMaterial::Ed25519(key))
90 }
91 Some(p) if p == MULTICODEC_P256 => {
92 let key: [u8; 33] = decoded[2..].try_into().map_err(|_| {
93 AcdpError::KeyResolution(format!(
94 "did:key p256 key must be a 33-byte SEC1-compressed point after the \
95 multicodec prefix, got {}",
96 decoded.len().saturating_sub(2)
97 ))
98 })?;
99 if !matches!(key[0], 0x02 | 0x03) {
100 return Err(AcdpError::KeyResolution(
101 "did:key p256 key must be SEC1-compressed (leading 0x02/0x03)".into(),
102 ));
103 }
104 Ok(DidKeyMaterial::EcdsaP256(key))
105 }
106 _ => Err(AcdpError::KeyResolution(
107 "did:key multicodec prefix is neither ed25519-pub (0xed 0x01) \
108 nor p256-pub (0x80 0x24)"
109 .into(),
110 )),
111 }
112}
113
114pub fn resolve_did_key_url(key_id: &str) -> Result<DidKeyMaterial, AcdpError> {
123 let (did_part, fragment) = key_id
124 .split_once('#')
125 .ok_or_else(|| AcdpError::KeyResolution(format!("key_id '{key_id}' has no '#fragment'")))?;
126 let msi = did_part
127 .strip_prefix("did:key:")
128 .ok_or_else(|| AcdpError::KeyResolution(format!("not a did:key DID URL: {key_id}")))?;
129 if fragment != msi {
130 return Err(AcdpError::KeyResolution(format!(
131 "did:key fragment '#{fragment}' must equal the method-specific identifier \
132 '{msi}' (the did:key document's only verification method is the key itself)"
133 )));
134 }
135 decode_multibase_key(msi)
136}
137
138pub fn did_key_from_ed25519(public_key: &[u8; 32]) -> String {
140 let mut prefixed = Vec::with_capacity(2 + 32);
141 prefixed.extend_from_slice(&MULTICODEC_ED25519);
142 prefixed.extend_from_slice(public_key);
143 format!("did:key:z{}", bs58::encode(&prefixed).into_string())
144}
145
146pub fn did_key_from_p256_sec1(sec1: &[u8]) -> Result<String, AcdpError> {
150 let vk = p256::ecdsa::VerifyingKey::from_sec1_bytes(sec1)
151 .map_err(|e| AcdpError::KeyResolution(format!("p256 SEC1 parse: {e}")))?;
152 let compressed = vk.to_encoded_point(true);
153 let mut prefixed = Vec::with_capacity(2 + 33);
154 prefixed.extend_from_slice(&MULTICODEC_P256);
155 prefixed.extend_from_slice(compressed.as_bytes());
156 Ok(format!(
157 "did:key:z{}",
158 bs58::encode(&prefixed).into_string()
159 ))
160}
161
162pub fn did_key_url(did: &str) -> Result<String, AcdpError> {
165 let msi = did
166 .strip_prefix("did:key:")
167 .ok_or_else(|| AcdpError::KeyResolution(format!("not a did:key DID: {did}")))?;
168 Ok(format!("{did}#{msi}"))
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 const TEST_PUB_HEX: &str = "3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29";
177
178 fn test_pub() -> [u8; 32] {
179 hex::decode(TEST_PUB_HEX).unwrap().try_into().unwrap()
180 }
181
182 #[test]
183 fn ed25519_round_trip() {
184 let did = did_key_from_ed25519(&test_pub());
185 assert!(did.starts_with("did:key:z"));
186 match resolve_did_key(&did).unwrap() {
187 DidKeyMaterial::Ed25519(k) => assert_eq!(k, test_pub()),
188 other => panic!("expected Ed25519, got {other:?}"),
189 }
190 }
191
192 #[test]
193 fn p256_round_trip_compresses_uncompressed_input() {
194 let key = acdp_crypto::sign::P256SigningKey::generate();
195 let did = did_key_from_p256_sec1(&key.verifying_key_sec1()).unwrap();
196 match resolve_did_key(&did).unwrap() {
197 DidKeyMaterial::EcdsaP256(k) => {
198 assert_eq!(k.len(), 33);
199 assert!(matches!(k[0], 0x02 | 0x03));
200 let vk = p256::ecdsa::VerifyingKey::from_sec1_bytes(&k).unwrap();
202 assert_eq!(
203 vk.to_encoded_point(false).as_bytes(),
204 key.verifying_key_sec1().as_slice()
205 );
206 }
207 other => panic!("expected EcdsaP256, got {other:?}"),
208 }
209 }
210
211 #[test]
212 fn key_url_fragment_must_equal_msi() {
213 let did = did_key_from_ed25519(&test_pub());
214 let url = did_key_url(&did).unwrap();
215 resolve_did_key_url(&url).unwrap();
216
217 let bad = format!("{did}#key-1");
219 let err = resolve_did_key_url(&bad).unwrap_err();
220 assert!(matches!(err, AcdpError::KeyResolution(_)), "got {err:?}");
221
222 let err = resolve_did_key_url(&did).unwrap_err();
224 assert!(matches!(err, AcdpError::KeyResolution(_)), "got {err:?}");
225 }
226
227 #[test]
228 fn rejects_unknown_multicodec() {
229 let mut prefixed = vec![0xe7, 0x01];
231 prefixed.extend_from_slice(&[0u8; 33]);
232 let did = format!("did:key:z{}", bs58::encode(&prefixed).into_string());
233 let err = resolve_did_key(&did).unwrap_err();
234 assert!(matches!(err, AcdpError::KeyResolution(_)), "got {err:?}");
235 }
236
237 #[test]
238 fn rejects_non_z_multibase_and_garbage() {
239 for bad in ["did:key:uAAAA", "did:key:z!!!not-base58!!!", "did:web:x"] {
240 assert!(
241 resolve_did_key(bad).is_err(),
242 "'{bad}' must fail did:key resolution"
243 );
244 }
245 }
246
247 #[test]
248 fn rejects_wrong_length_ed25519() {
249 let mut prefixed = MULTICODEC_ED25519.to_vec();
250 prefixed.extend_from_slice(&[0u8; 31]); let did = format!("did:key:z{}", bs58::encode(&prefixed).into_string());
252 assert!(resolve_did_key(&did).is_err());
253 }
254
255 #[test]
256 fn rejects_uncompressed_p256_payload() {
257 let key = acdp_crypto::sign::P256SigningKey::generate();
260 let mut prefixed = MULTICODEC_P256.to_vec();
261 prefixed.extend_from_slice(&key.verifying_key_sec1());
262 let did = format!("did:key:z{}", bs58::encode(&prefixed).into_string());
263 assert!(resolve_did_key(&did).is_err());
264 }
265
266 #[test]
267 fn algorithm_strings() {
268 assert_eq!(DidKeyMaterial::Ed25519([0; 32]).algorithm(), "ed25519");
269 assert_eq!(DidKeyMaterial::EcdsaP256([2; 33]).algorithm(), "ecdsa-p256");
270 }
271}