baseid_wallet_core/
receive.rs

1//! Credential receive operations — bridging OID4VCI responses into the wallet store.
2
3use crate::error::WalletError;
4use crate::holder::WalletStore;
5use crate::AnyCredential;
6use baseid_core::types::CredentialId;
7
8/// Parse a credential value from an OID4VCI response into an AnyCredential.
9///
10/// Uses `format_hint` to determine parsing strategy:
11/// - `"jwt_vc_json"`: JWT string -> decode -> parse VerifiableCredential from `vc` claim
12/// - `"dc+sd-jwt"` or `"vc+sd-jwt"`: compact SD-JWT string -> `SdJwt::parse()`
13/// - Other: attempt W3C VC JSON parsing as fallback
14pub fn parse_issued_credential(
15    credential_value: &serde_json::Value,
16    format_hint: &str,
17) -> baseid_core::Result<AnyCredential> {
18    match format_hint {
19        "jwt_vc_json" => {
20            let jwt_str = credential_value.as_str().ok_or_else(|| {
21                WalletError::CredentialParseError(
22                    "jwt_vc_json credential value must be a string".to_string(),
23                )
24            })?;
25
26            let (_header, claims) = baseid_crypto::decode_jwt_unverified(jwt_str)?;
27
28            let vc_value = claims.get("vc").ok_or_else(|| {
29                WalletError::CredentialParseError("JWT missing 'vc' claim".to_string())
30            })?;
31
32            let vc: baseid_vc::VerifiableCredential = serde_json::from_value(vc_value.clone())
33                .map_err(|e| WalletError::CredentialParseError(e.to_string()))?;
34
35            Ok(AnyCredential::W3cVc(vc))
36        }
37        hint if hint.starts_with("dc+sd-jwt") || hint.starts_with("vc+sd-jwt") => {
38            let compact = credential_value.as_str().ok_or_else(|| {
39                WalletError::CredentialParseError(
40                    "SD-JWT credential value must be a string".to_string(),
41                )
42            })?;
43
44            let sd_jwt = baseid_sd_jwt::SdJwt::parse(compact)?;
45            Ok(AnyCredential::SdJwtVc(sd_jwt))
46        }
47        _ => {
48            // Attempt W3C VC JSON parsing as fallback (e.g. ldp_vc)
49            let vc: baseid_vc::VerifiableCredential =
50                serde_json::from_value(credential_value.clone())
51                    .map_err(|_| WalletError::UnsupportedFormat(format_hint.to_string()))?;
52            Ok(AnyCredential::W3cVc(vc))
53        }
54    }
55}
56
57/// Tracking info for deferred credential issuance.
58#[derive(Debug, Clone)]
59pub struct DeferredIssuance {
60    /// The transaction ID for polling the deferred credential endpoint.
61    pub transaction_id: String,
62    /// Polling interval in seconds.
63    pub interval: u64,
64    /// The credential format hint for parsing when the credential arrives.
65    pub format_hint: String,
66}
67
68/// Extract deferred issuance info from a `CredentialResponse`, if present.
69pub fn extract_deferred(
70    response: &baseid_oid4vci::credential::CredentialResponse,
71    format_hint: &str,
72) -> Option<DeferredIssuance> {
73    response
74        .transaction_id
75        .as_ref()
76        .map(|tid| DeferredIssuance {
77            transaction_id: tid.clone(),
78            interval: response.interval.unwrap_or(5),
79            format_hint: format_hint.to_string(),
80        })
81}
82
83/// Receive credentials from an OID4VCI response and store them.
84///
85/// Returns the list of newly stored credential IDs.
86/// For deferred responses (no credentials, has `transaction_id`), returns an empty vec.
87pub async fn receive_oid4vci_credential<S: WalletStore>(
88    store: &S,
89    response: &baseid_oid4vci::credential::CredentialResponse,
90    format_hint: &str,
91) -> baseid_core::Result<Vec<CredentialId>> {
92    let entries = match &response.credentials {
93        Some(creds) => creds,
94        None => return Ok(vec![]),
95    };
96
97    let mut ids = Vec::with_capacity(entries.len());
98    for entry in entries {
99        let cred = parse_issued_credential(&entry.credential, format_hint)?;
100        let id = store.store_credential(cred).await?;
101        ids.push(id);
102    }
103
104    Ok(ids)
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110    use crate::store::InMemoryStore;
111    use baseid_core::types::KeyType;
112    use baseid_crypto::KeyPair;
113    use baseid_oid4vci::credential::{CredentialEntry, CredentialResponse};
114    use baseid_vc::credential::Issuer;
115    use baseid_vc::VerifiableCredential;
116
117    fn sample_vc() -> VerifiableCredential {
118        VerifiableCredential {
119            context: vec!["https://www.w3.org/ns/credentials/v2".to_string()],
120            id: Some("urn:uuid:test-receive".to_string()),
121            r#type: vec![
122                "VerifiableCredential".to_string(),
123                "UniversityDegreeCredential".to_string(),
124            ],
125            issuer: Issuer::Uri("did:key:z6MkIssuer".to_string()),
126            valid_from: Some("2024-01-01T00:00:00Z".to_string()),
127            valid_until: None,
128            credential_subject: serde_json::json!({"id": "did:key:z6MkHolder"}),
129            credential_status: None,
130            proof: None,
131        }
132    }
133
134    #[test]
135    fn parse_jwt_vc_credential() {
136        let kp = KeyPair::generate(KeyType::Ed25519).unwrap();
137        let vc = sample_vc();
138        let jwt = baseid_vc::sign_credential_jwt(&vc, &kp, "did:key:z6MkIssuer#key-0").unwrap();
139
140        let value = serde_json::Value::String(jwt);
141        let parsed = parse_issued_credential(&value, "jwt_vc_json").unwrap();
142
143        match &parsed {
144            AnyCredential::W3cVc(decoded_vc) => {
145                assert_eq!(decoded_vc.id, vc.id);
146                assert_eq!(decoded_vc.r#type, vc.r#type);
147            }
148            _ => panic!("Expected W3cVc variant"),
149        }
150    }
151
152    #[test]
153    fn parse_sd_jwt_credential() {
154        let sd_jwt = baseid_sd_jwt::SdJwt {
155            jwt: "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJ0ZXN0In0.c2ln".to_string(),
156            disclosures: vec![],
157            key_binding_jwt: None,
158        };
159        let compact = sd_jwt.serialize();
160        let value = serde_json::Value::String(compact);
161
162        let parsed = parse_issued_credential(&value, "dc+sd-jwt").unwrap();
163        match &parsed {
164            AnyCredential::SdJwtVc(parsed_sd) => {
165                assert_eq!(parsed_sd.jwt, sd_jwt.jwt);
166            }
167            _ => panic!("Expected SdJwtVc variant"),
168        }
169    }
170
171    #[test]
172    fn parse_json_vc_credential() {
173        let vc = sample_vc();
174        let value = serde_json::to_value(&vc).unwrap();
175
176        let parsed = parse_issued_credential(&value, "ldp_vc").unwrap();
177        match &parsed {
178            AnyCredential::W3cVc(decoded_vc) => {
179                assert_eq!(decoded_vc.id, vc.id);
180                assert_eq!(decoded_vc.issuer, vc.issuer);
181            }
182            _ => panic!("Expected W3cVc variant"),
183        }
184    }
185
186    #[tokio::test]
187    async fn receive_single_credential() {
188        let kp = KeyPair::generate(KeyType::Ed25519).unwrap();
189        let vc = sample_vc();
190        let jwt = baseid_vc::sign_credential_jwt(&vc, &kp, "kid").unwrap();
191
192        let response = CredentialResponse {
193            credentials: Some(vec![CredentialEntry {
194                credential: serde_json::Value::String(jwt),
195            }]),
196            transaction_id: None,
197            notification_id: None,
198            interval: None,
199        };
200
201        let store = InMemoryStore::new();
202        let ids = receive_oid4vci_credential(&store, &response, "jwt_vc_json")
203            .await
204            .unwrap();
205
206        assert_eq!(ids.len(), 1);
207        assert_eq!(store.len(), 1);
208    }
209
210    #[tokio::test]
211    async fn receive_multiple_credentials() {
212        let kp = KeyPair::generate(KeyType::Ed25519).unwrap();
213        let vc1 = sample_vc();
214        let mut vc2 = sample_vc();
215        vc2.id = Some("urn:uuid:test-receive-2".to_string());
216
217        let jwt1 = baseid_vc::sign_credential_jwt(&vc1, &kp, "kid").unwrap();
218        let jwt2 = baseid_vc::sign_credential_jwt(&vc2, &kp, "kid").unwrap();
219
220        let response = CredentialResponse {
221            credentials: Some(vec![
222                CredentialEntry {
223                    credential: serde_json::Value::String(jwt1),
224                },
225                CredentialEntry {
226                    credential: serde_json::Value::String(jwt2),
227                },
228            ]),
229            transaction_id: None,
230            notification_id: None,
231            interval: None,
232        };
233
234        let store = InMemoryStore::new();
235        let ids = receive_oid4vci_credential(&store, &response, "jwt_vc_json")
236            .await
237            .unwrap();
238
239        assert_eq!(ids.len(), 2);
240        assert_eq!(store.len(), 2);
241    }
242
243    #[tokio::test]
244    async fn receive_deferred_returns_empty() {
245        let response = CredentialResponse {
246            credentials: None,
247            transaction_id: Some("txn-123".to_string()),
248            notification_id: None,
249            interval: Some(5),
250        };
251
252        let store = InMemoryStore::new();
253        let ids = receive_oid4vci_credential(&store, &response, "jwt_vc_json")
254            .await
255            .unwrap();
256
257        assert!(ids.is_empty());
258        assert!(store.is_empty());
259    }
260
261    #[test]
262    fn extract_deferred_present() {
263        let response = CredentialResponse {
264            credentials: None,
265            transaction_id: Some("txn-456".to_string()),
266            notification_id: None,
267            interval: Some(10),
268        };
269
270        let deferred = extract_deferred(&response, "jwt_vc_json").unwrap();
271        assert_eq!(deferred.transaction_id, "txn-456");
272        assert_eq!(deferred.interval, 10);
273        assert_eq!(deferred.format_hint, "jwt_vc_json");
274    }
275
276    #[test]
277    fn extract_deferred_absent() {
278        let kp = KeyPair::generate(KeyType::Ed25519).unwrap();
279        let vc = sample_vc();
280        let jwt = baseid_vc::sign_credential_jwt(&vc, &kp, "kid").unwrap();
281
282        let response = CredentialResponse {
283            credentials: Some(vec![CredentialEntry {
284                credential: serde_json::Value::String(jwt),
285            }]),
286            transaction_id: None,
287            notification_id: None,
288            interval: None,
289        };
290
291        assert!(extract_deferred(&response, "jwt_vc_json").is_none());
292    }
293}