Skip to main content

qualia_core_db/identity/credentials/
codecs.rs

1//! Credential transport codecs (from `0.0.19-g3-cbor-ld`) — encode/decode a
2//! [`Credential`](super::Credential) to alternative carriers. Uses the unified
3//! `Credential` type from the parent module (the branch's duplicate was dropped).
4
5use super::Credential;
6
7#[derive(Debug)]
8pub enum CodecError {
9    SerializationError(String),
10}
11
12/// Encode/decode a Verifiable Credential to/from a transport representation.
13pub trait CredentialCodec {
14    fn encode(&self, credential: &Credential) -> Result<Vec<u8>, CodecError>;
15    fn decode(&self, payload: &[u8]) -> Result<Credential, CodecError>;
16}
17
18/// OpenBadges v3 carrier: adds the OB context + type, then JSON-encodes.
19pub struct OpenBadgeCodec;
20
21const OB_CONTEXT: &str = "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json";
22
23impl CredentialCodec for OpenBadgeCodec {
24    fn encode(&self, credential: &Credential) -> Result<Vec<u8>, CodecError> {
25        let mut c = credential.clone();
26        if !c.context.iter().any(|s| s == OB_CONTEXT) {
27            c.context.push(OB_CONTEXT.to_string());
28        }
29        if !c.types.iter().any(|s| s == "OpenBadgeCredential") {
30            c.types.push("OpenBadgeCredential".to_string());
31        }
32        serde_json::to_vec(&c)
33            .map_err(|e| CodecError::SerializationError(format!("OpenBadge encoding failed: {e}")))
34    }
35
36    fn decode(&self, payload: &[u8]) -> Result<Credential, CodecError> {
37        let credential: Credential = serde_json::from_slice(payload).map_err(|e| {
38            CodecError::SerializationError(format!("OpenBadge decoding failed: {e}"))
39        })?;
40        if !credential.types.iter().any(|s| s == "OpenBadgeCredential") {
41            return Err(CodecError::SerializationError(
42                "Not a valid OpenBadgeCredential".to_string(),
43            ));
44        }
45        Ok(credential)
46    }
47}
48
49/// PDF carrier: embeds the credential JSON as a `/Metadata` stream in a minimal PDF.
50pub struct PdfCodec;
51
52impl CredentialCodec for PdfCodec {
53    fn encode(&self, credential: &Credential) -> Result<Vec<u8>, CodecError> {
54        let json = serde_json::to_string(credential)
55            .map_err(|e| CodecError::SerializationError(format!("PDF encoding failed: {e}")))?;
56
57        let mut pdf = Vec::new();
58        pdf.extend_from_slice(b"%PDF-1.7\n");
59        pdf.extend_from_slice(b"%\xE2\xE3\xCF\xD3\n");
60        pdf.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
61        pdf.extend_from_slice(b"2 0 obj\n<< /Type /Pages /Kids [] /Count 0 >>\nendobj\n");
62        pdf.extend_from_slice(b"3 0 obj\n");
63        pdf.extend_from_slice(
64            format!(
65                "<< /Type /Metadata /Subtype /XML /Length {} >>\n",
66                json.len()
67            )
68            .as_bytes(),
69        );
70        pdf.extend_from_slice(b"stream\n");
71        pdf.extend_from_slice(json.as_bytes());
72        pdf.extend_from_slice(b"\nendstream\nendobj\n");
73        pdf.extend_from_slice(b"xref\n0 4\n0000000000 65535 f \n0000000015 00000 n \n0000000066 00000 n \n0000000125 00000 n \n");
74        pdf.extend_from_slice(b"trailer\n<< /Size 4 /Root 1 0 R >>\nstartxref\n250\n%%EOF\n");
75        Ok(pdf)
76    }
77
78    fn decode(&self, payload: &[u8]) -> Result<Credential, CodecError> {
79        let s = String::from_utf8_lossy(payload);
80        if let Some(start) = s.find("stream\n{") {
81            let json_start = start + 7;
82            if let Some(end) = s[json_start..].find("\nendstream") {
83                return serde_json::from_str(&s[json_start..json_start + end]).map_err(|e| {
84                    CodecError::SerializationError(format!("PDF decoding failed: {e}"))
85                });
86            }
87        }
88        Err(CodecError::SerializationError(
89            "Could not locate VC in PDF payload".to_string(),
90        ))
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97    use std::collections::HashMap;
98
99    fn test_credential() -> Credential {
100        let mut subject = HashMap::new();
101        subject.insert("id".to_string(), "did:example:123".to_string());
102        subject.insert("achievement".to_string(), "Course Completion".to_string());
103        Credential {
104            context: vec!["https://www.w3.org/2018/credentials/v1".to_string()],
105            id: "http://example.edu/credentials/3732".to_string(),
106            types: vec!["VerifiableCredential".to_string()],
107            issuer: "https://example.edu/issuers/565049".to_string(),
108            issuance_date: "2010-01-01T19:23:24Z".to_string(),
109            credential_subject: subject,
110            proof: None,
111        }
112    }
113
114    #[test]
115    fn openbadge_roundtrip() {
116        let codec = OpenBadgeCodec;
117        let cred = test_credential();
118        let encoded = codec.encode(&cred).expect("encode");
119        let decoded = codec.decode(&encoded).expect("decode");
120        assert!(decoded.types.iter().any(|s| s == "OpenBadgeCredential"));
121        assert!(decoded.context.iter().any(|s| s == OB_CONTEXT));
122        assert_eq!(decoded.id, cred.id);
123    }
124
125    #[test]
126    fn pdf_roundtrip() {
127        let codec = PdfCodec;
128        let cred = test_credential();
129        let encoded = codec.encode(&cred).expect("encode");
130        assert!(encoded.starts_with(b"%PDF-1.7"));
131        assert!(encoded.ends_with(b"%%EOF\n"));
132        let decoded = codec.decode(&encoded).expect("decode");
133        assert_eq!(decoded.id, cred.id);
134        assert_eq!(decoded.issuer, cred.issuer);
135    }
136}