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