Skip to main content

bh_sd_jwt/
sd_jwt.rs

1// Copyright (C) 2020-2026  The Blockhouse Technology Limited (TBTL).
2//
3// This program is free software: you can redistribute it and/or modify it
4// under the terms of the GNU Affero General Public License as published by
5// the Free Software Foundation, either version 3 of the License, or (at your
6// option) any later version.
7//
8// This program is distributed in the hope that it will be useful, but
9// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
10// or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public
11// License for more details.
12//
13// You should have received a copy of the GNU Affero General Public License
14// along with this program.  If not, see <https://www.gnu.org/licenses/>.
15
16//! Implementation of basic `SD-JWT` and `SD-JWT+KB` presentation construction and parsing.
17
18use bherror::Error;
19
20use crate::error::FormatError;
21
22pub(crate) const SD_JWT_DELIMITER: &str = "~";
23
24/// A struct representing an `SD-JWT`.
25///
26/// An `SD-JWT` is composed of the following:
27/// - an Issuer-signed JWT,
28/// - zero or more Disclosures.
29///
30/// Instance of an `SD-JWT` can be parsed from a `&str` containing an `SD-JWT`
31/// in the JWS Compact Serialization format.
32///
33/// Instance of an `SD-JWT` can be turned into `SD-JWT+KB` by adding a Key Binding
34/// JWT (KB-JWT) to the instance.
35#[derive(Debug)]
36pub(crate) struct SdJwt {
37    pub(crate) jwt: String,
38    pub(crate) disclosures: Vec<String>,
39}
40
41/// A struct representing an `SD-JWT+KB`.
42///
43/// An `SD-JWT+KB` is composed of the following:
44/// - an SD-JWT (i.e., an Issuer-signed JWT and zero or more Disclosures), and
45/// - Key Binding JWT.
46///
47/// Instance of an `SD-JWT+KB` can be parsed from a `&str` containing an `SD-JWT+KB`
48/// in the Compact Serialization format.
49#[derive(Debug)]
50pub struct SdJwtKB {
51    pub(crate) sd_jwt: SdJwt,
52    pub(crate) key_binding_jwt: String,
53}
54
55impl SdJwt {
56    pub(crate) fn new(jwt: String, disclosures: Vec<String>) -> Self {
57        Self { jwt, disclosures }
58    }
59}
60
61impl SdJwtKB {
62    /// Create a new instance of an [`SdJwtKB`], from the provided parts.
63    /// The provided key binding string should not be a empty.
64    ///
65    /// # Note
66    /// The function only check if key binding is not empty. No other checks
67    /// are carried out on any of the provided parts, e.g. there is not a
68    /// check on the `jwt` signature.
69    pub(crate) fn new(
70        sd_jwt: SdJwt,
71        key_binding_jwt: String,
72    ) -> Result<Self, bherror::Error<FormatError>> {
73        if key_binding_jwt.is_empty() {
74            return Err(Error::root(FormatError::InvalidSdJwtFormat));
75        }
76        Ok(Self {
77            sd_jwt,
78            key_binding_jwt,
79        })
80    }
81}
82
83impl std::str::FromStr for SdJwt {
84    type Err = bherror::Error<FormatError>;
85
86    /// Create a new instance of an [`SdJwt`], from the provided string in the
87    /// JWS Compact Serialization format.
88    ///
89    /// As specified in the [draft v13], the compact format is composed of
90    /// the Issuer-signed `JWT`, a `~` (tilde character), zero or more
91    /// Disclosures each followed by a `~`. The provided string is expected
92    /// to end with `~` character.
93    ///
94    /// # Note
95    /// No checks are carried out on any of the provided parts, e.g. there is
96    /// not a check on the `jwt` signature.
97    ///
98    /// # Examples
99    ///
100    /// An `SD-JWT` without Disclosures:\
101    /// `<Issuer-signed JWT>~`.
102    ///
103    /// An `SD-JWT` with Disclosures:\
104    /// `<Issuer-signed JWT>~<Disclosure 1>~<Disclosure N>~`.
105    ///
106    /// [draft v13]: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-selective-disclosure-jwt-13#name-sd-jwt-and-sd-jwtkb-data-fo
107    fn from_str(value: &str) -> Result<Self, Self::Err> {
108        // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-selective-disclosure-jwt-13#section-4-8
109        if !value.ends_with('~') {
110            return Err(Error::root(FormatError::InvalidSdJwtFormat));
111        }
112        let sd_jwt_parts: Vec<&str> = value.split(SD_JWT_DELIMITER).collect();
113
114        // NOTE: removes the last element because it is a empty string which split
115        //       function collects after the final SD_JWT_DELIMITER '~'
116        debug_assert!(sd_jwt_parts.last().unwrap().is_empty());
117        sd_jwt_from_parts(&sd_jwt_parts[0..sd_jwt_parts.len() - 1])
118    }
119}
120
121impl std::str::FromStr for SdJwtKB {
122    type Err = bherror::Error<FormatError>;
123
124    /// Create a new instance of an [`SdJwtKB`], from the provided string in the
125    /// JWS Compact Serialization format.
126    ///
127    /// As specified in the [draft v13], the compact format is composed of
128    /// the Issuer-signed `JWT`, a `~` (tilde character), zero or more
129    /// Disclosures each followed by a `~`, and lastly a Key Binding JWT (`KB-JWT`).
130    ///
131    /// # Note
132    /// No checks are carried out on any of the provided parts, e.g. there is
133    /// not a check on the `jwt` signature.
134    ///
135    /// # Examples
136    ///
137    /// An `SD-JWT+KB` without Disclosures:\
138    /// `<Issuer-signed JWT>~<KB-JWT>`.
139    ///
140    /// An `SD-JWT+KB` with Disclosures:\
141    /// `<Issuer-signed JWT>~<Disclosure 1>~<Disclosure N>~<KB-JWT>`.
142    ///
143    /// [draft v13]: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-selective-disclosure-jwt-13#name-sd-jwt-and-sd-jwtkb-data-fo
144    fn from_str(value: &str) -> Result<Self, Self::Err> {
145        let sd_jwt_parts: Vec<&str> = value.split(SD_JWT_DELIMITER).collect();
146
147        let parts_len = sd_jwt_parts.len();
148        let sd_jwt = sd_jwt_from_parts(&sd_jwt_parts[0..parts_len - 1])?;
149        let key_binding_jwt = sd_jwt_parts[parts_len - 1];
150
151        Self::new(sd_jwt, key_binding_jwt.to_owned())
152    }
153}
154
155impl std::fmt::Display for SdJwt {
156    /// Serialize the `SD-JWT` in the JWS Compact Serialization format.
157    ///
158    /// As specified in the [draft v13], the JWS Compact Serialization format is
159    /// composed of the Issuer-signed `JWT`, a `~` (tilde character) and zero or
160    /// more Disclosures each followed by a `~`. The last separating tilde
161    /// character must not be omitted.
162    ///
163    /// # Examples
164    ///
165    /// An `SD-JWT` without Disclosures:\
166    /// `<Issuer-signed JWT>~`.
167    ///
168    /// An `SD-JWT` with Disclosures:\
169    /// `<Issuer-signed JWT>~<Disclosure 1>~<Disclosure N>~`.
170    ///
171    /// [draft v13]: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-selective-disclosure-jwt-13#name-sd-jwt-and-sd-jwtkb-data-fo
172    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
173        write!(f, "{}{}", self.jwt, SD_JWT_DELIMITER)?;
174
175        for disclosure in &self.disclosures {
176            write!(f, "{}{}", disclosure, SD_JWT_DELIMITER)?;
177        }
178
179        Ok(())
180    }
181}
182
183impl std::fmt::Display for SdJwtKB {
184    /// Serialize the `SD-JWT+KB` in the JWS Compact serialization format.
185    ///
186    /// As specified in the [draft v13], the compact format is composed of the
187    /// Issuer-signed `JWT`, a `~` (tilde character), zero or more Disclosures
188    /// each followed by a `~`, and lastly a Key Binding JWT (`KB-JWT`).
189    ///
190    /// # Examples
191    ///
192    /// An `SD-JWT+KB` without Disclosures:\
193    /// `<Issuer-signed JWT>~<KB-JWT>`.
194    ///
195    /// An `SD-JWT+KB` with Disclosures:\
196    /// `<Issuer-signed JWT>~<Disclosure 1>~<Disclosure N>~<KB-JWT>`.
197    ///
198    /// [draft v13]: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-selective-disclosure-jwt-13#name-sd-jwt-and-sd-jwtkb-data-fo
199    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
200        write!(f, "{}{}", self.sd_jwt, self.key_binding_jwt)?;
201        Ok(())
202    }
203}
204
205fn sd_jwt_from_parts(sd_jwt_parts: &[&str]) -> Result<SdJwt, bherror::Error<FormatError>> {
206    let sd_jwt_parts = sd_jwt_parts.split_first();
207    let Some((jwt, disclosures)) = sd_jwt_parts else {
208        return Err(Error::root(FormatError::InvalidSdJwtFormat));
209    };
210
211    let disclosures: Vec<String> = disclosures.iter().map(|&s| s.to_owned()).collect();
212
213    Ok(SdJwt::new(jwt.to_string(), disclosures))
214}
215
216#[cfg(test)]
217mod test {
218    use super::*;
219
220    const JWT: &str = "\
221eyJhbGciOiAiRVMyNTYiLCAidHlwIjogImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOiBb\
222IkNyUWU3UzVrcUJBSHQtbk1ZWGdjNmJkdDJTSDVhVFkxc1VfTS1QZ2tqUEkiLCAiSnpZ\
223akg0c3ZsaUgwUjNQeUVNZmVadTZKdDY5dTVxZWhabzdGN0VQWWxTRSIsICJQb3JGYnBL\
224dVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwgIlRHZjRvTGJnd2Q1\
225SlFhSHlLVlFaVTlVZEdFMHc1cnREc3JaemZVYW9tTG8iLCAiWFFfM2tQS3QxWHlYN0tB\
226TmtxVlI2eVoyVmE1TnJQSXZQWWJ5TXZSS0JNTSIsICJYekZyendzY002R242Q0pEYzZ2\
227Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwgImdiT3NJNEVkcTJ4Mkt3LXc1d1BFemFr\
228b2I5aFYxY1JEMEFUTjNvUUw5Sk0iLCAianN1OXlWdWx3UVFsaEZsTV8zSmx6TWFTRnpn\
229bGhRRzBEcGZheVF3TFVLNCJdLCAiaXNzIjogImh0dHBzOi8vaXNzdWVyLmV4YW1wbGUu\
230Y29tIiwgImlhdCI6IDE2ODMwMDAwMDAsICJleHAiOiAxODgzMDAwMDAwLCAic3ViIjog\
231InVzZXJfNDIiLCAibmF0aW9uYWxpdGllcyI6IFt7Ii4uLiI6ICJwRm5kamtaX1ZDem15\
232VGE2VWpsWm8zZGgta284YUlLUWM5RGxHemhhVllvIn0sIHsiLi4uIjogIjdDZjZKa1B1\
233ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sICJfc2RfYWxnIjog\
234InNoYS0yNTYiLCAiY25mIjogeyJqd2siOiB7Imt0eSI6ICJFQyIsICJjcnYiOiAiUC0y\
235NTYiLCAieCI6ICJUQ0FFUjE5WnZ1M09IRjRqNFc0dmZTVm9ISVAxSUxpbERsczd2Q2VH\
236ZW1jIiwgInkiOiAiWnhqaVdXYlpNUUdIVldLVlE0aGJTSWlyc1ZmdWVjQ0U2dDRqVDlG\
237MkhaUSJ9fX0.7oEYwv1H4rBa54xAhDH19DEIy-RRSTdwyJvhbjOKVFyQeM0-gcgpwCq-\
238yFCbWj9THEjD9M4yYkAeaWXfuvBS-Q";
239    const DISCLOSURE_1: &str = "WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd";
240    const DISCLOSURE_2: &str = "WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0";
241    const KEY_BINDING_JWT: &str = "\
242eyJhbGciOiAiRVMyNTYiLCAidHlwIjogImtiK2p3dCJ9.eyJub25jZSI6ICIxMjM0NTY\
2433ODkwIiwgImF1ZCI6ICJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwgImlhdCI\
2446IDE3MDIzMTYwMTUsICJzZF9oYXNoIjogIm5ZY09YeVA0M3Y5c3pLcnluX2tfNEdrUnJ\
245fajNTVEhoTlNTLWkxRHVhdW8ifQ.12Qymun2geGbkYOwiV-DUVfS-zBBKqNe83yNbxM4\
2465J93bno-oM7mph3L1-rPa4lFKQ04wB-T9rU3uAZnBAan5g";
247
248    #[test]
249    fn test_from_str_without_disclosures_without_kb_jwt() {
250        let sd_jwt_presentation = format!("{JWT}~");
251
252        let sd_jwt: SdJwt = sd_jwt_presentation.parse().unwrap();
253
254        assert!(sd_jwt.disclosures.is_empty());
255        assert_eq!(sd_jwt.jwt, JWT);
256
257        // should not parse serialized SdJwt as a SdJwtKB
258        let error: Result<SdJwtKB, Error<FormatError>> = sd_jwt_presentation.parse();
259        assert_eq!(error.unwrap_err().error, FormatError::InvalidSdJwtFormat);
260    }
261
262    #[test]
263    fn test_from_str_without_disclosures_with_kb_jwt() {
264        let sd_jwt_kb_presentation = format!("{JWT}~{KEY_BINDING_JWT}");
265
266        let sd_jwt_kb: SdJwtKB = sd_jwt_kb_presentation.parse().unwrap();
267
268        assert!(sd_jwt_kb.sd_jwt.disclosures.is_empty());
269
270        assert_eq!(sd_jwt_kb.sd_jwt.jwt, JWT);
271        assert_eq!(sd_jwt_kb.key_binding_jwt, KEY_BINDING_JWT);
272
273        // should not parse serialized SdJwtKB as a SdJwt
274        let error: Result<SdJwt, Error<FormatError>> = sd_jwt_kb_presentation.parse();
275        assert_eq!(error.unwrap_err().error, FormatError::InvalidSdJwtFormat);
276    }
277
278    #[test]
279    fn test_from_str_with_disclosures_without_kb_jwt() {
280        let sd_jwt_presentation = format!("{JWT}~{DISCLOSURE_1}~{DISCLOSURE_2}~");
281
282        let sd_jwt: SdJwt = sd_jwt_presentation.parse().unwrap();
283
284        assert_eq!(sd_jwt.disclosures.len(), 2);
285
286        assert_eq!(sd_jwt.jwt, JWT);
287        assert_eq!(sd_jwt.disclosures, &[DISCLOSURE_1, DISCLOSURE_2]);
288
289        // should not parse serialized SdJwt as a SdJwtKB
290        let error: Result<SdJwtKB, Error<FormatError>> = sd_jwt_presentation.parse();
291        assert_eq!(error.unwrap_err().error, FormatError::InvalidSdJwtFormat);
292    }
293
294    #[test]
295    fn test_from_str_with_disclosures_with_kb_jwt() {
296        let sd_jwt_kb_presentation =
297            format!("{JWT}~{DISCLOSURE_1}~{DISCLOSURE_2}~{KEY_BINDING_JWT}");
298
299        let sd_jwt_kb: SdJwtKB = sd_jwt_kb_presentation.parse().unwrap();
300
301        assert_eq!(sd_jwt_kb.sd_jwt.disclosures.len(), 2);
302
303        assert_eq!(sd_jwt_kb.sd_jwt.jwt, JWT);
304        assert_eq!(sd_jwt_kb.sd_jwt.disclosures, &[DISCLOSURE_1, DISCLOSURE_2]);
305        assert_eq!(sd_jwt_kb.key_binding_jwt, KEY_BINDING_JWT);
306
307        // should not parse serialized SdJwtKB as a SdJwt
308        let error: Result<SdJwt, Error<FormatError>> = sd_jwt_kb_presentation.parse();
309        assert_eq!(error.unwrap_err().error, FormatError::InvalidSdJwtFormat);
310    }
311
312    #[test]
313    fn test_from_str_invalid_format() {
314        let result = JWT.parse::<SdJwtKB>();
315        assert!(result.is_err());
316        let result = JWT.parse::<SdJwt>();
317        assert!(result.is_err());
318    }
319
320    #[test]
321    fn test_from_str_sd_jwt_kb_empty_key_binding() {
322        let sd_jwt_kb_with_empty_key_binding_presentation =
323            format!("{JWT}~{DISCLOSURE_1}~{DISCLOSURE_2}~");
324
325        let error: Result<SdJwtKB, Error<FormatError>> =
326            sd_jwt_kb_with_empty_key_binding_presentation.parse();
327
328        assert_eq!(error.unwrap_err().error, FormatError::InvalidSdJwtFormat);
329    }
330
331    #[test]
332    fn test_from_str_empty() {
333        let result = "".parse::<SdJwtKB>();
334        assert!(result.is_err());
335        let result = "".parse::<SdJwt>();
336        assert!(result.is_err());
337    }
338
339    #[test]
340    fn test_display_without_disclosures_without_kb_jwt() {
341        let sd_jwt = SdJwt::new(JWT.to_owned(), Vec::new());
342
343        let expected_presentation = format!("{JWT}~");
344
345        assert_eq!(sd_jwt.to_string(), expected_presentation);
346    }
347
348    #[test]
349    fn test_display_without_disclosures_with_kb_jwt() {
350        let sd_jwt = SdJwt {
351            jwt: JWT.to_owned(),
352            disclosures: Vec::new(),
353        };
354        let sd_jwt_kb = SdJwtKB::new(sd_jwt, KEY_BINDING_JWT.to_owned());
355
356        let expected_presentation = format!("{JWT}~{KEY_BINDING_JWT}");
357
358        assert_eq!(sd_jwt_kb.unwrap().to_string(), expected_presentation);
359    }
360
361    #[test]
362    fn test_display_with_disclosures_without_kb_jwt() {
363        let sd_jwt = SdJwt::new(
364            JWT.to_owned(),
365            vec![DISCLOSURE_1.to_owned(), DISCLOSURE_2.to_owned()],
366        );
367
368        let expected_presentation = format!("{JWT}~{DISCLOSURE_1}~{DISCLOSURE_2}~");
369
370        assert_eq!(sd_jwt.to_string(), expected_presentation);
371    }
372
373    #[test]
374    fn test_display_with_disclosures_with_kb_jwt() {
375        let sd_jwt = SdJwt {
376            jwt: JWT.to_owned(),
377            disclosures: vec![DISCLOSURE_1.to_owned(), DISCLOSURE_2.to_owned()],
378        };
379        let sd_jwt_kb = SdJwtKB::new(sd_jwt, KEY_BINDING_JWT.to_owned());
380
381        let expected_presentation =
382            format!("{JWT}~{DISCLOSURE_1}~{DISCLOSURE_2}~{KEY_BINDING_JWT}");
383
384        assert_eq!(sd_jwt_kb.unwrap().to_string(), expected_presentation);
385    }
386}