Skip to main content

qualia_core_db/identity/credentials/
mod.rs

1//! W3C Verifiable Credentials — issue / hold / present / verify, signed with real
2//! ML-DSA-65 (FIPS-204) via [`crate::crypto::fiduciary_crypto`].
3//!
4//! Consolidated from two stranded worktree branches (`0.0.19-g2-vc` = the runtime,
5//! `0.0.19-g3-cbor-ld` = the [`codecs`]) into one module with a **single** `Credential`
6//! type — the duplicate `Credential` each branch had defined is unified here.
7//!
8//! Verification is **fail-closed**: a missing proof, a tampered credential, or a failed
9//! signature check all return an error, never a silent `false`/`true`.
10
11use std::collections::HashMap;
12
13use serde::{Deserialize, Serialize};
14
15use crate::crypto::fiduciary_crypto::{FiduciaryCrypto, MlDsaSignature};
16
17pub mod codecs;
18
19/// Error types for Verifiable Credentials.
20#[derive(Debug, Clone, PartialEq)]
21pub enum VcError {
22    CryptoError(String),
23    SerializationError(String),
24    MissingProof,
25    VerificationFailed,
26    NotImplemented,
27}
28
29/// W3C Verifiable Credential Data Model.
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct Credential {
32    #[serde(rename = "@context")]
33    pub context: Vec<String>,
34    pub id: String,
35    #[serde(rename = "type")]
36    pub types: Vec<String>,
37    pub issuer: String,
38    #[serde(rename = "issuanceDate")]
39    pub issuance_date: String,
40    #[serde(rename = "credentialSubject")]
41    pub credential_subject: HashMap<String, String>,
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub proof: Option<Proof>,
44}
45
46/// W3C Verifiable Presentation Data Model.
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct Presentation {
49    #[serde(rename = "@context")]
50    pub context: Vec<String>,
51    #[serde(rename = "type")]
52    pub types: Vec<String>,
53    #[serde(rename = "verifiableCredential")]
54    pub verifiable_credential: Vec<Credential>,
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub proof: Option<Proof>,
57}
58
59/// Proof object (Data Integrity Proof) carrying the ML-DSA signature.
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct Proof {
62    #[serde(rename = "type")]
63    pub proof_type: String,
64    pub created: String,
65    #[serde(rename = "verificationMethod")]
66    pub verification_method: String,
67    #[serde(rename = "proofPurpose")]
68    pub proof_purpose: String,
69    #[serde(rename = "proofValue")]
70    pub proof_value: MlDsaSignature,
71}
72
73/// Selective-disclosure interface (BBS-style); not yet implemented.
74pub trait SelectiveDisclosure {
75    fn generate_selective_presentation(
76        &self,
77        credential: &Credential,
78        disclosed_fields: &[String],
79    ) -> Result<Presentation, VcError>;
80    fn verify_selective_presentation(&self, presentation: &Presentation) -> Result<bool, VcError>;
81}
82
83/// Zero-knowledge predicate-disclosure interface; not yet implemented.
84pub trait ZkDisclosure {
85    fn generate_zk_proof(
86        &self,
87        credential: &Credential,
88        predicates: &[String],
89    ) -> Result<Presentation, VcError>;
90    fn verify_zk_proof(&self, presentation: &Presentation) -> Result<bool, VcError>;
91}
92
93/// Credential status (revocation/suspension) interface.
94pub trait CredentialStatus {
95    fn check_status(&self, credential_id: &str) -> Result<StatusResult, VcError>;
96}
97
98#[derive(Debug, Clone, PartialEq)]
99pub enum StatusResult {
100    Valid,
101    Revoked,
102    Suspended,
103}
104
105/// Issues, holds, presents, and verifies Verifiable Credentials with ML-DSA-65 proofs.
106pub struct VcRuntime {
107    crypto: FiduciaryCrypto,
108}
109
110impl VcRuntime {
111    pub fn new(crypto: FiduciaryCrypto) -> Self {
112        Self { crypto }
113    }
114
115    /// Issue a Verifiable Credential: sign the proof-less credential and attach the proof.
116    pub fn issue(
117        &self,
118        mut credential: Credential,
119        key_id: Option<&str>,
120    ) -> Result<Credential, VcError> {
121        credential.proof = None;
122
123        let serialized = serde_json::to_vec(&credential)
124            .map_err(|e| VcError::SerializationError(e.to_string()))?;
125
126        let signature = self
127            .crypto
128            .sign(
129                &serialized,
130                key_id,
131                "vc-dm".to_string(),
132                "assertionMethod".to_string(),
133            )
134            .map_err(|e| VcError::CryptoError(format!("{:?}", e)))?;
135
136        credential.proof = Some(Proof {
137            proof_type: "MlDsaSignature2024".to_string(),
138            created: "2026-06-24T00:00:00Z".to_string(),
139            verification_method: key_id.unwrap_or("default").to_string(),
140            proof_purpose: "assertionMethod".to_string(),
141            proof_value: signature,
142        });
143        Ok(credential)
144    }
145
146    /// Hold a credential (secure storage is the caller's responsibility for now).
147    pub fn hold(&self, _credential: &Credential) -> Result<(), VcError> {
148        Ok(())
149    }
150
151    /// Present one or more credentials inside a signed Verifiable Presentation.
152    pub fn present(
153        &self,
154        credentials: Vec<Credential>,
155        key_id: Option<&str>,
156    ) -> Result<Presentation, VcError> {
157        let mut presentation = Presentation {
158            context: vec!["https://www.w3.org/2018/credentials/v1".to_string()],
159            types: vec!["VerifiablePresentation".to_string()],
160            verifiable_credential: credentials,
161            proof: None,
162        };
163
164        let serialized = serde_json::to_vec(&presentation)
165            .map_err(|e| VcError::SerializationError(e.to_string()))?;
166
167        let signature = self
168            .crypto
169            .sign(
170                &serialized,
171                key_id,
172                "vc-dm".to_string(),
173                "authentication".to_string(),
174            )
175            .map_err(|e| VcError::CryptoError(format!("{:?}", e)))?;
176
177        presentation.proof = Some(Proof {
178            proof_type: "MlDsaSignature2024".to_string(),
179            created: "2026-06-24T00:00:00Z".to_string(),
180            verification_method: key_id.unwrap_or("default").to_string(),
181            proof_purpose: "authentication".to_string(),
182            proof_value: signature,
183        });
184        Ok(presentation)
185    }
186
187    /// Verify a credential's proof. **Fail-closed**: missing proof or a failed check errors.
188    pub fn verify_credential(&self, credential: &Credential) -> Result<bool, VcError> {
189        let proof = credential.proof.as_ref().ok_or(VcError::MissingProof)?;
190
191        let mut bare = credential.clone();
192        bare.proof = None;
193        let serialized =
194            serde_json::to_vec(&bare).map_err(|e| VcError::SerializationError(e.to_string()))?;
195
196        let ok = self
197            .crypto
198            .verify(
199                &serialized,
200                &proof.proof_value,
201                Some(&proof.verification_method),
202                "vc-dm".to_string(),
203                proof.proof_purpose.clone(),
204            )
205            .map_err(|e| VcError::CryptoError(format!("{:?}", e)))?;
206
207        if !ok {
208            return Err(VcError::VerificationFailed);
209        }
210        Ok(true)
211    }
212
213    /// Verify a presentation's proof and every credential it carries (all fail-closed).
214    pub fn verify_presentation(&self, presentation: &Presentation) -> Result<bool, VcError> {
215        let proof = presentation.proof.as_ref().ok_or(VcError::MissingProof)?;
216
217        let mut bare = presentation.clone();
218        bare.proof = None;
219        let serialized =
220            serde_json::to_vec(&bare).map_err(|e| VcError::SerializationError(e.to_string()))?;
221
222        let ok = self
223            .crypto
224            .verify(
225                &serialized,
226                &proof.proof_value,
227                Some(&proof.verification_method),
228                "vc-dm".to_string(),
229                proof.proof_purpose.clone(),
230            )
231            .map_err(|e| VcError::CryptoError(format!("{:?}", e)))?;
232
233        if !ok {
234            return Err(VcError::VerificationFailed);
235        }
236
237        for cred in &presentation.verifiable_credential {
238            if !self.verify_credential(cred)? {
239                return Err(VcError::VerificationFailed);
240            }
241        }
242        Ok(true)
243    }
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249
250    fn test_credential() -> Credential {
251        let mut subject = HashMap::new();
252        subject.insert(
253            "id".to_string(),
254            "did:example:ebfeb1f712ebc6f1c276e12ec21".to_string(),
255        );
256        subject.insert(
257            "degree".to_string(),
258            "Bachelor of Science and Arts".to_string(),
259        );
260        Credential {
261            context: vec!["https://www.w3.org/2018/credentials/v1".to_string()],
262            id: "http://example.edu/credentials/3732".to_string(),
263            types: vec![
264                "VerifiableCredential".to_string(),
265                "UniversityDegreeCredential".to_string(),
266            ],
267            issuer: "https://example.edu/issuers/565049".to_string(),
268            issuance_date: "2010-01-01T19:23:24Z".to_string(),
269            credential_subject: subject,
270            proof: None,
271        }
272    }
273
274    #[test]
275    fn issue_verify_roundtrip() {
276        let mut crypto = FiduciaryCrypto::new();
277        crypto.generate_key("default".to_string()).unwrap();
278        let runtime = VcRuntime::new(crypto);
279
280        let issued = runtime
281            .issue(test_credential(), Some("default"))
282            .expect("issue");
283        assert!(issued.proof.is_some());
284        assert_eq!(runtime.verify_credential(&issued), Ok(true));
285    }
286
287    #[test]
288    fn tampered_credential_fails_closed() {
289        let mut crypto = FiduciaryCrypto::new();
290        crypto.generate_key("default".to_string()).unwrap();
291        let runtime = VcRuntime::new(crypto);
292
293        let mut issued = runtime.issue(test_credential(), Some("default")).unwrap();
294        issued.issuer = "https://hacker.com".to_string();
295        assert!(matches!(
296            runtime.verify_credential(&issued),
297            Err(VcError::VerificationFailed) | Err(VcError::CryptoError(_))
298        ));
299    }
300
301    #[test]
302    fn unsigned_credential_fails_closed() {
303        let runtime = VcRuntime::new(FiduciaryCrypto::new());
304        assert_eq!(
305            runtime.verify_credential(&test_credential()),
306            Err(VcError::MissingProof)
307        );
308    }
309
310    #[test]
311    fn present_verify_roundtrip() {
312        let mut crypto = FiduciaryCrypto::new();
313        crypto.generate_key("default".to_string()).unwrap();
314        let runtime = VcRuntime::new(crypto);
315
316        let issued = runtime.issue(test_credential(), Some("default")).unwrap();
317        let presentation = runtime.present(vec![issued], Some("default")).unwrap();
318        assert_eq!(runtime.verify_presentation(&presentation), Ok(true));
319    }
320}