1use crate::error::WalletError;
4use crate::holder::WalletStore;
5use crate::AnyCredential;
6use baseid_core::types::CredentialId;
7
8pub 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 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#[derive(Debug, Clone)]
59pub struct DeferredIssuance {
60 pub transaction_id: String,
62 pub interval: u64,
64 pub format_hint: String,
66}
67
68pub 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
83pub 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}