Skip to main content

auth_framework/protocols/
openid4vp.rs

1//! OpenID for Verifiable Presentations (OpenID4VP) and Credential Issuance.
2//!
3//! Provides the infrastructure for issuing, storing, and presenting
4//! Verifiable Credentials using the W3C data model and the OpenID protocol suite.
5//!
6//! **Experimental**: Core data types and request/response structures are defined.
7//! DID resolution supports `did:key:` (Ed25519, P-256) and `did:web:` methods.
8//! JWS proof verification is implemented for EdDSA and ES256 algorithms.
9
10use crate::errors::{AuthError, Result};
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13
14// ── Configuration ───────────────────────────────────────────────────
15
16#[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// ── DID Document types ──────────────────────────────────────────────
34
35/// Minimal DID Document following the W3C DID Core specification.
36#[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/// A verification method within a DID Document.
48#[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// ── Presentation Definition (DIF Presentation Exchange) ─────────────
61
62/// A Presentation Definition per DIF Presentation Exchange v2.
63///
64/// Describes what kinds of credential(s) a verifier requires.
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct PresentationDefinition {
67    /// Unique identifier for this definition.
68    pub id: String,
69    /// Human-readable name.
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub name: Option<String>,
72    /// Human-readable purpose statement.
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub purpose: Option<String>,
75    /// Input descriptors — each describes one required credential.
76    pub input_descriptors: Vec<InputDescriptor>,
77}
78
79/// Describes a single credential input requirement.
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct InputDescriptor {
82    /// Unique identifier for this descriptor.
83    pub id: String,
84    /// Human-readable name.
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub name: Option<String>,
87    /// Purpose of this credential requirement.
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub purpose: Option<String>,
90    /// Constraints on the credential.
91    pub constraints: InputConstraints,
92}
93
94/// Constraint filters on a credential's JSON structure.
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct InputConstraints {
97    /// JSONPath field filters.
98    pub fields: Vec<FieldConstraint>,
99    /// If `required`, the field set in `fields` must all be present.
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub limit_disclosure: Option<String>,
102}
103
104/// A single field filter (JSONPath + optional JSON Schema filter).
105#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct FieldConstraint {
107    /// JSONPath expressions to the target field(s).
108    pub path: Vec<String>,
109    /// Optional JSON-Schema filter on the field value.
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub filter: Option<serde_json::Value>,
112    /// Whether this field is optional.
113    #[serde(default)]
114    pub optional: bool,
115}
116
117// ── Presentation Submission ─────────────────────────────────────────
118
119/// A Presentation Submission per DIF Presentation Exchange v2.
120///
121/// Accompanies a `vp_token` in the authorization response, mapping
122/// each Input Descriptor to the credential that fulfills it.
123#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct PresentationSubmission {
125    /// Unique identifier for this submission.
126    pub id: String,
127    /// The `id` of the Presentation Definition this submission fulfills.
128    pub definition_id: String,
129    /// Descriptor maps connecting descriptors to credentials.
130    pub descriptor_map: Vec<DescriptorMap>,
131}
132
133/// Maps an Input Descriptor to a credential location in the VP Token.
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct DescriptorMap {
136    /// The `id` of the matched Input Descriptor.
137    pub id: String,
138    /// Credential format (e.g. "jwt_vp", "ldp_vp").
139    pub format: String,
140    /// JSONPath to the credential in the VP Token.
141    pub path: String,
142    /// Nested path for credentials inside a VP envelope.
143    #[serde(skip_serializing_if = "Option::is_none")]
144    pub path_nested: Option<Box<DescriptorMap>>,
145}
146
147/// Validate that a Presentation Submission matches a Presentation Definition.
148///
149/// Checks that every required Input Descriptor is covered by a descriptor map entry.
150pub 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
176/// Build a minimal Presentation Definition requiring a single credential type.
177pub 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
201// ── DID Resolution ──────────────────────────────────────────────────
202
203/// Resolve a DID to its DID Document.
204///
205/// Supports:
206/// - `did:key:` — self-contained Ed25519 and P-256 keys
207/// - `did:web:` — HTTPS-based resolution
208pub 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
221/// Resolve a `did:key:` using multicodec prefixes.
222///
223/// Supported key types:
224/// - Ed25519 (multicodec 0xed, 32-byte key)
225/// - P-256 / secp256r1 (multicodec 0x1200, 33-byte compressed key)
226fn 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    // Multibase base58btc prefix is 'z'
232    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    // Parse multicodec varint prefix
251    let (key_type, public_key_bytes) = if decoded[0] == 0xed && decoded[1] == 0x01 {
252        // Ed25519: prefix 0xed01, 32-byte key
253        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        // P-256: varint 0x1200 encodes as [0x80, 0x24], 33-byte compressed key
265        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
303/// Resolve a `did:web:` by fetching the DID document over HTTPS.
304///
305/// `did:web:example.com` → `https://example.com/.well-known/did.json`
306/// `did:web:example.com:path:to` → `https://example.com/path/to/did.json`
307async 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    // Percent-decode the domain
321    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    // Verify the document ID matches the DID
352    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
384// ── JWS Verification ────────────────────────────────────────────────
385
386/// Extract the public key bytes from a verification method.
387fn 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        // For JWK, extract the raw key bytes based on key type
402        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                    // Uncompressed point: 0x04 || x || y
420                    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
451/// Verify a JWS compact serialization signature against a resolved public key.
452///
453/// Supports EdDSA (Ed25519) and ES256 (P-256) algorithms.
454fn 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
524// ── OpenID4VP Service ───────────────────────────────────────────────
525
526pub struct OpenId4vpService {
527    config: OpenId4vpConfig,
528}
529
530impl OpenId4vpService {
531    pub fn new(config: OpenId4vpConfig) -> Self {
532        Self { config }
533    }
534
535    /// Verifies an incoming Presentation Exchange request containing a W3C Verifiable Presentation.
536    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        // 1. Check for valid VP structure according to W3C Verifiable Credentials Data Model v1.1
544        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 // Is likely already the root VP object
552                } else {
553                    return Err(AuthError::invalid_credential(
554                        "openid4vp",
555                        "Missing verifiable presentation wrapper",
556                    ));
557                }
558            }
559        };
560
561        // 2. Validate @context
562        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        // 3. Validate type
580        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        // 4. Check for verifiableCredentials array
598        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        // 5. Proof / Signature Verification
616        // Resolve the DID and verify the JWS signature against the public key
617        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        // Validate challenge/nonce if present (recommended by OpenID4VP spec)
642        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        // Validate domain binding if present
652        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        // Resolve the DID to get the public key for signature verification
662        let verification_method_id = proof.get("verificationMethod").and_then(|v| v.as_str());
663
664        // Extract the DID from the verification method or the VP holder
665        let did = if let Some(vm_id) = verification_method_id {
666            // verificationMethod is typically "did:key:z...#z..."
667            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        // Resolve DID and verify JWS
678        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            // Use the first matching verification method, or the first one
689            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        // 6. Validate individual credentials
710        for credential in credentials {
711            // Each credential should have an issuer
712            if credential.get("issuer").is_none() {
713                return Err(AuthError::invalid_credential(
714                    "openid4vp",
715                    "Credential missing 'issuer' field",
716                ));
717            }
718
719            // Each credential should have a credentialSubject
720            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    /// Create an authorization request for a Verifiable Presentation (OpenID4VP §5)
742    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    // ── DID Resolution ──────────────────────────────────────────
780
781    #[test]
782    fn test_resolve_did_key_ed25519() {
783        // Known Ed25519 did:key (multicodec 0xed01 + 32 zero bytes)
784        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        // P-256 compressed point (0x02 prefix + 32 bytes)
802        let mut key_bytes = vec![0x80, 0x24]; // varint encoding of 0x1200
803        let mut compressed_point = vec![0x02]; // compressed point prefix
804        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"; // 'm' is not base58btc
830        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    // ── Key Extraction ──────────────────────────────────────────
849
850    #[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        // Uncompressed point: 0x04 || x(32) || y(32) = 65 bytes
901        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    // ── JWS Verification ────────────────────────────────────────
918
919    #[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        // Generate an Ed25519 key pair
943        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        // Create JWS components
949        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    // ── Service Tests ───────────────────────────────────────────
982
983    #[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        // Generate key pair
1073        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        // Build did:key from Ed25519 public key
1079        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        // Create JWS
1085        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    // ── DID Document serialization ──────────────────────────────
1147
1148    #[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}