1use acdp_primitives::error::AcdpError;
4use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct DidDocument {
10 pub id: String,
11
12 #[serde(rename = "verificationMethod", default)]
13 pub verification_methods: Vec<VerificationMethod>,
14
15 #[serde(rename = "assertionMethod", default)]
16 pub assertion_method: Vec<AssertionMethodRef>,
17}
18
19impl DidDocument {
20 fn fragment_of(id: &str) -> Option<&str> {
23 id.rsplit_once('#').map(|(_, frag)| frag)
24 }
25
26 fn absolutize(&self, vm_ref: &str) -> String {
30 match vm_ref.strip_prefix('#') {
31 Some(frag) => format!("{}#{frag}", self.id),
32 None => vm_ref.to_string(),
33 }
34 }
35
36 pub fn find_by_fragment(&self, fragment: &str) -> Option<&VerificationMethod> {
42 self.verification_methods
43 .iter()
44 .find(|m| Self::fragment_of(&m.id) == Some(fragment))
45 }
46
47 pub fn is_assertion_method(&self, vm_id: &str) -> bool {
54 let target = self.absolutize(vm_id);
55 self.assertion_method.iter().any(|r| match r {
56 AssertionMethodRef::Id(id) => self.absolutize(id) == target,
57 AssertionMethodRef::Embedded(m) => self.absolutize(&m.id) == target,
58 })
59 }
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct VerificationMethod {
65 pub id: String,
66 #[serde(rename = "type")]
67 pub method_type: String,
68 pub controller: String,
69
70 #[serde(rename = "publicKeyJwk", skip_serializing_if = "Option::is_none")]
72 pub public_key_jwk: Option<serde_json::Value>,
73
74 #[serde(rename = "publicKeyMultibase", skip_serializing_if = "Option::is_none")]
76 pub public_key_multibase: Option<String>,
77}
78
79impl VerificationMethod {
80 pub fn ed25519_public_key_bytes(&self) -> Result<[u8; 32], AcdpError> {
85 if let Some(jwk) = &self.public_key_jwk {
86 return extract_from_jwk(jwk);
87 }
88 if let Some(mb) = &self.public_key_multibase {
89 return extract_from_multibase(mb);
90 }
91 Err(AcdpError::KeyResolution(
92 "verification method has neither publicKeyJwk nor publicKeyMultibase".into(),
93 ))
94 }
95
96 pub fn ecdsa_p256_public_key_sec1(&self) -> Result<Vec<u8>, AcdpError> {
99 if let Some(jwk) = &self.public_key_jwk {
100 return extract_p256_from_jwk(jwk);
101 }
102 Err(AcdpError::KeyResolution(
103 "ecdsa-p256 verification method requires publicKeyJwk \
104 (publicKeyMultibase not yet supported for P-256)"
105 .into(),
106 ))
107 }
108
109 pub fn declared_algorithm(&self) -> Option<&'static str> {
120 match self.method_type.as_str() {
121 "Ed25519VerificationKey2020" | "Ed25519VerificationKey2018" => Some("ed25519"),
122 "EcdsaSecp256r1VerificationKey2019" => Some("ecdsa-p256"),
123 "JsonWebKey2020" => {
124 let jwk = self.public_key_jwk.as_ref()?;
125 let kty = jwk.get("kty").and_then(|v| v.as_str())?;
126 let crv = jwk.get("crv").and_then(|v| v.as_str())?;
127 match (kty, crv) {
128 ("OKP", "Ed25519") => Some("ed25519"),
129 ("EC", "P-256") => Some("ecdsa-p256"),
130 _ => None,
131 }
132 }
133 _ => self
140 .public_key_multibase
141 .as_deref()
142 .and_then(multicodec_algorithm),
143 }
144 }
145}
146
147fn multicodec_algorithm(mb: &str) -> Option<&'static str> {
153 let rest = mb.strip_prefix('z')?;
154 let decoded = bs58::decode(rest).into_vec().ok()?;
155 match decoded.get(0..2) {
156 Some([0xed, 0x01]) => Some("ed25519"),
157 Some([0x80, 0x24]) => Some("ecdsa-p256"),
158 _ => None,
159 }
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize)]
164#[serde(untagged)]
165pub enum AssertionMethodRef {
166 Id(String),
167 Embedded(Box<VerificationMethod>),
168}
169
170fn extract_from_jwk(jwk: &serde_json::Value) -> Result<[u8; 32], AcdpError> {
173 let kty = jwk["kty"].as_str().unwrap_or("");
174 let crv = jwk["crv"].as_str().unwrap_or("");
175
176 if kty != "OKP" || crv != "Ed25519" {
177 return Err(AcdpError::KeyResolution(format!(
178 "expected OKP/Ed25519 JWK, got kty={kty} crv={crv}"
179 )));
180 }
181
182 let x = jwk["x"]
183 .as_str()
184 .ok_or_else(|| AcdpError::KeyResolution("JWK missing 'x' parameter".into()))?;
185
186 let bytes = URL_SAFE_NO_PAD
187 .decode(x)
188 .map_err(|e| AcdpError::KeyResolution(format!("JWK 'x' base64url decode: {e}")))?;
189
190 bytes
191 .try_into()
192 .map_err(|_| AcdpError::KeyResolution("JWK 'x' is not 32 bytes (not Ed25519)".into()))
193}
194
195fn extract_p256_from_jwk(jwk: &serde_json::Value) -> Result<Vec<u8>, AcdpError> {
196 let kty = jwk["kty"].as_str().unwrap_or("");
197 let crv = jwk["crv"].as_str().unwrap_or("");
198 if kty != "EC" || crv != "P-256" {
199 return Err(AcdpError::KeyResolution(format!(
200 "expected EC/P-256 JWK, got kty={kty} crv={crv}"
201 )));
202 }
203 let x = jwk["x"]
204 .as_str()
205 .ok_or_else(|| AcdpError::KeyResolution("JWK missing 'x'".into()))?;
206 let y = jwk["y"]
207 .as_str()
208 .ok_or_else(|| AcdpError::KeyResolution("JWK missing 'y'".into()))?;
209 let x_bytes = URL_SAFE_NO_PAD
210 .decode(x)
211 .map_err(|e| AcdpError::KeyResolution(format!("JWK 'x' base64url: {e}")))?;
212 let y_bytes = URL_SAFE_NO_PAD
213 .decode(y)
214 .map_err(|e| AcdpError::KeyResolution(format!("JWK 'y' base64url: {e}")))?;
215 if x_bytes.len() != 32 || y_bytes.len() != 32 {
216 return Err(AcdpError::KeyResolution(format!(
217 "P-256 JWK x/y must be 32 bytes each, got x={} y={}",
218 x_bytes.len(),
219 y_bytes.len()
220 )));
221 }
222 let mut sec1 = Vec::with_capacity(65);
223 sec1.push(0x04);
224 sec1.extend_from_slice(&x_bytes);
225 sec1.extend_from_slice(&y_bytes);
226 Ok(sec1)
227}
228
229fn extract_from_multibase(mb: &str) -> Result<[u8; 32], AcdpError> {
230 if !mb.starts_with('z') {
231 return Err(AcdpError::KeyResolution(
232 "only 'z' (base58btc) multibase prefix is supported".into(),
233 ));
234 }
235
236 let decoded = bs58::decode(&mb[1..])
237 .into_vec()
238 .map_err(|e| AcdpError::KeyResolution(format!("base58 decode: {e}")))?;
239
240 if decoded.len() < 2 || decoded[0] != 0xed || decoded[1] != 0x01 {
242 return Err(AcdpError::KeyResolution(
243 "multibase key does not have Ed25519 multicodec prefix (0xed 0x01)".into(),
244 ));
245 }
246
247 decoded[2..].try_into().map_err(|_| {
248 AcdpError::KeyResolution("Ed25519 key must be 32 bytes after multicodec prefix".into())
249 })
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255 use serde_json::json;
256
257 const TEST_PUB_HEX: &str = "3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29";
258
259 fn test_pub_bytes() -> [u8; 32] {
260 hex::decode(TEST_PUB_HEX).unwrap().try_into().unwrap()
261 }
262
263 #[test]
264 fn extracts_from_jwk() {
265 let raw = test_pub_bytes();
266 let x = URL_SAFE_NO_PAD.encode(raw);
267 let jwk = json!({ "kty": "OKP", "crv": "Ed25519", "x": x });
268 let vm = VerificationMethod {
269 id: "did:web:example.com#key-1".into(),
270 method_type: "JsonWebKey2020".into(),
271 controller: "did:web:example.com".into(),
272 public_key_jwk: Some(jwk),
273 public_key_multibase: None,
274 };
275 assert_eq!(vm.ed25519_public_key_bytes().unwrap(), raw);
276 }
277
278 #[test]
279 fn rejects_wrong_kty() {
280 let jwk = json!({ "kty": "EC", "crv": "P-256", "x": "abc" });
281 let vm = VerificationMethod {
282 id: "did:web:example.com#key-1".into(),
283 method_type: "JsonWebKey2020".into(),
284 controller: "did:web:example.com".into(),
285 public_key_jwk: Some(jwk),
286 public_key_multibase: None,
287 };
288 assert!(matches!(
289 vm.ed25519_public_key_bytes(),
290 Err(AcdpError::KeyResolution(_))
291 ));
292 }
293
294 #[test]
295 fn extracts_from_multibase() {
296 let raw = test_pub_bytes();
297 let mut prefixed = vec![0xed, 0x01];
298 prefixed.extend_from_slice(&raw);
299 let mb = format!("z{}", bs58::encode(&prefixed).into_string());
300 let vm = VerificationMethod {
301 id: "did:web:example.com#key-1".into(),
302 method_type: "Ed25519VerificationKey2020".into(),
303 controller: "did:web:example.com".into(),
304 public_key_jwk: None,
305 public_key_multibase: Some(mb),
306 };
307 assert_eq!(vm.ed25519_public_key_bytes().unwrap(), raw);
308 }
309
310 #[test]
311 fn rejects_non_z_multibase() {
312 let vm = VerificationMethod {
313 id: "did:web:example.com#key-1".into(),
314 method_type: "Ed25519VerificationKey2020".into(),
315 controller: "did:web:example.com".into(),
316 public_key_jwk: None,
317 public_key_multibase: Some("uAAAA".into()),
318 };
319 assert!(matches!(
320 vm.ed25519_public_key_bytes(),
321 Err(AcdpError::KeyResolution(_))
322 ));
323 }
324
325 #[test]
326 fn rejects_non_ed25519_multicodec() {
327 let mut prefixed = vec![0xe7, 0x01];
329 prefixed.extend_from_slice(&[0u8; 32]);
330 let mb = format!("z{}", bs58::encode(&prefixed).into_string());
331 let vm = VerificationMethod {
332 id: "did:web:example.com#key-1".into(),
333 method_type: "X".into(),
334 controller: "did:web:example.com".into(),
335 public_key_jwk: None,
336 public_key_multibase: Some(mb),
337 };
338 assert!(matches!(
339 vm.ed25519_public_key_bytes(),
340 Err(AcdpError::KeyResolution(_))
341 ));
342 }
343
344 #[test]
345 fn assertion_method_authorization_by_full_id() {
346 let doc = DidDocument {
347 id: "did:web:example.com".into(),
348 verification_methods: vec![VerificationMethod {
349 id: "did:web:example.com#key-1".into(),
350 method_type: "Ed25519VerificationKey2020".into(),
351 controller: "did:web:example.com".into(),
352 public_key_jwk: None,
353 public_key_multibase: None,
354 }],
355 assertion_method: vec![AssertionMethodRef::Id("did:web:example.com#key-1".into())],
356 };
357 assert!(doc.is_assertion_method("did:web:example.com#key-1"));
358 assert!(!doc.is_assertion_method("did:web:example.com#key-2"));
359 }
360
361 #[test]
362 fn assertion_method_authorization_by_relative_fragment() {
363 let doc = DidDocument {
364 id: "did:web:example.com".into(),
365 verification_methods: vec![VerificationMethod {
366 id: "did:web:example.com#key-1".into(),
367 method_type: "Ed25519VerificationKey2020".into(),
368 controller: "did:web:example.com".into(),
369 public_key_jwk: None,
370 public_key_multibase: None,
371 }],
372 assertion_method: vec![AssertionMethodRef::Id("#key-1".into())],
373 };
374 assert!(doc.is_assertion_method("did:web:example.com#key-1"));
375 }
376
377 #[test]
378 fn find_by_fragment() {
379 let doc = DidDocument {
380 id: "did:web:example.com".into(),
381 verification_methods: vec![VerificationMethod {
382 id: "did:web:example.com#key-1".into(),
383 method_type: "Ed25519VerificationKey2020".into(),
384 controller: "did:web:example.com".into(),
385 public_key_jwk: None,
386 public_key_multibase: None,
387 }],
388 assertion_method: vec![],
389 };
390 assert!(doc.find_by_fragment("key-1").is_some());
391 assert!(doc.find_by_fragment("key-2").is_none());
392 }
393
394 #[test]
395 fn find_by_fragment_no_loose_suffix_match() {
396 let doc = DidDocument {
399 id: "did:web:example.com".into(),
400 verification_methods: vec![VerificationMethod {
401 id: "did:web:example.com#evil-key-1".into(),
402 method_type: "Ed25519VerificationKey2020".into(),
403 controller: "did:web:example.com".into(),
404 public_key_jwk: None,
405 public_key_multibase: None,
406 }],
407 assertion_method: vec![],
408 };
409 assert!(doc.find_by_fragment("key-1").is_none());
410 assert!(doc.find_by_fragment("evil-key-1").is_some());
411 }
412
413 #[test]
414 fn assertion_method_no_loose_suffix_match() {
415 let doc = DidDocument {
417 id: "did:web:example.com".into(),
418 verification_methods: vec![],
419 assertion_method: vec![AssertionMethodRef::Id("#key-1".into())],
420 };
421 assert!(doc.is_assertion_method("did:web:example.com#key-1"));
422 assert!(!doc.is_assertion_method("did:web:example.com#evil-key-1"));
423 assert!(!doc.is_assertion_method("did:web:attacker.com#key-1"));
425 }
426
427 #[test]
434 fn declared_algorithm_from_multibase_multicodec() {
435 let mk = |prefix: &[u8], body_len: usize| {
436 let mut prefixed = prefix.to_vec();
437 prefixed.resize(prefix.len() + body_len, 0u8);
438 let mb = format!("z{}", bs58::encode(&prefixed).into_string());
439 VerificationMethod {
440 id: "did:web:example.com#key-1".into(),
441 method_type: "Multikey".into(),
442 controller: "did:web:example.com".into(),
443 public_key_jwk: None,
444 public_key_multibase: Some(mb),
445 }
446 };
447 assert_eq!(mk(&[0xed, 0x01], 32).declared_algorithm(), Some("ed25519"));
449 assert_eq!(
451 mk(&[0x80, 0x24], 33).declared_algorithm(),
452 Some("ecdsa-p256")
453 );
454 assert_eq!(mk(&[0x12, 0x00], 33).declared_algorithm(), None);
456 assert_eq!(mk(&[0xe7, 0x01], 33).declared_algorithm(), None);
458 }
459}