Skip to main content

auth_framework/protocols/
openid4vci.rs

1//! OpenID for Verifiable Credential Issuance (OpenID4VCI).
2//!
3//! Implements the issuer-side of the OpenID4VCI specification for issuing
4//! Verifiable Credentials using OAuth 2.0 authorization flows.
5//!
6//! # Architecture
7//!
8//! - **Credential Issuer Metadata** — discovery endpoint for supported credentials
9//! - **Credential Offer** — issuer-initiated issuance flow
10//! - **Token → Credential** — exchange access tokens for VCs
11//! - **Credential format support** — `jwt_vc_json` and `ldp_vc`
12//!
13//! # References
14//!
15//! - [OpenID4VCI spec](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html)
16
17use crate::errors::{AuthError, Result};
18use serde::{Deserialize, Serialize};
19use std::collections::HashMap;
20use std::sync::Arc;
21use std::time::{SystemTime, UNIX_EPOCH};
22use tokio::sync::RwLock;
23use uuid::Uuid;
24
25// ── Credential format ───────────────────────────────────────────────
26
27/// Supported Verifiable Credential formats.
28#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
29pub enum CredentialFormat {
30    /// JWT-based VC (W3C VC Data Model, JWT encoding).
31    #[serde(rename = "jwt_vc_json")]
32    JwtVcJson,
33    /// JSON-LD with Linked Data Proofs.
34    #[serde(rename = "ldp_vc")]
35    LdpVc,
36    /// SD-JWT VC.
37    #[serde(rename = "vc+sd-jwt")]
38    SdJwtVc,
39}
40
41// ── Issuer Metadata (§10.2) ─────────────────────────────────────────
42
43/// Credential Issuer Metadata.
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct IssuerMetadata {
46    /// The Credential Issuer's identifier (URL).
47    pub credential_issuer: String,
48    /// URL of the Credential Endpoint.
49    pub credential_endpoint: String,
50    /// URL of the Batch Credential Endpoint (optional).
51    #[serde(default)]
52    pub batch_credential_endpoint: Option<String>,
53    /// Supported credential configurations.
54    pub credential_configurations_supported: HashMap<String, CredentialConfiguration>,
55    /// Display properties for the issuer.
56    #[serde(default)]
57    pub display: Vec<IssuerDisplay>,
58}
59
60impl IssuerMetadata {
61    /// Build issuer metadata from the issuer URL.
62    ///
63    /// The credential endpoint defaults to `{issuer}/credential`. Use
64    /// [`credential_endpoint`](Self::credential_endpoint) to override.
65    ///
66    /// # Example
67    /// ```rust
68    /// use auth_framework::protocols::openid4vci::*;
69    ///
70    /// let meta = IssuerMetadata::builder("https://issuer.example.com")
71    ///     .add_credential("Degree", CredentialConfiguration::new(CredentialFormat::JwtVcJson)
72    ///         .scope("degree")
73    ///         .signing_algorithms(vec!["ES256"]))
74    ///     .display("Example University", Some("en"))
75    ///     .build();
76    /// assert!(meta.credential_configurations_supported.contains_key("Degree"));
77    /// ```
78    pub fn builder(issuer: impl Into<String>) -> IssuerMetadataBuilder {
79        let issuer = issuer.into();
80        let endpoint = format!("{}/credential", issuer);
81        IssuerMetadataBuilder {
82            issuer,
83            credential_endpoint: endpoint,
84            batch_credential_endpoint: None,
85            configs: HashMap::new(),
86            display: Vec::new(),
87        }
88    }
89}
90
91/// Builder for [`IssuerMetadata`].
92pub struct IssuerMetadataBuilder {
93    issuer: String,
94    credential_endpoint: String,
95    batch_credential_endpoint: Option<String>,
96    configs: HashMap<String, CredentialConfiguration>,
97    display: Vec<IssuerDisplay>,
98}
99
100impl IssuerMetadataBuilder {
101    /// Override the credential endpoint URL.
102    pub fn credential_endpoint(mut self, url: impl Into<String>) -> Self {
103        self.credential_endpoint = url.into();
104        self
105    }
106
107    /// Set the batch credential endpoint URL.
108    pub fn batch_credential_endpoint(mut self, url: impl Into<String>) -> Self {
109        self.batch_credential_endpoint = Some(url.into());
110        self
111    }
112
113    /// Add a supported credential configuration.
114    pub fn add_credential(
115        mut self,
116        id: impl Into<String>,
117        config: CredentialConfiguration,
118    ) -> Self {
119        self.configs.insert(id.into(), config);
120        self
121    }
122
123    /// Add a display entry for the issuer.
124    pub fn display(mut self, name: impl Into<String>, locale: Option<&str>) -> Self {
125        self.display.push(IssuerDisplay {
126            name: name.into(),
127            locale: locale.map(String::from),
128        });
129        self
130    }
131
132    /// Consume the builder and produce [`IssuerMetadata`].
133    pub fn build(self) -> IssuerMetadata {
134        IssuerMetadata {
135            credential_issuer: self.issuer,
136            credential_endpoint: self.credential_endpoint,
137            batch_credential_endpoint: self.batch_credential_endpoint,
138            credential_configurations_supported: self.configs,
139            display: self.display,
140        }
141    }
142}
143
144/// Display properties for an issuer.
145#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct IssuerDisplay {
147    pub name: String,
148    #[serde(default)]
149    pub locale: Option<String>,
150}
151
152/// Configuration for a supported credential type.
153#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct CredentialConfiguration {
155    /// Credential format.
156    pub format: CredentialFormat,
157    /// Scope value for requesting this credential.
158    #[serde(default)]
159    pub scope: Option<String>,
160    /// Cryptographic binding methods supported.
161    #[serde(default)]
162    pub cryptographic_binding_methods_supported: Vec<String>,
163    /// Credential signing algorithms supported.
164    #[serde(default)]
165    pub credential_signing_alg_values_supported: Vec<String>,
166    /// Display properties.
167    #[serde(default)]
168    pub display: Vec<CredentialDisplay>,
169    /// Credential definition (type, claims).
170    #[serde(default)]
171    pub credential_definition: Option<CredentialDefinition>,
172}
173
174impl CredentialConfiguration {
175    /// Create a new credential configuration for the given format.
176    ///
177    /// # Example
178    /// ```rust
179    /// use auth_framework::protocols::openid4vci::{CredentialConfiguration, CredentialFormat};
180    ///
181    /// let cfg = CredentialConfiguration::new(CredentialFormat::JwtVcJson)
182    ///     .scope("degree")
183    ///     .signing_algorithms(vec!["ES256"]);
184    /// assert_eq!(cfg.format, CredentialFormat::JwtVcJson);
185    /// assert_eq!(cfg.scope.as_deref(), Some("degree"));
186    /// ```
187    pub fn new(format: CredentialFormat) -> Self {
188        Self {
189            format,
190            scope: None,
191            cryptographic_binding_methods_supported: Vec::new(),
192            credential_signing_alg_values_supported: Vec::new(),
193            display: Vec::new(),
194            credential_definition: None,
195        }
196    }
197
198    /// Set the scope value.
199    pub fn scope(mut self, scope: impl Into<String>) -> Self {
200        self.scope = Some(scope.into());
201        self
202    }
203
204    /// Set the supported cryptographic binding methods.
205    pub fn binding_methods(mut self, methods: Vec<impl Into<String>>) -> Self {
206        self.cryptographic_binding_methods_supported =
207            methods.into_iter().map(Into::into).collect();
208        self
209    }
210
211    /// Set the supported signing algorithms.
212    pub fn signing_algorithms(mut self, algs: Vec<impl Into<String>>) -> Self {
213        self.credential_signing_alg_values_supported = algs.into_iter().map(Into::into).collect();
214        self
215    }
216
217    /// Add a display entry.
218    pub fn with_display(mut self, display: CredentialDisplay) -> Self {
219        self.display.push(display);
220        self
221    }
222
223    /// Set the credential definition.
224    pub fn with_definition(mut self, definition: CredentialDefinition) -> Self {
225        self.credential_definition = Some(definition);
226        self
227    }
228}
229
230/// Display properties for a credential type.
231#[derive(Debug, Clone, Serialize, Deserialize)]
232pub struct CredentialDisplay {
233    pub name: String,
234    #[serde(default)]
235    pub locale: Option<String>,
236    #[serde(default)]
237    pub description: Option<String>,
238    #[serde(default)]
239    pub background_color: Option<String>,
240    #[serde(default)]
241    pub text_color: Option<String>,
242}
243
244/// Credential definition per the spec.
245#[derive(Debug, Clone, Serialize, Deserialize)]
246pub struct CredentialDefinition {
247    #[serde(rename = "type")]
248    pub types: Vec<String>,
249    #[serde(default)]
250    pub credential_subject: Option<HashMap<String, ClaimMetadata>>,
251}
252
253/// Metadata about a credential claim.
254#[derive(Debug, Clone, Serialize, Deserialize)]
255pub struct ClaimMetadata {
256    #[serde(default)]
257    pub mandatory: bool,
258    #[serde(default)]
259    pub display: Vec<ClaimDisplay>,
260}
261
262/// Display info for a claim.
263#[derive(Debug, Clone, Serialize, Deserialize)]
264pub struct ClaimDisplay {
265    pub name: String,
266    #[serde(default)]
267    pub locale: Option<String>,
268}
269
270// ── Credential Offer (§4.1) ─────────────────────────────────────────
271
272/// A Credential Offer from the issuer to the holder.
273#[derive(Debug, Clone, Serialize, Deserialize)]
274pub struct CredentialOffer {
275    /// The issuer URL.
276    pub credential_issuer: String,
277    /// List of credential configuration IDs offered.
278    pub credential_configuration_ids: Vec<String>,
279    /// Pre-authorized code grant parameters (optional).
280    #[serde(default)]
281    pub grants: Option<CredentialOfferGrants>,
282}
283
284#[derive(Debug, Clone, Serialize, Deserialize)]
285pub struct CredentialOfferGrants {
286    /// Authorization code grant.
287    #[serde(default, rename = "authorization_code")]
288    pub authorization_code: Option<AuthorizationCodeGrant>,
289    /// Pre-authorized code grant.
290    #[serde(
291        default,
292        rename = "urn:ietf:params:oauth:grant-type:pre-authorized_code"
293    )]
294    pub pre_authorized_code: Option<PreAuthorizedCodeGrant>,
295}
296
297#[derive(Debug, Clone, Serialize, Deserialize)]
298pub struct AuthorizationCodeGrant {
299    #[serde(default)]
300    pub issuer_state: Option<String>,
301}
302
303#[derive(Debug, Clone, Serialize, Deserialize)]
304pub struct PreAuthorizedCodeGrant {
305    #[serde(rename = "pre-authorized_code")]
306    pub pre_authorized_code: String,
307    #[serde(default)]
308    pub user_pin_required: bool,
309}
310
311// ── Credential Request (§7.2) ───────────────────────────────────────
312
313/// A credential request from the wallet/holder.
314#[derive(Debug, Clone, Serialize, Deserialize)]
315pub struct CredentialRequest {
316    /// Credential format.
317    pub format: CredentialFormat,
318    /// Credential definition (types, etc.).
319    #[serde(default)]
320    pub credential_definition: Option<CredentialDefinition>,
321    /// Proof of key possession.
322    #[serde(default)]
323    pub proof: Option<CredentialProof>,
324}
325
326/// Proof of possession in a credential request.
327#[derive(Debug, Clone, Serialize, Deserialize)]
328pub struct CredentialProof {
329    /// Proof type (e.g., "jwt").
330    pub proof_type: String,
331    /// The proof value (JWT or other).
332    #[serde(default)]
333    pub jwt: Option<String>,
334}
335
336// ── Credential Response (§7.3) ──────────────────────────────────────
337
338/// Credential issuance response.
339#[derive(Debug, Clone, Serialize, Deserialize)]
340pub struct CredentialResponse {
341    /// The issued credential (format-specific).
342    #[serde(default)]
343    pub credential: Option<serde_json::Value>,
344    /// Transaction ID for deferred issuance.
345    #[serde(default)]
346    pub transaction_id: Option<String>,
347    /// Nonce for subsequent requests.
348    #[serde(default)]
349    pub c_nonce: Option<String>,
350    /// Nonce lifetime in seconds.
351    #[serde(default)]
352    pub c_nonce_expires_in: Option<u64>,
353}
354
355impl CredentialResponse {
356    /// Create an immediate issuance response containing the credential.
357    ///
358    /// # Example
359    /// ```rust
360    /// use auth_framework::protocols::openid4vci::CredentialResponse;
361    ///
362    /// let resp = CredentialResponse::immediate(
363    ///     serde_json::json!({"vc": "..."}),
364    ///     "nonce-abc",
365    ///     300,
366    /// );
367    /// assert!(resp.credential.is_some());
368    /// assert!(resp.transaction_id.is_none());
369    /// ```
370    pub fn immediate(
371        credential: serde_json::Value,
372        c_nonce: impl Into<String>,
373        c_nonce_expires_in: u64,
374    ) -> Self {
375        Self {
376            credential: Some(credential),
377            transaction_id: None,
378            c_nonce: Some(c_nonce.into()),
379            c_nonce_expires_in: Some(c_nonce_expires_in),
380        }
381    }
382
383    /// Create a deferred issuance response with a transaction ID.
384    ///
385    /// # Example
386    /// ```rust
387    /// use auth_framework::protocols::openid4vci::CredentialResponse;
388    ///
389    /// let resp = CredentialResponse::deferred("tx-123", "nonce-xyz", 300);
390    /// assert!(resp.credential.is_none());
391    /// assert_eq!(resp.transaction_id.as_deref(), Some("tx-123"));
392    /// ```
393    pub fn deferred(
394        transaction_id: impl Into<String>,
395        c_nonce: impl Into<String>,
396        c_nonce_expires_in: u64,
397    ) -> Self {
398        Self {
399            credential: None,
400            transaction_id: Some(transaction_id.into()),
401            c_nonce: Some(c_nonce.into()),
402            c_nonce_expires_in: Some(c_nonce_expires_in),
403        }
404    }
405
406    /// Create a completed deferred response (credential ready, no nonce needed).
407    pub fn completed(credential: serde_json::Value) -> Self {
408        Self {
409            credential: Some(credential),
410            transaction_id: None,
411            c_nonce: None,
412            c_nonce_expires_in: None,
413        }
414    }
415}
416
417// ── Credential Issuer Service ───────────────────────────────────────
418
419fn base64_url_decode(input: &str) -> Result<Vec<u8>> {
420    use base64::Engine;
421    base64::engine::general_purpose::URL_SAFE_NO_PAD
422        .decode(input)
423        .map_err(|e| AuthError::validation(format!("Base64url decode error: {e}")))
424}
425
426/// Pending credential issuance (deferred).
427struct PendingIssuance {
428    request: CredentialRequest,
429    subject_id: String,
430    created_at: u64,
431}
432
433/// OpenID4VCI Credential Issuer.
434pub struct CredentialIssuer {
435    metadata: IssuerMetadata,
436    /// Pre-authorized codes → (credential_config_id, subject_id).
437    pre_auth_codes: Arc<RwLock<HashMap<String, (String, String)>>>,
438    /// Deferred issuance: transaction_id → pending issuance.
439    deferred: Arc<RwLock<HashMap<String, PendingIssuance>>>,
440    /// Active c_nonces for proof validation.
441    nonces: Arc<RwLock<HashMap<String, u64>>>,
442}
443
444impl CredentialIssuer {
445    /// Create a new credential issuer.
446    pub fn new(metadata: IssuerMetadata) -> Self {
447        Self {
448            metadata,
449            pre_auth_codes: Arc::new(RwLock::new(HashMap::new())),
450            deferred: Arc::new(RwLock::new(HashMap::new())),
451            nonces: Arc::new(RwLock::new(HashMap::new())),
452        }
453    }
454
455    /// Get the issuer metadata (for the discovery endpoint).
456    pub fn metadata(&self) -> &IssuerMetadata {
457        &self.metadata
458    }
459
460    /// Generate a fresh c_nonce.
461    pub async fn generate_nonce(&self, lifetime_secs: u64) -> String {
462        let nonce = Uuid::new_v4().to_string();
463        let expires = SystemTime::now()
464            .duration_since(UNIX_EPOCH)
465            .unwrap_or_default()
466            .as_secs()
467            + lifetime_secs;
468        self.nonces.write().await.insert(nonce.clone(), expires);
469        nonce
470    }
471
472    /// Validate a c_nonce.
473    pub async fn validate_nonce(&self, nonce: &str) -> bool {
474        let now = SystemTime::now()
475            .duration_since(UNIX_EPOCH)
476            .unwrap_or_default()
477            .as_secs();
478        let nonces = self.nonces.read().await;
479        matches!(nonces.get(nonce), Some(&exp) if exp > now)
480    }
481
482    /// Consume a c_nonce (single use).
483    pub async fn consume_nonce(&self, nonce: &str) -> bool {
484        let now = SystemTime::now()
485            .duration_since(UNIX_EPOCH)
486            .unwrap_or_default()
487            .as_secs();
488        let mut nonces = self.nonces.write().await;
489        match nonces.remove(nonce) {
490            Some(exp) if exp > now => true,
491            _ => false,
492        }
493    }
494
495    /// Create a credential offer with a pre-authorized code.
496    pub async fn create_offer(
497        &self,
498        credential_config_ids: Vec<String>,
499        subject_id: &str,
500    ) -> Result<CredentialOffer> {
501        if credential_config_ids.is_empty() {
502            return Err(AuthError::validation(
503                "At least one credential configuration ID is required",
504            ));
505        }
506
507        // Validate that all config IDs are supported
508        for id in &credential_config_ids {
509            if !self
510                .metadata
511                .credential_configurations_supported
512                .contains_key(id)
513            {
514                return Err(AuthError::validation(&format!(
515                    "Unknown credential configuration: {id}"
516                )));
517            }
518        }
519
520        let code = Uuid::new_v4().to_string();
521        self.pre_auth_codes.write().await.insert(
522            code.clone(),
523            (credential_config_ids[0].clone(), subject_id.to_string()),
524        );
525
526        Ok(CredentialOffer {
527            credential_issuer: self.metadata.credential_issuer.clone(),
528            credential_configuration_ids: credential_config_ids,
529            grants: Some(CredentialOfferGrants {
530                authorization_code: None,
531                pre_authorized_code: Some(PreAuthorizedCodeGrant {
532                    pre_authorized_code: code,
533                    user_pin_required: false,
534                }),
535            }),
536        })
537    }
538
539    /// Validate a pre-authorized code and return (config_id, subject_id).
540    pub async fn validate_pre_auth_code(&self, code: &str) -> Result<(String, String)> {
541        self.pre_auth_codes
542            .write()
543            .await
544            .remove(code)
545            .ok_or_else(|| AuthError::validation("Invalid or expired pre-authorized code"))
546    }
547
548    /// Process a credential request and produce a response.
549    ///
550    /// This validates the request format, checks the proof if present,
551    /// and returns either the credential or a deferred transaction ID.
552    pub async fn issue_credential(
553        &self,
554        request: &CredentialRequest,
555        subject_id: &str,
556        credential_data: Option<serde_json::Value>,
557    ) -> Result<CredentialResponse> {
558        // Validate the credential format is supported
559        let supported = self
560            .metadata
561            .credential_configurations_supported
562            .values()
563            .any(|c| c.format == request.format);
564        if !supported {
565            return Err(AuthError::validation(&format!(
566                "Unsupported credential format: {:?}",
567                request.format
568            )));
569        }
570
571        // If proof is provided, validate it
572        if let Some(ref proof) = request.proof {
573            self.validate_proof(proof).await?;
574        }
575
576        // Generate a new c_nonce
577        let c_nonce = self.generate_nonce(300).await;
578
579        match credential_data {
580            Some(data) => Ok(CredentialResponse::immediate(data, c_nonce, 300)),
581            None => {
582                // Deferred issuance
583                let tx_id = Uuid::new_v4().to_string();
584                let now = SystemTime::now()
585                    .duration_since(UNIX_EPOCH)
586                    .unwrap_or_default()
587                    .as_secs();
588                self.deferred.write().await.insert(
589                    tx_id.clone(),
590                    PendingIssuance {
591                        request: request.clone(),
592                        subject_id: subject_id.to_string(),
593                        created_at: now,
594                    },
595                );
596                Ok(CredentialResponse::deferred(tx_id, c_nonce, 300))
597            }
598        }
599    }
600
601    /// Validate a proof of possession JWT.
602    ///
603    /// Checks:
604    /// - `proof_type` is `"jwt"`
605    /// - JWT value is present and structurally valid (3-part compact serialization)
606    /// - c_nonce claim is present and matches a live nonce (consumed on success)
607    async fn validate_proof(&self, proof: &CredentialProof) -> Result<()> {
608        if proof.proof_type != "jwt" {
609            return Err(AuthError::validation(&format!(
610                "Unsupported proof type: {}",
611                proof.proof_type
612            )));
613        }
614        let jwt = proof
615            .jwt
616            .as_deref()
617            .ok_or_else(|| AuthError::validation("JWT proof value is missing"))?;
618
619        // Structural check — must be 3-part JWS compact serialization
620        let parts: Vec<&str> = jwt.split('.').collect();
621        if parts.len() != 3 {
622            return Err(AuthError::validation(
623                "Proof JWT must be compact JWS (header.payload.signature)",
624            ));
625        }
626
627        // Decode payload and check c_nonce
628        let payload_bytes = base64_url_decode(parts[1])?;
629        let payload: serde_json::Value = serde_json::from_slice(&payload_bytes)
630            .map_err(|e| AuthError::validation(format!("Invalid proof JWT payload: {e}")))?;
631
632        if let Some(nonce) = payload.get("nonce").and_then(|v| v.as_str()) {
633            if !self.consume_nonce(nonce).await {
634                return Err(AuthError::validation(
635                    "Proof JWT nonce is invalid or expired",
636                ));
637            }
638        }
639
640        Ok(())
641    }
642
643    /// Process a batch credential request (§8).
644    ///
645    /// Issues multiple credentials in a single response, one per request.
646    pub async fn issue_batch(
647        &self,
648        requests: &[CredentialRequest],
649        subject_id: &str,
650        credential_data: &[Option<serde_json::Value>],
651    ) -> Result<Vec<CredentialResponse>> {
652        if requests.is_empty() {
653            return Err(AuthError::validation(
654                "Batch request must contain at least one credential request",
655            ));
656        }
657        if requests.len() != credential_data.len() {
658            return Err(AuthError::validation(
659                "Credential data array length must match requests array length",
660            ));
661        }
662
663        let mut responses = Vec::with_capacity(requests.len());
664        for (req, data) in requests.iter().zip(credential_data.iter()) {
665            let resp = self.issue_credential(req, subject_id, data.clone()).await?;
666            responses.push(resp);
667        }
668        Ok(responses)
669    }
670
671    /// Complete a deferred credential issuance.
672    pub async fn complete_deferred(
673        &self,
674        transaction_id: &str,
675        credential_data: serde_json::Value,
676    ) -> Result<CredentialResponse> {
677        let pending = self
678            .deferred
679            .write()
680            .await
681            .remove(transaction_id)
682            .ok_or_else(|| AuthError::validation("Unknown or expired transaction ID"))?;
683
684        let mut credential = match credential_data {
685            serde_json::Value::Object(map) => map,
686            _ => {
687                return Err(AuthError::validation(
688                    "Deferred credential data must be a JSON object",
689                ));
690            }
691        };
692
693        credential
694            .entry("sub".to_string())
695            .or_insert_with(|| serde_json::Value::String(pending.subject_id.clone()));
696        credential.entry("format".to_string()).or_insert_with(|| {
697            serde_json::to_value(&pending.request.format).unwrap_or(serde_json::Value::Null)
698        });
699        credential
700            .entry("issuance_requested_at".to_string())
701            .or_insert_with(|| {
702                serde_json::Value::Number(serde_json::Number::from(pending.created_at))
703            });
704        if let Some(definition) = pending.request.credential_definition {
705            credential
706                .entry("credential_definition".to_string())
707                .or_insert_with(|| {
708                    serde_json::to_value(definition).unwrap_or(serde_json::Value::Null)
709                });
710        }
711
712        Ok(CredentialResponse::completed(serde_json::Value::Object(
713            credential,
714        )))
715    }
716
717    /// Get the count of pending deferred issuances.
718    pub async fn pending_count(&self) -> usize {
719        self.deferred.read().await.len()
720    }
721
722    /// Clean up expired nonces.
723    pub async fn cleanup_nonces(&self) -> usize {
724        let now = SystemTime::now()
725            .duration_since(UNIX_EPOCH)
726            .unwrap_or_default()
727            .as_secs();
728        let mut nonces = self.nonces.write().await;
729        let before = nonces.len();
730        nonces.retain(|_, exp| *exp > now);
731        before - nonces.len()
732    }
733}
734
735#[cfg(test)]
736mod tests {
737    use super::*;
738
739    fn test_metadata() -> IssuerMetadata {
740        IssuerMetadata::builder("https://issuer.example.com")
741            .add_credential(
742                "UniversityDegree",
743                CredentialConfiguration::new(CredentialFormat::JwtVcJson)
744                    .scope("degree")
745                    .binding_methods(vec!["did:key"])
746                    .signing_algorithms(vec!["ES256"])
747                    .with_display(CredentialDisplay {
748                        name: "University Degree".to_string(),
749                        locale: Some("en".to_string()),
750                        description: Some("A university degree credential".to_string()),
751                        background_color: Some("#12107c".to_string()),
752                        text_color: Some("#ffffff".to_string()),
753                    })
754                    .with_definition(CredentialDefinition {
755                        types: vec![
756                            "VerifiableCredential".to_string(),
757                            "UniversityDegreeCredential".to_string(),
758                        ],
759                        credential_subject: None,
760                    }),
761            )
762            .display("Example University", Some("en"))
763            .build()
764    }
765
766    // ── Credential format serialization ─────────────────────────
767
768    #[test]
769    fn test_credential_format_serialization() {
770        assert_eq!(
771            serde_json::to_string(&CredentialFormat::JwtVcJson).unwrap(),
772            r#""jwt_vc_json""#
773        );
774        assert_eq!(
775            serde_json::to_string(&CredentialFormat::LdpVc).unwrap(),
776            r#""ldp_vc""#
777        );
778        assert_eq!(
779            serde_json::to_string(&CredentialFormat::SdJwtVc).unwrap(),
780            r#""vc+sd-jwt""#
781        );
782    }
783
784    // ── Metadata ────────────────────────────────────────────────
785
786    #[test]
787    fn test_issuer_metadata_serialization() {
788        let meta = test_metadata();
789        let json = serde_json::to_value(&meta).unwrap();
790        assert_eq!(json["credential_issuer"], "https://issuer.example.com");
791        assert!(json["credential_configurations_supported"]["UniversityDegree"].is_object());
792    }
793
794    #[test]
795    fn test_metadata_roundtrip() {
796        let meta = test_metadata();
797        let json_str = serde_json::to_string(&meta).unwrap();
798        let parsed: IssuerMetadata = serde_json::from_str(&json_str).unwrap();
799        assert_eq!(parsed.credential_issuer, meta.credential_issuer);
800        assert!(
801            parsed
802                .credential_configurations_supported
803                .contains_key("UniversityDegree")
804        );
805    }
806
807    // ── Credential Offer ────────────────────────────────────────
808
809    #[tokio::test]
810    async fn test_create_offer() {
811        let issuer = CredentialIssuer::new(test_metadata());
812        let offer = issuer
813            .create_offer(vec!["UniversityDegree".to_string()], "user-1")
814            .await
815            .unwrap();
816
817        assert_eq!(offer.credential_issuer, "https://issuer.example.com");
818        assert_eq!(offer.credential_configuration_ids, vec!["UniversityDegree"]);
819        let grants = offer.grants.unwrap();
820        assert!(grants.pre_authorized_code.is_some());
821    }
822
823    #[tokio::test]
824    async fn test_create_offer_invalid_config() {
825        let issuer = CredentialIssuer::new(test_metadata());
826        let result = issuer
827            .create_offer(vec!["NonExistent".to_string()], "user-1")
828            .await;
829        assert!(result.is_err());
830    }
831
832    #[tokio::test]
833    async fn test_create_offer_empty_configs() {
834        let issuer = CredentialIssuer::new(test_metadata());
835        let result = issuer.create_offer(vec![], "user-1").await;
836        assert!(result.is_err());
837    }
838
839    // ── Pre-authorized code ─────────────────────────────────────
840
841    #[tokio::test]
842    async fn test_pre_auth_code_flow() {
843        let issuer = CredentialIssuer::new(test_metadata());
844        let offer = issuer
845            .create_offer(vec!["UniversityDegree".to_string()], "user-1")
846            .await
847            .unwrap();
848
849        let code = &offer
850            .grants
851            .unwrap()
852            .pre_authorized_code
853            .unwrap()
854            .pre_authorized_code;
855
856        let (config_id, subject_id) = issuer.validate_pre_auth_code(code).await.unwrap();
857        assert_eq!(config_id, "UniversityDegree");
858        assert_eq!(subject_id, "user-1");
859
860        // Code is single-use
861        assert!(issuer.validate_pre_auth_code(code).await.is_err());
862    }
863
864    // ── Nonce management ────────────────────────────────────────
865
866    #[tokio::test]
867    async fn test_nonce_lifecycle() {
868        let issuer = CredentialIssuer::new(test_metadata());
869        let nonce = issuer.generate_nonce(300).await;
870
871        assert!(issuer.validate_nonce(&nonce).await);
872        assert!(issuer.consume_nonce(&nonce).await);
873        // After consumption
874        assert!(!issuer.validate_nonce(&nonce).await);
875        assert!(!issuer.consume_nonce(&nonce).await);
876    }
877
878    #[tokio::test]
879    async fn test_nonce_invalid() {
880        let issuer = CredentialIssuer::new(test_metadata());
881        assert!(!issuer.validate_nonce("nonexistent").await);
882    }
883
884    // ── Credential issuance ─────────────────────────────────────
885
886    #[tokio::test]
887    async fn test_issue_credential_immediate() {
888        let issuer = CredentialIssuer::new(test_metadata());
889        let request = CredentialRequest {
890            format: CredentialFormat::JwtVcJson,
891            credential_definition: None,
892            proof: None,
893        };
894
895        let cred_data = serde_json::json!({
896            "@context": ["https://www.w3.org/2018/credentials/v1"],
897            "type": ["VerifiableCredential", "UniversityDegreeCredential"],
898            "credentialSubject": {
899                "degree": {
900                    "type": "BachelorDegree",
901                    "name": "Bachelor of Science"
902                }
903            }
904        });
905
906        let resp = issuer
907            .issue_credential(&request, "user-1", Some(cred_data))
908            .await
909            .unwrap();
910
911        assert!(resp.credential.is_some());
912        assert!(resp.transaction_id.is_none());
913        assert!(resp.c_nonce.is_some());
914    }
915
916    #[tokio::test]
917    async fn test_issue_credential_deferred() {
918        let issuer = CredentialIssuer::new(test_metadata());
919        let request = CredentialRequest {
920            format: CredentialFormat::JwtVcJson,
921            credential_definition: None,
922            proof: None,
923        };
924
925        let resp = issuer
926            .issue_credential(&request, "user-1", None)
927            .await
928            .unwrap();
929
930        assert!(resp.credential.is_none());
931        assert!(resp.transaction_id.is_some());
932        assert_eq!(issuer.pending_count().await, 1);
933
934        // Complete deferred
935        let tx_id = resp.transaction_id.unwrap();
936        let final_resp = issuer
937            .complete_deferred(&tx_id, serde_json::json!({"credential": "data"}))
938            .await
939            .unwrap();
940        assert!(final_resp.credential.is_some());
941        let completed = final_resp.credential.unwrap();
942        assert_eq!(completed["credential"], "data");
943        assert_eq!(completed["sub"], "user-1");
944        assert_eq!(completed["format"], "jwt_vc_json");
945        assert!(completed["issuance_requested_at"].is_number());
946        assert_eq!(issuer.pending_count().await, 0);
947    }
948
949    #[tokio::test]
950    async fn test_issue_credential_unsupported_format() {
951        let issuer = CredentialIssuer::new(test_metadata());
952        let request = CredentialRequest {
953            format: CredentialFormat::LdpVc,
954            credential_definition: None,
955            proof: None,
956        };
957
958        let result = issuer.issue_credential(&request, "user-1", None).await;
959        assert!(result.is_err());
960    }
961
962    #[tokio::test]
963    async fn test_issue_credential_invalid_proof_type() {
964        let issuer = CredentialIssuer::new(test_metadata());
965        let request = CredentialRequest {
966            format: CredentialFormat::JwtVcJson,
967            credential_definition: None,
968            proof: Some(CredentialProof {
969                proof_type: "ldp".to_string(),
970                jwt: None,
971            }),
972        };
973
974        let result = issuer
975            .issue_credential(&request, "user-1", Some(serde_json::json!({})))
976            .await;
977        assert!(result.is_err());
978    }
979
980    #[tokio::test]
981    async fn test_issue_credential_missing_jwt_proof() {
982        let issuer = CredentialIssuer::new(test_metadata());
983        let request = CredentialRequest {
984            format: CredentialFormat::JwtVcJson,
985            credential_definition: None,
986            proof: Some(CredentialProof {
987                proof_type: "jwt".to_string(),
988                jwt: None,
989            }),
990        };
991
992        let result = issuer
993            .issue_credential(&request, "user-1", Some(serde_json::json!({})))
994            .await;
995        assert!(result.is_err());
996    }
997
998    // ── Deferred issuance ───────────────────────────────────────
999
1000    #[tokio::test]
1001    async fn test_complete_deferred_invalid_tx() {
1002        let issuer = CredentialIssuer::new(test_metadata());
1003        let result = issuer
1004            .complete_deferred("nonexistent", serde_json::json!({}))
1005            .await;
1006        assert!(result.is_err());
1007    }
1008
1009    // ── Credential offer serialization ──────────────────────────
1010
1011    #[test]
1012    fn test_credential_offer_roundtrip() {
1013        let offer = CredentialOffer {
1014            credential_issuer: "https://issuer.example".to_string(),
1015            credential_configuration_ids: vec!["DegreeCredential".to_string()],
1016            grants: Some(CredentialOfferGrants {
1017                authorization_code: None,
1018                pre_authorized_code: Some(PreAuthorizedCodeGrant {
1019                    pre_authorized_code: "code123".to_string(),
1020                    user_pin_required: true,
1021                }),
1022            }),
1023        };
1024        let json = serde_json::to_string(&offer).unwrap();
1025        let parsed: CredentialOffer = serde_json::from_str(&json).unwrap();
1026        assert_eq!(parsed.credential_issuer, offer.credential_issuer);
1027        assert!(
1028            parsed
1029                .grants
1030                .unwrap()
1031                .pre_authorized_code
1032                .unwrap()
1033                .user_pin_required
1034        );
1035    }
1036
1037    // ── Request / Response models ───────────────────────────────
1038
1039    #[test]
1040    fn test_credential_request_serialization() {
1041        let req = CredentialRequest {
1042            format: CredentialFormat::JwtVcJson,
1043            credential_definition: Some(CredentialDefinition {
1044                types: vec!["VerifiableCredential".to_string()],
1045                credential_subject: None,
1046            }),
1047            proof: Some(CredentialProof {
1048                proof_type: "jwt".to_string(),
1049                jwt: Some("eyJ...".to_string()),
1050            }),
1051        };
1052        let json = serde_json::to_value(&req).unwrap();
1053        assert_eq!(json["format"], "jwt_vc_json");
1054        assert!(json["proof"]["jwt"].is_string());
1055    }
1056
1057    // ── Builder / factory helpers ───────────────────────────────
1058
1059    #[test]
1060    fn test_credential_response_immediate() {
1061        let resp = CredentialResponse::immediate(serde_json::json!({"vc": "data"}), "nonce-1", 300);
1062        assert!(resp.credential.is_some());
1063        assert!(resp.transaction_id.is_none());
1064        assert_eq!(resp.c_nonce.as_deref(), Some("nonce-1"));
1065        assert_eq!(resp.c_nonce_expires_in, Some(300));
1066    }
1067
1068    #[test]
1069    fn test_credential_response_deferred() {
1070        let resp = CredentialResponse::deferred("tx-1", "nonce-2", 600);
1071        assert!(resp.credential.is_none());
1072        assert_eq!(resp.transaction_id.as_deref(), Some("tx-1"));
1073        assert_eq!(resp.c_nonce.as_deref(), Some("nonce-2"));
1074    }
1075
1076    #[test]
1077    fn test_credential_response_completed() {
1078        let resp = CredentialResponse::completed(serde_json::json!({"done": true}));
1079        assert!(resp.credential.is_some());
1080        assert!(resp.transaction_id.is_none());
1081        assert!(resp.c_nonce.is_none());
1082    }
1083
1084    #[test]
1085    fn test_credential_configuration_builder() {
1086        let cfg = CredentialConfiguration::new(CredentialFormat::JwtVcJson)
1087            .scope("degree")
1088            .binding_methods(vec!["did:key"])
1089            .signing_algorithms(vec!["ES256", "EdDSA"]);
1090        assert_eq!(cfg.format, CredentialFormat::JwtVcJson);
1091        assert_eq!(cfg.scope.as_deref(), Some("degree"));
1092        assert_eq!(cfg.cryptographic_binding_methods_supported, vec!["did:key"]);
1093        assert_eq!(cfg.credential_signing_alg_values_supported.len(), 2);
1094    }
1095
1096    #[test]
1097    fn test_issuer_metadata_builder() {
1098        let meta = IssuerMetadata::builder("https://issuer.example.com")
1099            .add_credential(
1100                "TestCred",
1101                CredentialConfiguration::new(CredentialFormat::SdJwtVc).scope("test"),
1102            )
1103            .display("Test Issuer", None)
1104            .build();
1105        assert_eq!(meta.credential_issuer, "https://issuer.example.com");
1106        assert_eq!(
1107            meta.credential_endpoint,
1108            "https://issuer.example.com/credential"
1109        );
1110        assert!(
1111            meta.credential_configurations_supported
1112                .contains_key("TestCred")
1113        );
1114        assert_eq!(meta.display[0].name, "Test Issuer");
1115        assert!(meta.display[0].locale.is_none());
1116    }
1117
1118    #[test]
1119    fn test_issuer_metadata_builder_custom_endpoint() {
1120        let meta = IssuerMetadata::builder("https://example.com")
1121            .credential_endpoint("https://example.com/api/vc/issue")
1122            .batch_credential_endpoint("https://example.com/api/vc/batch")
1123            .build();
1124        assert_eq!(meta.credential_endpoint, "https://example.com/api/vc/issue");
1125        assert_eq!(
1126            meta.batch_credential_endpoint.as_deref(),
1127            Some("https://example.com/api/vc/batch")
1128        );
1129    }
1130}