baseid_wallet_core/
lib.rs

1//! # baseid-wallet-core
2//!
3//! Wallet business logic for credential lifecycle management.
4//!
5//! Provides the core logic for a digital identity wallet:
6//! - Receive credentials (via OID4VCI, DIDComm)
7//! - Store credentials (via `WalletStore` trait)
8//! - Match credentials against presentation requests (PE + DCQL)
9//! - Present credentials (via OID4VP, DIDComm)
10//! - Manage consent (via `baseid-pctf`)
11//!
12//! The UI layer is platform-native (React Native / Swift / Kotlin);
13//! this crate provides only the business logic.
14
15pub mod error;
16pub mod holder;
17pub mod matcher;
18pub mod presenter;
19pub mod receive;
20pub mod store;
21pub mod wallet;
22
23use serde::{Deserialize, Serialize};
24
25use baseid_anoncreds::AnonCredsCredential;
26use baseid_bbs::BbsCredential;
27use baseid_core::credential::{CredentialMetadata, CredentialSummary};
28use baseid_core::types::{CredentialFormat, CredentialId};
29use baseid_vc::credential::Issuer;
30
31/// A format-agnostic credential wrapper.
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub enum AnyCredential {
34    /// W3C Verifiable Credential
35    W3cVc(baseid_vc::VerifiableCredential),
36    /// ISO 18013-5 mobile document
37    Mdoc(baseid_mdl::MobileDocument),
38    /// SD-JWT Verifiable Credential
39    SdJwtVc(baseid_sd_jwt::SdJwt),
40    /// BBS+ Data Integrity credential (unlinkable selective disclosure)
41    Bbs(BbsCredential),
42    /// Hyperledger AnonCreds credential (CL signatures)
43    AnonCreds(AnonCredsCredential),
44}
45
46impl AnyCredential {
47    /// Returns the credential format for this credential.
48    pub fn credential_format(&self) -> CredentialFormat {
49        match self {
50            AnyCredential::W3cVc(_) => CredentialFormat::W3cVc,
51            AnyCredential::Mdoc(_) => CredentialFormat::Mdl,
52            AnyCredential::SdJwtVc(_) => CredentialFormat::SdJwtVc,
53            AnyCredential::Bbs(_) => CredentialFormat::Bbs,
54            AnyCredential::AnonCreds(_) => CredentialFormat::AnonCreds,
55        }
56    }
57
58    /// Extract the issuer identifier from the credential.
59    pub fn issuer_id(&self) -> String {
60        match self {
61            AnyCredential::W3cVc(vc) => match &vc.issuer {
62                Issuer::Uri(uri) => uri.clone(),
63                Issuer::Object { id, .. } => id.clone(),
64            },
65            AnyCredential::Mdoc(_) => "mdoc-issuer".to_string(), // mDL issuer is in MSO
66            AnyCredential::SdJwtVc(_) => "sd-jwt-issuer".to_string(), // SD-JWT issuer is in JWT iss claim
67            AnyCredential::Bbs(cred) => cred.issuer.clone(),
68            AnyCredential::AnonCreds(cred) => cred.issuer_did.clone(),
69        }
70    }
71
72    /// Generate a human-readable label for this credential.
73    pub fn label(&self) -> String {
74        match self {
75            AnyCredential::W3cVc(vc) => {
76                let types: Vec<&str> = vc
77                    .r#type
78                    .iter()
79                    .filter(|t| *t != "VerifiableCredential")
80                    .map(|s| s.as_str())
81                    .collect();
82                if types.is_empty() {
83                    "Verifiable Credential".to_string()
84                } else {
85                    types.join(", ")
86                }
87            }
88            AnyCredential::Mdoc(_) => "Mobile Document".to_string(),
89            AnyCredential::SdJwtVc(_) => "SD-JWT Credential".to_string(),
90            AnyCredential::Bbs(cred) => {
91                let types: Vec<&str> = cred
92                    .types
93                    .iter()
94                    .filter(|t| *t != "VerifiableCredential")
95                    .map(|s| s.as_str())
96                    .collect();
97                if types.is_empty() {
98                    "BBS+ Credential".to_string()
99                } else {
100                    types[0].to_string()
101                }
102            }
103            AnyCredential::AnonCreds(cred) => {
104                let vc = cred.to_w3c_vc();
105                let types: Vec<&str> = vc
106                    .r#type
107                    .iter()
108                    .filter(|t| *t != "VerifiableCredential")
109                    .map(|s| s.as_str())
110                    .collect();
111                if types.is_empty() {
112                    "AnonCreds Credential".to_string()
113                } else {
114                    types[0].to_string()
115                }
116            }
117        }
118    }
119}
120
121/// A stored credential with its metadata.
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct CredentialRecord {
124    /// Local credential identifier.
125    pub id: CredentialId,
126    /// The credential data.
127    pub credential: AnyCredential,
128    /// Original raw form (JWT string or compact serialization) for re-presentation.
129    #[serde(skip_serializing_if = "Option::is_none")]
130    pub raw: Option<String>,
131}
132
133impl CredentialRecord {
134    /// Build CredentialMetadata from this record.
135    pub fn metadata(&self) -> CredentialMetadata {
136        let (issuance_date, expiration_date) = match &self.credential {
137            AnyCredential::W3cVc(vc) => (
138                vc.valid_from.clone().unwrap_or_default(),
139                vc.valid_until.clone(),
140            ),
141            AnyCredential::Mdoc(_) => (String::new(), None),
142            AnyCredential::SdJwtVc(_) => (String::new(), None),
143            AnyCredential::Bbs(_) => (String::new(), None),
144            AnyCredential::AnonCreds(_) => (String::new(), None),
145        };
146
147        CredentialMetadata {
148            id: self.id.clone(),
149            format: self.credential.credential_format(),
150            issuer: self.credential.issuer_id(),
151            issuance_date,
152            expiration_date,
153        }
154    }
155
156    /// Build a CredentialSummary for listing.
157    pub fn summary(&self) -> CredentialSummary {
158        CredentialSummary {
159            metadata: self.metadata(),
160            label: self.credential.label(),
161        }
162    }
163}
164
165/// Filter for querying stored credentials.
166#[derive(Debug, Clone, Default, Serialize, Deserialize)]
167pub struct CredentialFilter {
168    /// Filter by credential format.
169    pub format: Option<CredentialFormat>,
170    /// Filter by issuer.
171    pub issuer: Option<String>,
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177    use baseid_vc::credential::Issuer;
178    use baseid_vc::VerifiableCredential;
179
180    fn sample_vc() -> VerifiableCredential {
181        VerifiableCredential {
182            context: vec!["https://www.w3.org/ns/credentials/v2".to_string()],
183            id: Some("urn:uuid:test".to_string()),
184            r#type: vec![
185                "VerifiableCredential".to_string(),
186                "UniversityDegreeCredential".to_string(),
187            ],
188            issuer: Issuer::Uri("did:key:z6Mk...".to_string()),
189            valid_from: Some("2024-01-01T00:00:00Z".to_string()),
190            valid_until: Some("2030-01-01T00:00:00Z".to_string()),
191            credential_subject: serde_json::json!({"id": "did:key:holder"}),
192            credential_status: None,
193            proof: None,
194        }
195    }
196
197    #[test]
198    fn any_credential_format_w3c_vc() {
199        let cred = AnyCredential::W3cVc(sample_vc());
200        assert_eq!(cred.credential_format(), CredentialFormat::W3cVc);
201    }
202
203    #[test]
204    fn any_credential_issuer_id_uri() {
205        let cred = AnyCredential::W3cVc(sample_vc());
206        assert_eq!(cred.issuer_id(), "did:key:z6Mk...");
207    }
208
209    #[test]
210    fn any_credential_issuer_id_object() {
211        let mut vc = sample_vc();
212        vc.issuer = Issuer::Object {
213            id: "did:web:example.com".to_string(),
214            properties: serde_json::Map::new(),
215        };
216        let cred = AnyCredential::W3cVc(vc);
217        assert_eq!(cred.issuer_id(), "did:web:example.com");
218    }
219
220    #[test]
221    fn any_credential_label() {
222        let cred = AnyCredential::W3cVc(sample_vc());
223        assert_eq!(cred.label(), "UniversityDegreeCredential");
224    }
225
226    #[test]
227    fn credential_record_metadata() {
228        let record = CredentialRecord {
229            id: CredentialId("cred-1".to_string()),
230            credential: AnyCredential::W3cVc(sample_vc()),
231            raw: None,
232        };
233        let meta = record.metadata();
234        assert_eq!(meta.id, CredentialId("cred-1".to_string()));
235        assert_eq!(meta.format, CredentialFormat::W3cVc);
236        assert_eq!(meta.issuer, "did:key:z6Mk...");
237        assert_eq!(meta.issuance_date, "2024-01-01T00:00:00Z");
238        assert_eq!(
239            meta.expiration_date.as_deref(),
240            Some("2030-01-01T00:00:00Z")
241        );
242    }
243
244    #[test]
245    fn credential_record_summary() {
246        let record = CredentialRecord {
247            id: CredentialId("cred-1".to_string()),
248            credential: AnyCredential::W3cVc(sample_vc()),
249            raw: None,
250        };
251        let summary = record.summary();
252        assert_eq!(summary.label, "UniversityDegreeCredential");
253        assert_eq!(summary.metadata.format, CredentialFormat::W3cVc);
254    }
255}