baseid_oid4vci/
credential.rs

1//! Credential request and response types for OID4VCI.
2
3use serde::{Deserialize, Serialize};
4
5/// Token request for the pre-authorized code flow.
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct TokenRequest {
8    /// Grant type — `"urn:ietf:params:oauth:grant-type:pre-authorized_code"` for pre-auth flow.
9    pub grant_type: String,
10    /// The pre-authorized code from the credential offer.
11    #[serde(rename = "pre-authorized_code")]
12    pub pre_authorized_code: String,
13    /// Optional transaction code (PIN) required by the issuer.
14    #[serde(skip_serializing_if = "Option::is_none")]
15    pub tx_code: Option<String>,
16}
17
18/// Credential request sent to the credential endpoint (OID4VCI 1.0).
19///
20/// Use `credential_configuration_id` (preferred) or `credential_identifier` to identify
21/// the requested credential. The `format` field is kept for backward compatibility
22/// but is not required in OID4VCI 1.0.
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct CredentialRequest {
25    /// Credential configuration ID from issuer metadata (OID4VCI 1.0 preferred).
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub credential_configuration_id: Option<String>,
28    /// Credential identifier from token response authorization_details (OID4VCI 1.0).
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub credential_identifier: Option<String>,
31    /// Credential format identifier (e.g., "jwt_vc_json", "dc+sd-jwt", "mso_mdoc").
32    /// Deprecated in OID4VCI 1.0 — use credential_configuration_id instead.
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub format: Option<String>,
35    /// Credential definition specifying the type of credential.
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub credential_definition: Option<serde_json::Value>,
38    /// Proof of possession of the holder's key.
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub proof: Option<ProofOfPossession>,
41}
42
43/// Proof of key binding sent with a credential request.
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct ProofOfPossession {
46    /// Proof type — typically "jwt".
47    pub proof_type: String,
48    /// The signed JWT proving key possession.
49    pub jwt: String,
50}
51
52/// A single credential entry in the credential response.
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct CredentialEntry {
55    /// The issued credential (format depends on the request).
56    pub credential: serde_json::Value,
57}
58
59/// Credential response from the issuer's credential endpoint (OID4VCI 1.0).
60///
61/// For immediate issuance, `credentials` contains the issued credential(s).
62/// For deferred issuance, `transaction_id` identifies the pending issuance.
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct CredentialResponse {
65    /// Issued credentials (present for immediate issuance).
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub credentials: Option<Vec<CredentialEntry>>,
68    /// Transaction ID for deferred issuance.
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub transaction_id: Option<String>,
71    /// Notification ID for credential lifecycle events.
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub notification_id: Option<String>,
74    /// Polling interval in seconds (deferred issuance).
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub interval: Option<u64>,
77}
78
79impl CredentialResponse {
80    /// Returns the first issued credential, if any.
81    pub fn first_credential(&self) -> Option<&serde_json::Value> {
82        self.credentials
83            .as_ref()
84            .and_then(|creds| creds.first())
85            .map(|entry| &entry.credential)
86    }
87
88    /// Returns true if this is a deferred issuance response.
89    pub fn is_deferred(&self) -> bool {
90        self.transaction_id.is_some()
91    }
92}
93
94/// Well-known credential format identifiers from OID4VCI 1.0.
95#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
96pub enum CredentialFormat {
97    /// W3C VC signed as JWT (no JSON-LD processing).
98    #[serde(rename = "jwt_vc_json")]
99    JwtVcJson,
100    /// W3C VC secured using Data Integrity proofs.
101    #[serde(rename = "ldp_vc")]
102    LdpVc,
103    /// IETF SD-JWT Verifiable Credential.
104    #[serde(rename = "dc+sd-jwt")]
105    DcSdJwt,
106    /// ISO 18013-5 mobile document (mDL/mdoc).
107    #[serde(rename = "mso_mdoc")]
108    MsoMdoc,
109    /// W3C VC JWT with JSON-LD processing.
110    #[serde(rename = "jwt_vc_json-ld")]
111    JwtVcJsonLd,
112    /// Other/unknown format.
113    #[serde(untagged)]
114    Other(String),
115}
116
117/// Server error codes from OID4VCI 1.0 Section 8.3.2.
118#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
119pub enum ServerErrorCode {
120    #[serde(rename = "invalid_credential_request")]
121    InvalidCredentialRequest,
122    #[serde(rename = "unknown_credential_configuration")]
123    UnknownCredentialConfiguration,
124    #[serde(rename = "unknown_credential_identifier")]
125    UnknownCredentialIdentifier,
126    #[serde(rename = "invalid_proof")]
127    InvalidProof,
128    #[serde(rename = "invalid_nonce")]
129    InvalidNonce,
130    #[serde(rename = "invalid_encryption_parameters")]
131    InvalidEncryptionParameters,
132    #[serde(rename = "credential_request_denied")]
133    CredentialRequestDenied,
134    /// Other/unknown error code.
135    #[serde(untagged)]
136    Other(String),
137}
138
139/// Pre-authorized code grant type constant.
140pub const PRE_AUTHORIZED_CODE_GRANT_TYPE: &str =
141    "urn:ietf:params:oauth:grant-type:pre-authorized_code";
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn token_request_serialization() {
149        let req = TokenRequest {
150            grant_type: PRE_AUTHORIZED_CODE_GRANT_TYPE.to_string(),
151            pre_authorized_code: "code123".to_string(),
152            tx_code: Some("1234".to_string()),
153        };
154        let json = serde_json::to_value(&req).unwrap();
155        assert_eq!(
156            json["grant_type"],
157            "urn:ietf:params:oauth:grant-type:pre-authorized_code"
158        );
159        assert_eq!(json["pre-authorized_code"], "code123");
160        assert_eq!(json["tx_code"], "1234");
161    }
162
163    #[test]
164    fn credential_request_with_configuration_id() {
165        let req = CredentialRequest {
166            credential_configuration_id: Some("UniversityDegree".to_string()),
167            credential_identifier: None,
168            format: None,
169            credential_definition: None,
170            proof: None,
171        };
172        let json = serde_json::to_value(&req).unwrap();
173        assert_eq!(json["credential_configuration_id"], "UniversityDegree");
174        assert!(json.get("format").is_none());
175        assert!(json.get("proof").is_none());
176    }
177
178    #[test]
179    fn credential_request_with_identifier() {
180        let req = CredentialRequest {
181            credential_configuration_id: None,
182            credential_identifier: Some("CivilEngineeringDegree-2023".to_string()),
183            format: None,
184            credential_definition: None,
185            proof: None,
186        };
187        let json = serde_json::to_value(&req).unwrap();
188        assert_eq!(json["credential_identifier"], "CivilEngineeringDegree-2023");
189        assert!(json.get("credential_configuration_id").is_none());
190    }
191
192    #[test]
193    fn credential_response_immediate() {
194        let json = serde_json::json!({
195            "credentials": [
196                { "credential": "eyJhbGciOiJFUzI1NiJ9..." }
197            ]
198        });
199        let resp: CredentialResponse = serde_json::from_value(json).unwrap();
200        assert!(!resp.is_deferred());
201        assert_eq!(
202            resp.first_credential().unwrap(),
203            &serde_json::json!("eyJhbGciOiJFUzI1NiJ9...")
204        );
205    }
206
207    #[test]
208    fn credential_response_multiple_with_notification() {
209        let json = serde_json::json!({
210            "credentials": [
211                { "credential": "cred-1" },
212                { "credential": "cred-2" }
213            ],
214            "notification_id": "3fwe98js"
215        });
216        let resp: CredentialResponse = serde_json::from_value(json).unwrap();
217        let creds = resp.credentials.as_ref().unwrap();
218        assert_eq!(creds.len(), 2);
219        assert_eq!(creds[1].credential, "cred-2");
220        assert_eq!(resp.notification_id.as_deref(), Some("3fwe98js"));
221    }
222
223    #[test]
224    fn credential_response_deferred() {
225        let json = serde_json::json!({
226            "transaction_id": "8xLOxBtZp8",
227            "interval": 3600
228        });
229        let resp: CredentialResponse = serde_json::from_value(json).unwrap();
230        assert!(resp.is_deferred());
231        assert_eq!(resp.transaction_id.as_deref(), Some("8xLOxBtZp8"));
232        assert_eq!(resp.interval, Some(3600));
233        assert!(resp.first_credential().is_none());
234    }
235
236    // --- Spec vector tests ---
237
238    #[test]
239    fn spec_vector_credential_response_immediate() {
240        let resp: CredentialResponse =
241            serde_json::from_str(baseid_test_vectors::oid4vci::CREDENTIAL_RESPONSE_IMMEDIATE)
242                .unwrap();
243        assert!(!resp.is_deferred());
244        let cred = resp.first_credential().unwrap();
245        assert!(cred.as_str().unwrap().contains("LUpixVCWJk0"));
246    }
247
248    #[test]
249    fn spec_vector_credential_response_multiple() {
250        let resp: CredentialResponse =
251            serde_json::from_str(baseid_test_vectors::oid4vci::CREDENTIAL_RESPONSE_MULTIPLE)
252                .unwrap();
253        let creds = resp.credentials.as_ref().unwrap();
254        assert_eq!(creds.len(), 2);
255        assert_eq!(resp.notification_id.as_deref(), Some("3fwe98js"));
256    }
257
258    #[test]
259    fn spec_vector_credential_response_deferred() {
260        let resp: CredentialResponse =
261            serde_json::from_str(baseid_test_vectors::oid4vci::CREDENTIAL_RESPONSE_DEFERRED)
262                .unwrap();
263        assert!(resp.is_deferred());
264        assert_eq!(resp.transaction_id.as_deref(), Some("8xLOxBtZp8"));
265        assert_eq!(resp.interval, Some(3600));
266    }
267
268    #[test]
269    fn spec_vector_credential_request_mdoc() {
270        let v: serde_json::Value =
271            serde_json::from_str(baseid_test_vectors::oid4vci::CREDENTIAL_REQUEST_MDOC).unwrap();
272        assert_eq!(v["credential_configuration_id"], "org.iso.18013.5.1.mDL");
273        assert!(v["proofs"]["jwt"].is_array());
274    }
275
276    #[test]
277    fn spec_vector_credential_request_by_identifier() {
278        let v: serde_json::Value =
279            serde_json::from_str(baseid_test_vectors::oid4vci::CREDENTIAL_REQUEST_BY_IDENTIFIER)
280                .unwrap();
281        assert_eq!(v["credential_identifier"], "CivilEngineeringDegree-2023");
282    }
283
284    #[test]
285    fn spec_vector_proof_jwt_header() {
286        let v: serde_json::Value =
287            serde_json::from_str(baseid_test_vectors::oid4vci::PROOF_JWT_HEADER).unwrap();
288        assert_eq!(v["typ"], "openid4vci-proof+jwt");
289        assert_eq!(v["alg"], "ES256");
290        assert_eq!(v["jwk"]["kty"], "EC");
291        assert_eq!(v["jwk"]["crv"], "P-256");
292    }
293
294    #[test]
295    fn spec_vector_proof_jwt_payload() {
296        let v: serde_json::Value =
297            serde_json::from_str(baseid_test_vectors::oid4vci::PROOF_JWT_PAYLOAD).unwrap();
298        assert_eq!(v["aud"], "https://credential-issuer.example.com");
299        assert!(v["iat"].is_u64());
300        assert_eq!(v["nonce"], "LarRGSbmUPYtRYO6BQ4yn8");
301    }
302
303    #[test]
304    fn spec_vector_nonce_response() {
305        use crate::token::NonceResponse;
306        let resp: NonceResponse =
307            serde_json::from_str(baseid_test_vectors::oid4vci::NONCE_RESPONSE).unwrap();
308        assert_eq!(resp.c_nonce, "wKI4LT17ac15ES9bw8ac4");
309    }
310
311    #[test]
312    fn spec_vector_credential_error() {
313        let v: serde_json::Value =
314            serde_json::from_str(baseid_test_vectors::oid4vci::CREDENTIAL_ERROR).unwrap();
315        assert_eq!(v["error"], "unknown_credential_configuration");
316    }
317
318    #[test]
319    fn spec_vector_token_error() {
320        let v: serde_json::Value =
321            serde_json::from_str(baseid_test_vectors::oid4vci::TOKEN_ERROR).unwrap();
322        assert_eq!(v["error"], "invalid_grant");
323    }
324
325    // --- Phase E: Typed enum tests ---
326
327    #[test]
328    fn credential_format_serde_known() {
329        let formats = vec![
330            (CredentialFormat::JwtVcJson, "\"jwt_vc_json\""),
331            (CredentialFormat::LdpVc, "\"ldp_vc\""),
332            (CredentialFormat::DcSdJwt, "\"dc+sd-jwt\""),
333            (CredentialFormat::MsoMdoc, "\"mso_mdoc\""),
334            (CredentialFormat::JwtVcJsonLd, "\"jwt_vc_json-ld\""),
335        ];
336        for (variant, expected) in formats {
337            let json = serde_json::to_string(&variant).unwrap();
338            assert_eq!(json, expected);
339            let parsed: CredentialFormat = serde_json::from_str(&json).unwrap();
340            assert_eq!(parsed, variant);
341        }
342    }
343
344    #[test]
345    fn credential_format_serde_unknown() {
346        let unknown: CredentialFormat = serde_json::from_str("\"new_format\"").unwrap();
347        assert_eq!(unknown, CredentialFormat::Other("new_format".to_string()));
348        let json = serde_json::to_string(&unknown).unwrap();
349        assert_eq!(json, "\"new_format\"");
350    }
351
352    #[test]
353    fn server_error_code_serde_known() {
354        let code: ServerErrorCode =
355            serde_json::from_str("\"unknown_credential_configuration\"").unwrap();
356        assert_eq!(code, ServerErrorCode::UnknownCredentialConfiguration);
357        let json = serde_json::to_string(&code).unwrap();
358        assert_eq!(json, "\"unknown_credential_configuration\"");
359    }
360
361    #[test]
362    fn server_error_code_serde_all_variants() {
363        for code_str in baseid_test_vectors::oid4vci::CREDENTIAL_ERROR_CODES {
364            let code: ServerErrorCode = serde_json::from_str(&format!("\"{}\"", code_str)).unwrap();
365            let json = serde_json::to_string(&code).unwrap();
366            assert_eq!(json, format!("\"{}\"", code_str));
367        }
368    }
369}