1use crate::errors::{AuthError, Result};
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct OpenId4vpConfig {
18 pub enabled: bool,
19 pub issuer_did: String,
20 pub presentation_endpoint: String,
21}
22
23impl Default for OpenId4vpConfig {
24 fn default() -> Self {
25 Self {
26 enabled: false,
27 issuer_did: "did:web:example.com".to_string(),
28 presentation_endpoint: "/api/oid4vp/present".to_string(),
29 }
30 }
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct DidDocument {
38 pub id: String,
39 #[serde(default, rename = "verificationMethod")]
40 pub verification_method: Vec<VerificationMethod>,
41 #[serde(default)]
42 pub authentication: Vec<serde_json::Value>,
43 #[serde(default, rename = "assertionMethod")]
44 pub assertion_method: Vec<serde_json::Value>,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct VerificationMethod {
50 pub id: String,
51 #[serde(rename = "type")]
52 pub method_type: String,
53 pub controller: String,
54 #[serde(rename = "publicKeyJwk", skip_serializing_if = "Option::is_none")]
55 pub public_key_jwk: Option<serde_json::Value>,
56 #[serde(rename = "publicKeyMultibase", skip_serializing_if = "Option::is_none")]
57 pub public_key_multibase: Option<String>,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct PresentationDefinition {
67 pub id: String,
69 #[serde(skip_serializing_if = "Option::is_none")]
71 pub name: Option<String>,
72 #[serde(skip_serializing_if = "Option::is_none")]
74 pub purpose: Option<String>,
75 pub input_descriptors: Vec<InputDescriptor>,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct InputDescriptor {
82 pub id: String,
84 #[serde(skip_serializing_if = "Option::is_none")]
86 pub name: Option<String>,
87 #[serde(skip_serializing_if = "Option::is_none")]
89 pub purpose: Option<String>,
90 pub constraints: InputConstraints,
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct InputConstraints {
97 pub fields: Vec<FieldConstraint>,
99 #[serde(skip_serializing_if = "Option::is_none")]
101 pub limit_disclosure: Option<String>,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct FieldConstraint {
107 pub path: Vec<String>,
109 #[serde(skip_serializing_if = "Option::is_none")]
111 pub filter: Option<serde_json::Value>,
112 #[serde(default)]
114 pub optional: bool,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct PresentationSubmission {
125 pub id: String,
127 pub definition_id: String,
129 pub descriptor_map: Vec<DescriptorMap>,
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct DescriptorMap {
136 pub id: String,
138 pub format: String,
140 pub path: String,
142 #[serde(skip_serializing_if = "Option::is_none")]
144 pub path_nested: Option<Box<DescriptorMap>>,
145}
146
147pub fn validate_submission(
151 definition: &PresentationDefinition,
152 submission: &PresentationSubmission,
153) -> Result<()> {
154 if submission.definition_id != definition.id {
155 return Err(AuthError::validation(
156 "Presentation Submission definition_id does not match Presentation Definition id",
157 ));
158 }
159
160 for descriptor in &definition.input_descriptors {
161 let matched = submission
162 .descriptor_map
163 .iter()
164 .any(|dm| dm.id == descriptor.id);
165 if !matched {
166 return Err(AuthError::validation(format!(
167 "Input Descriptor '{}' not satisfied by Presentation Submission",
168 descriptor.id
169 )));
170 }
171 }
172
173 Ok(())
174}
175
176pub fn simple_presentation_definition(id: &str, credential_type: &str) -> PresentationDefinition {
178 PresentationDefinition {
179 id: id.to_string(),
180 name: Some(format!("Request for {credential_type}")),
181 purpose: None,
182 input_descriptors: vec![InputDescriptor {
183 id: format!("{id}_input"),
184 name: Some(credential_type.to_string()),
185 purpose: None,
186 constraints: InputConstraints {
187 fields: vec![FieldConstraint {
188 path: vec!["$.type".to_string()],
189 filter: Some(serde_json::json!({
190 "type": "array",
191 "contains": { "const": credential_type }
192 })),
193 optional: false,
194 }],
195 limit_disclosure: None,
196 },
197 }],
198 }
199}
200
201pub async fn resolve_did(did: &str) -> Result<DidDocument> {
209 if did.starts_with("did:key:") {
210 resolve_did_key(did)
211 } else if did.starts_with("did:web:") {
212 resolve_did_web(did).await
213 } else {
214 Err(AuthError::invalid_credential(
215 "openid4vp",
216 &format!("Unsupported DID method: {did}"),
217 ))
218 }
219}
220
221fn resolve_did_key(did: &str) -> Result<DidDocument> {
227 let key_part = did
228 .strip_prefix("did:key:")
229 .ok_or_else(|| AuthError::invalid_credential("openid4vp", "Invalid did:key format"))?;
230
231 if !key_part.starts_with('z') {
233 return Err(AuthError::invalid_credential(
234 "openid4vp",
235 "did:key must use base58btc encoding (prefix 'z')",
236 ));
237 }
238
239 let decoded = bs58::decode(&key_part[1..]).into_vec().map_err(|e| {
240 AuthError::invalid_credential("openid4vp", &format!("Base58 decode failed: {e}"))
241 })?;
242
243 if decoded.len() < 2 {
244 return Err(AuthError::invalid_credential(
245 "openid4vp",
246 "did:key decoded value too short",
247 ));
248 }
249
250 let (key_type, public_key_bytes) = if decoded[0] == 0xed && decoded[1] == 0x01 {
252 if decoded.len() != 34 {
254 return Err(AuthError::invalid_credential(
255 "openid4vp",
256 &format!(
257 "Ed25519 key must be 34 bytes (prefix+key), got {}",
258 decoded.len()
259 ),
260 ));
261 }
262 ("Ed25519VerificationKey2020", &decoded[2..])
263 } else if decoded[0] == 0x80 && decoded.len() > 2 && decoded[1] == 0x24 {
264 if decoded.len() != 35 {
266 return Err(AuthError::invalid_credential(
267 "openid4vp",
268 &format!(
269 "P-256 key must be 35 bytes (prefix+key), got {}",
270 decoded.len()
271 ),
272 ));
273 }
274 ("EcdsaSecp256r1VerificationKey2019", &decoded[2..])
275 } else {
276 return Err(AuthError::invalid_credential(
277 "openid4vp",
278 &format!(
279 "Unsupported multicodec prefix: 0x{:02x}{:02x}",
280 decoded[0],
281 decoded.get(1).copied().unwrap_or(0)
282 ),
283 ));
284 };
285
286 let multibase_key = format!("z{}", bs58::encode(public_key_bytes).into_string());
287 let vm_id = format!("{did}#{key_part}");
288
289 Ok(DidDocument {
290 id: did.to_string(),
291 verification_method: vec![VerificationMethod {
292 id: vm_id,
293 method_type: key_type.to_string(),
294 controller: did.to_string(),
295 public_key_jwk: None,
296 public_key_multibase: Some(multibase_key),
297 }],
298 authentication: vec![serde_json::json!(format!("{did}#{key_part}"))],
299 assertion_method: vec![serde_json::json!(format!("{did}#{key_part}"))],
300 })
301}
302
303async fn resolve_did_web(did: &str) -> Result<DidDocument> {
308 let domain_path = did
309 .strip_prefix("did:web:")
310 .ok_or_else(|| AuthError::invalid_credential("openid4vp", "Invalid did:web format"))?;
311
312 let parts: Vec<&str> = domain_path.split(':').collect();
313 if parts.is_empty() {
314 return Err(AuthError::invalid_credential(
315 "openid4vp",
316 "did:web missing domain",
317 ));
318 }
319
320 let domain = percent_decode(parts[0]);
322 let url = if parts.len() == 1 {
323 format!("https://{domain}/.well-known/did.json")
324 } else {
325 let path = parts[1..].join("/");
326 format!("https://{domain}/{path}/did.json")
327 };
328
329 let client = reqwest::Client::builder()
330 .timeout(std::time::Duration::from_secs(10))
331 .build()
332 .map_err(|e| {
333 AuthError::invalid_credential("openid4vp", &format!("HTTP client error: {e}"))
334 })?;
335
336 let resp = client.get(&url).send().await.map_err(|e| {
337 AuthError::invalid_credential("openid4vp", &format!("Failed to fetch DID document: {e}"))
338 })?;
339
340 if !resp.status().is_success() {
341 return Err(AuthError::invalid_credential(
342 "openid4vp",
343 &format!("DID document fetch returned HTTP {}", resp.status()),
344 ));
345 }
346
347 let doc: DidDocument = resp.json().await.map_err(|e| {
348 AuthError::invalid_credential("openid4vp", &format!("Invalid DID document JSON: {e}"))
349 })?;
350
351 if doc.id != did {
353 return Err(AuthError::invalid_credential(
354 "openid4vp",
355 &format!(
356 "DID document id '{}' does not match requested DID '{did}'",
357 doc.id
358 ),
359 ));
360 }
361
362 Ok(doc)
363}
364
365fn percent_decode(s: &str) -> String {
366 let mut result = String::with_capacity(s.len());
367 let mut chars = s.chars();
368 while let Some(c) = chars.next() {
369 if c == '%' {
370 let hex: String = chars.by_ref().take(2).collect();
371 if let Ok(byte) = u8::from_str_radix(&hex, 16) {
372 result.push(byte as char);
373 } else {
374 result.push('%');
375 result.push_str(&hex);
376 }
377 } else {
378 result.push(c);
379 }
380 }
381 result
382}
383
384fn extract_public_key(vm: &VerificationMethod) -> Result<Vec<u8>> {
388 if let Some(multibase) = &vm.public_key_multibase {
389 if let Some(data) = multibase.strip_prefix('z') {
390 return bs58::decode(data).into_vec().map_err(|e| {
391 AuthError::invalid_credential("openid4vp", &format!("Multibase decode failed: {e}"))
392 });
393 }
394 return Err(AuthError::invalid_credential(
395 "openid4vp",
396 "Only base58btc (prefix 'z') multibase encoding is supported",
397 ));
398 }
399
400 if let Some(jwk) = &vm.public_key_jwk {
401 if let Some(crv) = jwk.get("crv").and_then(|v| v.as_str()) {
403 match crv {
404 "Ed25519" => {
405 let x = jwk.get("x").and_then(|v| v.as_str()).ok_or_else(|| {
406 AuthError::invalid_credential("openid4vp", "Ed25519 JWK missing 'x'")
407 })?;
408 return base64_url_decode(x);
409 }
410 "P-256" => {
411 let x = jwk.get("x").and_then(|v| v.as_str()).ok_or_else(|| {
412 AuthError::invalid_credential("openid4vp", "P-256 JWK missing 'x'")
413 })?;
414 let y = jwk.get("y").and_then(|v| v.as_str()).ok_or_else(|| {
415 AuthError::invalid_credential("openid4vp", "P-256 JWK missing 'y'")
416 })?;
417 let x_bytes = base64_url_decode(x)?;
418 let y_bytes = base64_url_decode(y)?;
419 let mut point = Vec::with_capacity(1 + x_bytes.len() + y_bytes.len());
421 point.push(0x04);
422 point.extend_from_slice(&x_bytes);
423 point.extend_from_slice(&y_bytes);
424 return Ok(point);
425 }
426 _ => {
427 return Err(AuthError::invalid_credential(
428 "openid4vp",
429 &format!("Unsupported JWK curve: {crv}"),
430 ));
431 }
432 }
433 }
434 }
435
436 Err(AuthError::invalid_credential(
437 "openid4vp",
438 "Verification method has no extractable public key",
439 ))
440}
441
442fn base64_url_decode(input: &str) -> Result<Vec<u8>> {
443 use base64::Engine;
444 base64::engine::general_purpose::URL_SAFE_NO_PAD
445 .decode(input)
446 .map_err(|e| {
447 AuthError::invalid_credential("openid4vp", &format!("Base64url decode error: {e}"))
448 })
449}
450
451fn verify_jws(jws: &str, public_key_bytes: &[u8], key_type: &str) -> Result<bool> {
455 let parts: Vec<&str> = jws.split('.').collect();
456 if parts.len() != 3 {
457 return Err(AuthError::invalid_credential(
458 "openid4vp",
459 "JWS must have exactly 3 parts (header.payload.signature)",
460 ));
461 }
462
463 let header_json = base64_url_decode(parts[0])?;
464 let header: HashMap<String, serde_json::Value> =
465 serde_json::from_slice(&header_json).map_err(|e| {
466 AuthError::invalid_credential("openid4vp", &format!("Invalid JWS header: {e}"))
467 })?;
468
469 let alg = header.get("alg").and_then(|v| v.as_str()).unwrap_or("none");
470
471 let signing_input = format!("{}.{}", parts[0], parts[1]);
472 let signature = base64_url_decode(parts[2])?;
473
474 match alg {
475 "EdDSA" => {
476 if !key_type.contains("Ed25519") {
477 return Err(AuthError::invalid_credential(
478 "openid4vp",
479 "EdDSA algorithm requires Ed25519 key",
480 ));
481 }
482 let peer_key = ring::signature::UnparsedPublicKey::new(
483 &ring::signature::ED25519,
484 public_key_bytes,
485 );
486 peer_key
487 .verify(signing_input.as_bytes(), &signature)
488 .map_err(|_| {
489 AuthError::invalid_credential(
490 "openid4vp",
491 "EdDSA signature verification failed",
492 )
493 })?;
494 Ok(true)
495 }
496 "ES256" => {
497 if !key_type.contains("256") && !key_type.contains("P-256") {
498 return Err(AuthError::invalid_credential(
499 "openid4vp",
500 "ES256 algorithm requires P-256 key",
501 ));
502 }
503 let peer_key = ring::signature::UnparsedPublicKey::new(
504 &ring::signature::ECDSA_P256_SHA256_FIXED,
505 public_key_bytes,
506 );
507 peer_key
508 .verify(signing_input.as_bytes(), &signature)
509 .map_err(|_| {
510 AuthError::invalid_credential(
511 "openid4vp",
512 "ES256 signature verification failed",
513 )
514 })?;
515 Ok(true)
516 }
517 _ => Err(AuthError::invalid_credential(
518 "openid4vp",
519 &format!("Unsupported JWS algorithm: {alg}"),
520 )),
521 }
522}
523
524pub struct OpenId4vpService {
527 config: OpenId4vpConfig,
528}
529
530impl OpenId4vpService {
531 pub fn new(config: OpenId4vpConfig) -> Self {
532 Self { config }
533 }
534
535 pub async fn verify_presentation(&self, vp: &serde_json::Value) -> Result<bool> {
537 if !self.config.enabled {
538 return Err(AuthError::config(
539 "OpenID4VP protocol is currently disabled",
540 ));
541 }
542
543 let _is_vp = vp.get("verifiablePresentation").is_some() || vp.get("vp").is_some();
545 let presentation = vp.get("verifiablePresentation").or_else(|| vp.get("vp"));
546
547 let presentation_obj = match presentation {
548 Some(obj) => obj,
549 None => {
550 if vp.get("@context").is_some() && vp.get("type").is_some() {
551 vp } else {
553 return Err(AuthError::invalid_credential(
554 "openid4vp",
555 "Missing verifiable presentation wrapper",
556 ));
557 }
558 }
559 };
560
561 let context = presentation_obj
563 .get("@context")
564 .and_then(|c| c.as_array())
565 .ok_or_else(|| {
566 AuthError::invalid_credential("openid4vp", "Missing or invalid @context in VP")
567 })?;
568
569 let has_w3c_context = context
570 .iter()
571 .any(|c| c.as_str() == Some("https://www.w3.org/2018/credentials/v1"));
572 if !has_w3c_context {
573 return Err(AuthError::invalid_credential(
574 "openid4vp",
575 "VP missing W3C credentials context",
576 ));
577 }
578
579 let vp_type = presentation_obj
581 .get("type")
582 .and_then(|t| t.as_array())
583 .ok_or_else(|| {
584 AuthError::invalid_credential("openid4vp", "Missing or invalid type in VP")
585 })?;
586
587 if !vp_type
588 .iter()
589 .any(|t| t.as_str() == Some("VerifiablePresentation"))
590 {
591 return Err(AuthError::invalid_credential(
592 "openid4vp",
593 "Object is not a VerifiablePresentation",
594 ));
595 }
596
597 let credentials = presentation_obj
599 .get("verifiableCredential")
600 .and_then(|c| c.as_array())
601 .ok_or_else(|| {
602 AuthError::invalid_credential(
603 "openid4vp",
604 "No credentials included in presentation",
605 )
606 })?;
607
608 if credentials.is_empty() {
609 return Err(AuthError::invalid_credential(
610 "openid4vp",
611 "Empty verifiableCredential array",
612 ));
613 }
614
615 let proof = presentation_obj.get("proof").ok_or_else(|| {
618 AuthError::invalid_credential(
619 "openid4vp",
620 "Missing proof object in Verifiable Presentation",
621 )
622 })?;
623
624 if proof.get("type").is_none() {
625 return Err(AuthError::invalid_credential(
626 "openid4vp",
627 "Proof missing 'type' field",
628 ));
629 }
630
631 let jws = proof.get("jws").and_then(|v| v.as_str());
632 let proof_value = proof.get("proofValue").and_then(|v| v.as_str());
633
634 if jws.is_none() && proof_value.is_none() {
635 return Err(AuthError::invalid_credential(
636 "openid4vp",
637 "Proof missing both 'jws' and 'proofValue' — at least one is required",
638 ));
639 }
640
641 if let Some(challenge) = proof.get("challenge") {
643 if challenge.as_str().unwrap_or("").is_empty() {
644 return Err(AuthError::invalid_credential(
645 "openid4vp",
646 "Proof challenge must not be empty",
647 ));
648 }
649 }
650
651 if let Some(domain) = proof.get("domain") {
653 if domain.as_str().unwrap_or("").is_empty() {
654 return Err(AuthError::invalid_credential(
655 "openid4vp",
656 "Proof domain must not be empty",
657 ));
658 }
659 }
660
661 let verification_method_id = proof.get("verificationMethod").and_then(|v| v.as_str());
663
664 let did = if let Some(vm_id) = verification_method_id {
666 vm_id.split('#').next().unwrap_or(vm_id).to_string()
668 } else if let Some(holder) = presentation_obj.get("holder").and_then(|h| h.as_str()) {
669 holder.to_string()
670 } else {
671 return Err(AuthError::invalid_credential(
672 "openid4vp",
673 "Cannot determine DID: no verificationMethod or holder in proof",
674 ));
675 };
676
677 if let Some(jws_value) = jws {
679 let did_doc = resolve_did(&did).await?;
680
681 if did_doc.verification_method.is_empty() {
682 return Err(AuthError::invalid_credential(
683 "openid4vp",
684 "Resolved DID document has no verification methods",
685 ));
686 }
687
688 let vm = if let Some(vm_id) = verification_method_id {
690 did_doc
691 .verification_method
692 .iter()
693 .find(|vm| vm.id == vm_id)
694 .or_else(|| did_doc.verification_method.first())
695 } else {
696 did_doc.verification_method.first()
697 }
698 .ok_or_else(|| {
699 AuthError::invalid_credential(
700 "openid4vp",
701 "No matching verification method in DID document",
702 )
703 })?;
704
705 let public_key = extract_public_key(vm)?;
706 verify_jws(jws_value, &public_key, &vm.method_type)?;
707 }
708
709 for credential in credentials {
711 if credential.get("issuer").is_none() {
713 return Err(AuthError::invalid_credential(
714 "openid4vp",
715 "Credential missing 'issuer' field",
716 ));
717 }
718
719 if credential.get("credentialSubject").is_none() {
721 return Err(AuthError::invalid_credential(
722 "openid4vp",
723 "Credential missing 'credentialSubject' field",
724 ));
725 }
726 }
727
728 tracing::info!(
729 "OpenID4VP: Verifiable Presentation validated — structural checks and \
730 {} verification passed",
731 if jws.is_some() {
732 "JWS signature"
733 } else {
734 "proof structure"
735 }
736 );
737
738 Ok(true)
739 }
740
741 pub fn create_presentation_request(
743 &self,
744 nonce: &str,
745 presentation_definition: serde_json::Value,
746 ) -> Result<serde_json::Value> {
747 if !self.config.enabled {
748 return Err(AuthError::config(
749 "OpenID4VP protocol is currently disabled",
750 ));
751 }
752
753 if nonce.is_empty() {
754 return Err(AuthError::validation("nonce must not be empty"));
755 }
756
757 Ok(serde_json::json!({
758 "response_type": "vp_token",
759 "client_id": self.config.issuer_did,
760 "nonce": nonce,
761 "presentation_definition": presentation_definition,
762 "response_mode": "direct_post",
763 "response_uri": self.config.presentation_endpoint,
764 }))
765 }
766}
767
768#[cfg(test)]
769mod tests {
770 use super::*;
771
772 fn enabled_config() -> OpenId4vpConfig {
773 OpenId4vpConfig {
774 enabled: true,
775 ..Default::default()
776 }
777 }
778
779 #[test]
782 fn test_resolve_did_key_ed25519() {
783 let mut key_bytes = vec![0xed, 0x01];
785 key_bytes.extend_from_slice(&[0u8; 32]);
786 let encoded = format!("z{}", bs58::encode(&key_bytes).into_string());
787 let did = format!("did:key:{encoded}");
788
789 let doc = resolve_did_key(&did).unwrap();
790 assert_eq!(doc.id, did);
791 assert_eq!(doc.verification_method.len(), 1);
792 assert_eq!(
793 doc.verification_method[0].method_type,
794 "Ed25519VerificationKey2020"
795 );
796 assert!(doc.verification_method[0].public_key_multibase.is_some());
797 }
798
799 #[test]
800 fn test_resolve_did_key_p256() {
801 let mut key_bytes = vec![0x80, 0x24]; let mut compressed_point = vec![0x02]; compressed_point.extend_from_slice(&[0xAA; 32]);
805 key_bytes.extend_from_slice(&compressed_point);
806 let encoded = format!("z{}", bs58::encode(&key_bytes).into_string());
807 let did = format!("did:key:{encoded}");
808
809 let doc = resolve_did_key(&did).unwrap();
810 assert_eq!(doc.verification_method.len(), 1);
811 assert_eq!(
812 doc.verification_method[0].method_type,
813 "EcdsaSecp256r1VerificationKey2019"
814 );
815 }
816
817 #[test]
818 fn test_resolve_did_key_unsupported_prefix() {
819 let key_bytes = vec![0xFF, 0xFF, 0x00, 0x01];
820 let encoded = format!("z{}", bs58::encode(&key_bytes).into_string());
821 let did = format!("did:key:{encoded}");
822
823 let err = resolve_did_key(&did);
824 assert!(err.is_err());
825 }
826
827 #[test]
828 fn test_resolve_did_key_invalid_multibase() {
829 let did = "did:key:m123456"; let err = resolve_did_key(did);
831 assert!(err.is_err());
832 }
833
834 #[test]
835 fn test_resolve_did_key_short_value() {
836 let encoded = format!("z{}", bs58::encode(&[0xed]).into_string());
837 let did = format!("did:key:{encoded}");
838 let err = resolve_did_key(&did);
839 assert!(err.is_err());
840 }
841
842 #[tokio::test]
843 async fn test_resolve_did_unsupported_method() {
844 let err = resolve_did("did:example:12345").await;
845 assert!(err.is_err());
846 }
847
848 #[test]
851 fn test_extract_public_key_multibase() {
852 let vm = VerificationMethod {
853 id: "did:key:z...#z...".to_string(),
854 method_type: "Ed25519VerificationKey2020".to_string(),
855 controller: "did:key:z...".to_string(),
856 public_key_jwk: None,
857 public_key_multibase: Some(format!("z{}", bs58::encode(&[0u8; 32]).into_string())),
858 };
859 let key = extract_public_key(&vm).unwrap();
860 assert_eq!(key.len(), 32);
861 }
862
863 #[test]
864 fn test_extract_public_key_jwk_ed25519() {
865 use base64::Engine;
866 let x = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&[0xAB; 32]);
867 let vm = VerificationMethod {
868 id: "key1".to_string(),
869 method_type: "Ed25519VerificationKey2020".to_string(),
870 controller: "did:key:z...".to_string(),
871 public_key_jwk: Some(serde_json::json!({
872 "kty": "OKP",
873 "crv": "Ed25519",
874 "x": x,
875 })),
876 public_key_multibase: None,
877 };
878 let key = extract_public_key(&vm).unwrap();
879 assert_eq!(key.len(), 32);
880 }
881
882 #[test]
883 fn test_extract_public_key_jwk_p256() {
884 use base64::Engine;
885 let x = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&[0x01; 32]);
886 let y = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&[0x02; 32]);
887 let vm = VerificationMethod {
888 id: "key1".to_string(),
889 method_type: "EcdsaSecp256r1VerificationKey2019".to_string(),
890 controller: "did:key:z...".to_string(),
891 public_key_jwk: Some(serde_json::json!({
892 "kty": "EC",
893 "crv": "P-256",
894 "x": x,
895 "y": y,
896 })),
897 public_key_multibase: None,
898 };
899 let key = extract_public_key(&vm).unwrap();
900 assert_eq!(key.len(), 65);
902 assert_eq!(key[0], 0x04);
903 }
904
905 #[test]
906 fn test_extract_public_key_no_key_data() {
907 let vm = VerificationMethod {
908 id: "key1".to_string(),
909 method_type: "SomeType".to_string(),
910 controller: "did:key:z...".to_string(),
911 public_key_jwk: None,
912 public_key_multibase: None,
913 };
914 assert!(extract_public_key(&vm).is_err());
915 }
916
917 #[test]
920 fn test_verify_jws_invalid_parts() {
921 let err = verify_jws("only.two", &[0; 32], "Ed25519");
922 assert!(err.is_err());
923 }
924
925 #[test]
926 fn test_verify_jws_unsupported_algorithm() {
927 use base64::Engine;
928 let header = base64::engine::general_purpose::URL_SAFE_NO_PAD
929 .encode(r#"{"alg":"RS256"}"#.as_bytes());
930 let payload = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(b"test");
931 let sig = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(b"sig");
932 let jws = format!("{header}.{payload}.{sig}");
933 let err = verify_jws(&jws, &[0; 32], "Ed25519");
934 assert!(err.is_err());
935 }
936
937 #[test]
938 fn test_verify_jws_ed25519_with_real_signing() {
939 use base64::Engine;
940 use ring::signature::{Ed25519KeyPair, KeyPair};
941
942 let rng = ring::rand::SystemRandom::new();
944 let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
945 let key_pair = Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap();
946 let public_key = key_pair.public_key().as_ref().to_vec();
947
948 let header = base64::engine::general_purpose::URL_SAFE_NO_PAD
950 .encode(r#"{"alg":"EdDSA"}"#.as_bytes());
951 let payload = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(b"test payload");
952 let signing_input = format!("{header}.{payload}");
953 let signature = key_pair.sign(signing_input.as_bytes());
954 let sig_b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(signature.as_ref());
955
956 let jws = format!("{signing_input}.{sig_b64}");
957 let result = verify_jws(&jws, &public_key, "Ed25519VerificationKey2020").unwrap();
958 assert!(result);
959 }
960
961 #[test]
962 fn test_verify_jws_ed25519_bad_signature() {
963 use base64::Engine;
964 use ring::signature::{Ed25519KeyPair, KeyPair};
965
966 let rng = ring::rand::SystemRandom::new();
967 let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
968 let key_pair = Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap();
969 let public_key = key_pair.public_key().as_ref().to_vec();
970
971 let header = base64::engine::general_purpose::URL_SAFE_NO_PAD
972 .encode(r#"{"alg":"EdDSA"}"#.as_bytes());
973 let payload = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(b"test");
974 let bad_sig = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&[0u8; 64]);
975
976 let jws = format!("{header}.{payload}.{bad_sig}");
977 let err = verify_jws(&jws, &public_key, "Ed25519VerificationKey2020");
978 assert!(err.is_err());
979 }
980
981 #[tokio::test]
984 async fn test_verify_disabled() {
985 let svc = OpenId4vpService::new(OpenId4vpConfig::default());
986 let vp = serde_json::json!({"test": true});
987 let err = svc.verify_presentation(&vp).await;
988 assert!(err.is_err());
989 }
990
991 #[tokio::test]
992 async fn test_verify_missing_vp_wrapper() {
993 let svc = OpenId4vpService::new(enabled_config());
994 let vp = serde_json::json!({"not_a_vp": true});
995 let err = svc.verify_presentation(&vp).await;
996 assert!(err.is_err());
997 }
998
999 #[tokio::test]
1000 async fn test_verify_missing_context() {
1001 let svc = OpenId4vpService::new(enabled_config());
1002 let vp = serde_json::json!({
1003 "type": ["VerifiablePresentation"],
1004 });
1005 let err = svc.verify_presentation(&vp).await;
1006 assert!(err.is_err());
1007 }
1008
1009 #[tokio::test]
1010 async fn test_verify_wrong_type() {
1011 let svc = OpenId4vpService::new(enabled_config());
1012 let vp = serde_json::json!({
1013 "@context": ["https://www.w3.org/2018/credentials/v1"],
1014 "type": ["SomethingElse"],
1015 });
1016 let err = svc.verify_presentation(&vp).await;
1017 assert!(err.is_err());
1018 }
1019
1020 #[tokio::test]
1021 async fn test_verify_empty_credentials() {
1022 let svc = OpenId4vpService::new(enabled_config());
1023 let vp = serde_json::json!({
1024 "@context": ["https://www.w3.org/2018/credentials/v1"],
1025 "type": ["VerifiablePresentation"],
1026 "verifiableCredential": [],
1027 });
1028 let err = svc.verify_presentation(&vp).await;
1029 assert!(err.is_err());
1030 }
1031
1032 #[tokio::test]
1033 async fn test_verify_missing_proof() {
1034 let svc = OpenId4vpService::new(enabled_config());
1035 let vp = serde_json::json!({
1036 "@context": ["https://www.w3.org/2018/credentials/v1"],
1037 "type": ["VerifiablePresentation"],
1038 "verifiableCredential": [{
1039 "issuer": "did:example:123",
1040 "credentialSubject": {"id": "did:example:456"},
1041 }],
1042 });
1043 let err = svc.verify_presentation(&vp).await;
1044 assert!(err.is_err());
1045 }
1046
1047 #[tokio::test]
1048 async fn test_verify_proof_missing_jws_and_proof_value() {
1049 let svc = OpenId4vpService::new(enabled_config());
1050 let vp = serde_json::json!({
1051 "@context": ["https://www.w3.org/2018/credentials/v1"],
1052 "type": ["VerifiablePresentation"],
1053 "verifiableCredential": [{
1054 "issuer": "did:example:123",
1055 "credentialSubject": {"id": "did:example:456"},
1056 }],
1057 "proof": {
1058 "type": "Ed25519Signature2020",
1059 },
1060 });
1061 let err = svc.verify_presentation(&vp).await;
1062 assert!(err.is_err());
1063 }
1064
1065 #[tokio::test]
1066 async fn test_verify_full_vp_with_ed25519_proof() {
1067 use base64::Engine;
1068 use ring::signature::{Ed25519KeyPair, KeyPair};
1069
1070 let svc = OpenId4vpService::new(enabled_config());
1071
1072 let rng = ring::rand::SystemRandom::new();
1074 let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
1075 let key_pair = Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap();
1076 let public_key = key_pair.public_key().as_ref();
1077
1078 let mut mc_bytes = vec![0xed, 0x01];
1080 mc_bytes.extend_from_slice(public_key);
1081 let did_key_fragment = format!("z{}", bs58::encode(&mc_bytes).into_string());
1082 let did = format!("did:key:{did_key_fragment}");
1083
1084 let header = base64::engine::general_purpose::URL_SAFE_NO_PAD
1086 .encode(r#"{"alg":"EdDSA"}"#.as_bytes());
1087 let payload = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(b"{}");
1088 let signing_input = format!("{header}.{payload}");
1089 let signature = key_pair.sign(signing_input.as_bytes());
1090 let sig_b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(signature.as_ref());
1091 let jws = format!("{signing_input}.{sig_b64}");
1092
1093 let vm_id = format!("{did}#{did_key_fragment}");
1094
1095 let vp = serde_json::json!({
1096 "@context": ["https://www.w3.org/2018/credentials/v1"],
1097 "type": ["VerifiablePresentation"],
1098 "holder": did,
1099 "verifiableCredential": [{
1100 "issuer": "did:example:issuer",
1101 "credentialSubject": {"id": "did:example:subject"},
1102 }],
1103 "proof": {
1104 "type": "Ed25519Signature2020",
1105 "verificationMethod": vm_id,
1106 "jws": jws,
1107 "challenge": "abc123",
1108 },
1109 });
1110
1111 let result = svc.verify_presentation(&vp).await.unwrap();
1112 assert!(result);
1113 }
1114
1115 #[test]
1116 fn test_create_presentation_request() {
1117 let svc = OpenId4vpService::new(enabled_config());
1118 let req = svc
1119 .create_presentation_request("nonce123", serde_json::json!({"input_descriptors": []}))
1120 .unwrap();
1121 assert_eq!(req["response_type"], "vp_token");
1122 assert_eq!(req["nonce"], "nonce123");
1123 }
1124
1125 #[test]
1126 fn test_create_presentation_request_empty_nonce() {
1127 let svc = OpenId4vpService::new(enabled_config());
1128 let err = svc.create_presentation_request("", serde_json::json!({}));
1129 assert!(err.is_err());
1130 }
1131
1132 #[test]
1133 fn test_create_presentation_request_disabled() {
1134 let svc = OpenId4vpService::new(OpenId4vpConfig::default());
1135 let err = svc.create_presentation_request("nonce", serde_json::json!({}));
1136 assert!(err.is_err());
1137 }
1138
1139 #[test]
1140 fn test_percent_decode() {
1141 assert_eq!(percent_decode("hello%20world"), "hello world");
1142 assert_eq!(percent_decode("no-encoding"), "no-encoding");
1143 assert_eq!(percent_decode("%41%42"), "AB");
1144 }
1145
1146 #[test]
1149 fn test_did_document_roundtrip() {
1150 let doc = DidDocument {
1151 id: "did:key:z123".to_string(),
1152 verification_method: vec![VerificationMethod {
1153 id: "did:key:z123#z123".to_string(),
1154 method_type: "Ed25519VerificationKey2020".to_string(),
1155 controller: "did:key:z123".to_string(),
1156 public_key_jwk: None,
1157 public_key_multibase: Some("z123".to_string()),
1158 }],
1159 authentication: vec![serde_json::json!("did:key:z123#z123")],
1160 assertion_method: vec![],
1161 };
1162
1163 let json = serde_json::to_string(&doc).unwrap();
1164 let deserialized: DidDocument = serde_json::from_str(&json).unwrap();
1165 assert_eq!(deserialized.id, doc.id);
1166 assert_eq!(deserialized.verification_method.len(), 1);
1167 }
1168}