baseid_sd_jwt/
lib.rs

1//! # baseid-sd-jwt
2//!
3//! SD-JWT (Selective Disclosure JWT) and SD-JWT-VC implementation.
4//!
5//! Supports:
6//! - SD-JWT creation with selective disclosure of claims
7//! - SD-JWT presentation with disclosure selection
8//! - SD-JWT verification
9//! - SD-JWT-VC profile for verifiable credentials
10//!
11//! Reference: IETF SD-JWT draft
12
13pub mod disclosure;
14pub mod issuer;
15pub mod lifecycle;
16pub mod verifier;
17
18use baseid_core::error::CryptoError;
19use serde::{Deserialize, Serialize};
20
21/// An SD-JWT consisting of the issuer JWT and disclosures.
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct SdJwt {
24    /// The issuer-signed JWT.
25    pub jwt: String,
26    /// Disclosures (base64url-encoded).
27    pub disclosures: Vec<String>,
28    /// Optional key binding JWT.
29    pub key_binding_jwt: Option<String>,
30}
31
32impl SdJwt {
33    /// Serialize to compact format: `jwt~disc1~disc2~[kb_jwt]`
34    ///
35    /// The trailing `~` is always present. If a key binding JWT exists it
36    /// follows the last disclosure separator.
37    pub fn serialize(&self) -> String {
38        let mut parts = vec![self.jwt.clone()];
39        for d in &self.disclosures {
40            parts.push(d.clone());
41        }
42        // Trailing ~ separator (or kb_jwt follows)
43        match &self.key_binding_jwt {
44            Some(kb) => {
45                let mut s = parts.join("~");
46                s.push('~');
47                s.push_str(kb);
48                s
49            }
50            None => {
51                let mut s = parts.join("~");
52                s.push('~');
53                s
54            }
55        }
56    }
57
58    /// Parse an SD-JWT from compact format.
59    pub fn parse(compact: &str) -> baseid_core::Result<Self> {
60        // Split on ~, the first element is the JWT, remaining are disclosures,
61        // and the last empty string or a KB-JWT
62        let parts: Vec<&str> = compact.split('~').collect();
63        if parts.len() < 2 {
64            return Err(CryptoError::VerificationFailed.into());
65        }
66
67        let jwt = parts[0].to_string();
68        if jwt.is_empty() {
69            return Err(CryptoError::VerificationFailed.into());
70        }
71
72        // Last part may be empty (no KB-JWT) or a KB-JWT
73        let last = parts[parts.len() - 1];
74        let (disclosures_slice, key_binding_jwt) = if last.is_empty() {
75            (&parts[1..parts.len() - 1], None)
76        } else {
77            (&parts[1..parts.len() - 1], Some(last.to_string()))
78        };
79
80        let disclosures: Vec<String> = disclosures_slice
81            .iter()
82            .filter(|s| !s.is_empty())
83            .map(|s| s.to_string())
84            .collect();
85
86        Ok(Self {
87            jwt,
88            disclosures,
89            key_binding_jwt,
90        })
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn serialize_parse_roundtrip() {
100        let sd = SdJwt {
101            jwt: "eyJ0eXAiOiJKV1QifQ.eyJpc3MiOiJ0ZXN0In0.sig".to_string(),
102            disclosures: vec!["disc1".to_string(), "disc2".to_string()],
103            key_binding_jwt: None,
104        };
105        let compact = sd.serialize();
106        assert!(compact.ends_with('~'));
107
108        let parsed = SdJwt::parse(&compact).unwrap();
109        assert_eq!(parsed.jwt, sd.jwt);
110        assert_eq!(parsed.disclosures, sd.disclosures);
111        assert!(parsed.key_binding_jwt.is_none());
112    }
113
114    #[test]
115    fn serialize_parse_with_kb_jwt() {
116        let sd = SdJwt {
117            jwt: "jwt_part".to_string(),
118            disclosures: vec!["d1".to_string()],
119            key_binding_jwt: Some("kb_jwt".to_string()),
120        };
121        let compact = sd.serialize();
122        assert!(!compact.ends_with('~'));
123
124        let parsed = SdJwt::parse(&compact).unwrap();
125        assert_eq!(parsed.jwt, "jwt_part");
126        assert_eq!(parsed.disclosures, vec!["d1"]);
127        assert_eq!(parsed.key_binding_jwt, Some("kb_jwt".to_string()));
128    }
129
130    #[test]
131    fn serialize_no_disclosures() {
132        let sd = SdJwt {
133            jwt: "jwt".to_string(),
134            disclosures: vec![],
135            key_binding_jwt: None,
136        };
137        let compact = sd.serialize();
138        assert_eq!(compact, "jwt~");
139
140        let parsed = SdJwt::parse(&compact).unwrap();
141        assert_eq!(parsed.jwt, "jwt");
142        assert!(parsed.disclosures.is_empty());
143    }
144}