sd_jwt_rs/
holder.rs

1// Copyright (c) 2024 DSR Corporation, Denver, Colorado.
2// https://www.dsr-corporation.com
3// SPDX-License-Identifier: Apache-2.0
4
5use crate::{error, SDJWTJson, SDJWTSerializationFormat};
6use error::{Error, Result};
7use jsonwebtoken::{Algorithm, EncodingKey, Header};
8use serde_json::{Map, Value};
9use std::collections::HashMap;
10use std::ops::Add;
11use std::str::FromStr;
12use std::time;
13
14use crate::utils::base64_hash;
15use crate::SDJWTCommon;
16use crate::{
17    COMBINED_SERIALIZATION_FORMAT_SEPARATOR, DEFAULT_SIGNING_ALG, KB_DIGEST_KEY, SD_DIGESTS_KEY,
18    SD_LIST_PREFIX,
19};
20
21pub struct SDJWTHolder {
22    sd_jwt_engine: SDJWTCommon,
23    hs_disclosures: Vec<String>,
24    key_binding_jwt_header: HashMap<String, Value>,
25    key_binding_jwt_payload: HashMap<String, Value>,
26    serialized_key_binding_jwt: String,
27    sd_jwt_payload: Map<String, Value>,
28    serialized_sd_jwt: String,
29    sd_jwt_json: Option<SDJWTJson>,
30}
31
32impl SDJWTHolder {
33    /// Build an instance of holder to create one or more presentations based on SD JWT provided by issuer.
34    ///
35    /// # Arguments
36    /// * `sd_jwt_with_disclosures` - SD JWT with disclosures in the format specified by `serialization_format`
37    /// * `serialization_format` - Serialization format of the SD JWT, see [SDJWTSerializationFormat].
38    ///
39    /// # Returns
40    /// * `SDJWTHolder` - Instance of SDJWTHolder
41    ///
42    /// # Errors
43    /// * `InvalidInput` - If the serialization format is not supported
44    /// * `InvalidState` - If the SD JWT data is not valid
45    /// * `DeserializationError` - If the SD JWT serialization is not valid
46    pub fn new(sd_jwt_with_disclosures: String, serialization_format: SDJWTSerializationFormat) -> Result<Self> {
47        let mut holder = SDJWTHolder {
48            sd_jwt_engine: SDJWTCommon {
49                serialization_format,
50                ..Default::default()
51            },
52            hs_disclosures: Vec::new(),
53            key_binding_jwt_header: HashMap::new(),
54            key_binding_jwt_payload: HashMap::new(),
55            serialized_key_binding_jwt: "".to_string(),
56            sd_jwt_payload: Map::new(),
57            serialized_sd_jwt: "".to_string(),
58            sd_jwt_json: None,
59        };
60
61        holder
62            .sd_jwt_engine
63            .parse_sd_jwt(sd_jwt_with_disclosures.clone())?;
64
65        //TODO Verify signature before accepting the JWT
66        holder.sd_jwt_payload = holder
67            .sd_jwt_engine
68            .unverified_input_sd_jwt_payload
69            .take()
70            .ok_or(Error::InvalidState("Cannot take payload".to_string()))?;
71        holder.serialized_sd_jwt = holder
72            .sd_jwt_engine
73            .unverified_sd_jwt
74            .take()
75            .ok_or(Error::InvalidState("Cannot take jwt".to_string()))?;
76        holder.sd_jwt_json = holder.sd_jwt_engine.unverified_sd_jwt_json.clone();
77
78        holder.sd_jwt_engine.create_hash_mappings()?;
79
80        Ok(holder)
81    }
82
83    /// Create a presentation based on the SD JWT provided by issuer.
84    ///
85    /// # Arguments
86    /// * `claims_to_disclose` - Claims to disclose in the presentation
87    /// * `nonce` - Nonce to be used in the key-binding JWT
88    /// * `aud` - Audience to be used in the key-binding JWT
89    /// * `holder_key` - Key to sign the key-binding JWT
90    /// * `sign_alg` - Signing algorithm to be used in the key-binding JWT
91    ///
92    /// # Returns
93    /// * `String` - Presentation in the format specified by `serialization_format` in the constructor. It can be either compact or json.
94    pub fn create_presentation(
95        &mut self,
96        claims_to_disclose: Map<String, Value>,
97        nonce: Option<String>,
98        aud: Option<String>,
99        holder_key: Option<EncodingKey>,
100        sign_alg: Option<String>,
101    ) -> Result<String> {
102        self.key_binding_jwt_header = Default::default();
103        self.key_binding_jwt_payload = Default::default();
104        self.serialized_key_binding_jwt = Default::default();
105        self.hs_disclosures = self.select_disclosures(&self.sd_jwt_payload, claims_to_disclose)?;
106
107        match (nonce, aud, holder_key) {
108            (Some(nonce), Some(aud), Some(holder_key)) => {
109                self.create_key_binding_jwt(nonce, aud, &holder_key, sign_alg)?
110            }
111            (None, None, None) => {}
112            _ => {
113                return Err(Error::InvalidInput(
114                    "Inconsistency in parameters to determine JWT KB by holder".to_string(),
115                ));
116            }
117        }
118
119        let sd_jwt_presentation = if self.sd_jwt_engine.serialization_format == SDJWTSerializationFormat::Compact {
120            let mut combined: Vec<&str> = Vec::with_capacity(self.hs_disclosures.len() + 2);
121            combined.push(&self.serialized_sd_jwt);
122            combined.extend(self.hs_disclosures.iter().map(|s| s.as_str()));
123            combined.push(&self.serialized_key_binding_jwt);
124            let joined = combined.join(COMBINED_SERIALIZATION_FORMAT_SEPARATOR);
125            joined.to_string()
126        } else {
127            let mut sd_jwt_json = self
128                .sd_jwt_json
129                .take()
130                .ok_or(Error::InvalidState("Cannot take SDJWTJson".to_string()))?;
131            sd_jwt_json.disclosures = self.hs_disclosures.clone();
132            if !self.serialized_key_binding_jwt.is_empty() {
133                sd_jwt_json.kb_jwt = Some(self.serialized_key_binding_jwt.clone());
134            }
135            serde_json::to_string(&sd_jwt_json)
136                .map_err(|e| Error::DeserializationError(e.to_string()))?
137        };
138
139        Ok(sd_jwt_presentation)
140    }
141
142    fn select_disclosures(
143        &self,
144        sd_jwt_claims: &Map<String, Value>,
145        claims_to_disclose: Map<String, Value>,
146    ) -> Result<Vec<String>> {
147        let mut hash_to_disclosure = Vec::new();
148        let default_list = Vec::new();
149        let sd_map: HashMap<&str, (&Value, &str)> = sd_jwt_claims
150            .get(SD_DIGESTS_KEY)
151            .and_then(Value::as_array)
152            .unwrap_or(&default_list)
153            .iter()
154            .filter_map(|digest| {
155                let digest = match digest.as_str() {
156                    Some(digest) => digest,
157                    None => return None,
158                };
159                if let Some(Value::Array(disclosure)) =
160                    self.sd_jwt_engine.hash_to_decoded_disclosure.get(digest)
161                {
162                    let key = match disclosure[1].as_str() {
163                        Some(digest) => digest,
164                        None => return None,
165                    };
166                    return Some((key, (&disclosure[2], digest)));
167                }
168                None
169            })
170            .collect(); //TODO split to 2 maps
171        for (key_to_disclose, value_to_disclose) in claims_to_disclose {
172            match value_to_disclose {
173                Value::Bool(true) | Value::Number(_) | Value::String(_) => {
174                    /* disclose without children */
175                }
176                Value::Array(claims_to_disclose) => {
177                    if let Some(sd_jwt_claims) = sd_jwt_claims
178                        .get(&key_to_disclose)
179                        .and_then(Value::as_array)
180                    {
181                        hash_to_disclosure.append(
182                            &mut self.select_disclosures_from_disclosed_list(
183                                sd_jwt_claims,
184                                &claims_to_disclose,
185                            )?,
186                        )
187                    } else if let Some(sd_jwt_claims) = sd_map
188                        .get(key_to_disclose.as_str())
189                        .and_then(|(sd, _)| sd.as_array())
190                    {
191                        hash_to_disclosure.append(
192                            &mut self.select_disclosures_from_disclosed_list(
193                                sd_jwt_claims,
194                                &claims_to_disclose,
195                            )?,
196                        )
197                    }
198                }
199                Value::Object(claims_to_disclose) if (!claims_to_disclose.is_empty()) => {
200                    let sd_jwt_claims = if let Some(next) = sd_jwt_claims
201                        .get(&key_to_disclose)
202                        .and_then(Value::as_object)
203                    {
204                        next
205                    } else {
206                        sd_map[key_to_disclose.as_str()]
207                            .0
208                            .as_object()
209                            .ok_or(Error::ConversionError("json object".to_string()))?
210                    };
211                    hash_to_disclosure
212                        .append(&mut self.select_disclosures(sd_jwt_claims, claims_to_disclose)?);
213                }
214                Value::Object(_) => { /* disclose without children */ }
215                Value::Bool(false) | Value::Null => {
216                    // skip unrevealed
217                    continue;
218                }
219            }
220            if sd_jwt_claims.contains_key(&key_to_disclose) {
221                continue;
222            } else if let Some((_, digest)) = sd_map.get(key_to_disclose.as_str()) {
223                hash_to_disclosure.push(self.sd_jwt_engine.hash_to_disclosure[*digest].to_owned());
224            } else {
225                return Err(Error::InvalidState(
226                    "Requested claim doesn't exist".to_string(),
227                ));
228            }
229        }
230
231        Ok(hash_to_disclosure)
232    }
233
234    fn select_disclosures_from_disclosed_list(
235        &self,
236        sd_jwt_claims: &[Value],
237        claims_to_disclose: &[Value],
238    ) -> Result<Vec<String>> {
239        let mut hash_to_disclosure: Vec<String> = Vec::new();
240        for (claim_to_disclose, sd_jwt_claims) in claims_to_disclose.iter().zip(sd_jwt_claims) {
241            match (claim_to_disclose, sd_jwt_claims) {
242                (Value::Bool(true), Value::Object(sd_jwt_claims)) => {
243                    if let Some(Value::String(digest)) = sd_jwt_claims.get(SD_LIST_PREFIX) {
244                        hash_to_disclosure
245                            .push(self.sd_jwt_engine.hash_to_disclosure[digest].to_owned());
246                    }
247                }
248                (claim_to_disclose, Value::Object(sd_jwt_claims)) => {
249                    if let Some(Value::String(digest)) = sd_jwt_claims.get(SD_LIST_PREFIX) {
250                        let disclosure = self.sd_jwt_engine.hash_to_decoded_disclosure[digest]
251                            .as_array()
252                            .ok_or(Error::ConversionError("json array".to_string()))?;
253                        match (claim_to_disclose, disclosure.get(1)) {
254                            (
255                                Value::Array(claim_to_disclose),
256                                Some(Value::Array(sd_jwt_claims)),
257                            ) => {
258                                hash_to_disclosure.push(
259                                    self.sd_jwt_engine.hash_to_disclosure[digest].clone()
260                                );
261                                hash_to_disclosure.append(
262                                    &mut self.select_disclosures_from_disclosed_list(
263                                        sd_jwt_claims,
264                                        claim_to_disclose,
265                                    )?,
266                                );
267                            }
268                            (
269                                Value::Object(claim_to_disclose),
270                                Some(Value::Object(sd_jwt_claims)),
271                            ) => {
272                                hash_to_disclosure
273                                    .push(self.sd_jwt_engine.hash_to_disclosure[digest].to_owned());
274                                hash_to_disclosure.append(&mut self.select_disclosures(
275                                    sd_jwt_claims,
276                                    claim_to_disclose.to_owned(),
277                                )?);
278                            }
279                            _ => {}
280                        }
281                    } else if let Some(claim_to_disclose) = claim_to_disclose.as_object() {
282                        hash_to_disclosure.append(
283                            &mut self
284                                .select_disclosures(sd_jwt_claims, claim_to_disclose.to_owned())?,
285                        );
286                    }
287                }
288                (Value::Array(claim_to_disclose), Value::Array(sd_jwt_claims)) => {
289                    hash_to_disclosure.append(&mut self.select_disclosures_from_disclosed_list(
290                        sd_jwt_claims,
291                        claim_to_disclose,
292                    )?);
293                }
294                _ => {}
295            }
296        }
297
298        Ok(hash_to_disclosure)
299    }
300    fn create_key_binding_jwt(
301        &mut self,
302        nonce: String,
303        aud: String,
304        holder_key: &EncodingKey,
305        sign_alg: Option<String>,
306    ) -> Result<()> {
307        let alg = sign_alg.unwrap_or_else(|| DEFAULT_SIGNING_ALG.to_string());
308        // Set key-binding fields
309        self.key_binding_jwt_header
310            .insert("alg".to_string(), alg.clone().into());
311        self.key_binding_jwt_header
312            .insert("typ".to_string(), crate::KB_JWT_TYP_HEADER.into());
313        self.key_binding_jwt_payload
314            .insert("nonce".to_string(), nonce.into());
315        self.key_binding_jwt_payload
316            .insert("aud".to_string(), aud.into());
317        let timestamp = time::SystemTime::now()
318            .duration_since(time::UNIX_EPOCH)
319            .map_err(|e| Error::ConversionError(format!("timestamp: {}", e)))?
320            .as_secs();
321        self.key_binding_jwt_payload
322            .insert("iat".to_string(), timestamp.into());
323        self.set_key_binding_digest_key()?;
324        // Create key-binding jwt
325        let mut header = Header::new(
326            Algorithm::from_str(alg.as_str())
327                .map_err(|e| Error::DeserializationError(e.to_string()))?,
328        );
329        header.typ = Some(crate::KB_JWT_TYP_HEADER.into());
330        self.serialized_key_binding_jwt =
331            jsonwebtoken::encode(&header, &self.key_binding_jwt_payload, holder_key)
332                .map_err(|e| Error::DeserializationError(e.to_string()))?;
333        Ok(())
334    }
335
336    fn set_key_binding_digest_key(&mut self) -> Result<()> {
337        let mut combined: Vec<&str> = Vec::with_capacity(self.hs_disclosures.len() + 1);
338        combined.push(&self.serialized_sd_jwt);
339        combined.extend(self.hs_disclosures.iter().map(|s| s.as_str()));
340        let combined = combined
341            .join(COMBINED_SERIALIZATION_FORMAT_SEPARATOR)
342            .add(COMBINED_SERIALIZATION_FORMAT_SEPARATOR);
343
344        let sd_hash = base64_hash(combined.as_bytes());
345        self.key_binding_jwt_payload
346            .insert(KB_DIGEST_KEY.to_owned(), Value::String(sd_hash));
347
348        Ok(())
349    }
350}
351
352#[cfg(test)]
353mod tests {
354    use crate::issuer::ClaimsForSelectiveDisclosureStrategy;
355    use crate::{SDJWTHolder, SDJWTIssuer, COMBINED_SERIALIZATION_FORMAT_SEPARATOR, SDJWTSerializationFormat};
356    use jsonwebtoken::EncodingKey;
357    use serde_json::{json, Map, Value};
358    use std::collections::HashSet;
359
360    const PRIVATE_ISSUER_PEM: &str = "-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgUr2bNKuBPOrAaxsR\nnbSH6hIhmNTxSGXshDSUD1a1y7ihRANCAARvbx3gzBkyPDz7TQIbjF+ef1IsxUwz\nX1KWpmlVv+421F7+c1sLqGk4HUuoVeN8iOoAcE547pJhUEJyf5Asc6pP\n-----END PRIVATE KEY-----\n";
361
362    #[test]
363    fn create_full_presentation() {
364        let user_claims = json!({
365            "sub": "6c5c0a49-b589-431d-bae7-219122a9ec2c",
366            "iss": "https://example.com/issuer",
367            "iat": "1683000000",
368            "exp": "1883000000",
369            "address": {
370                "street_address": "Schulstr. 12",
371                "locality": "Schulpforta",
372                "region": "Sachsen-Anhalt",
373                "country": "DE"
374            }
375        });
376        let private_issuer_bytes = PRIVATE_ISSUER_PEM.as_bytes();
377        let issuer_key = EncodingKey::from_ec_pem(private_issuer_bytes).unwrap();
378        let sd_jwt = SDJWTIssuer::new(issuer_key, None).issue_sd_jwt(
379            user_claims.clone(),
380            ClaimsForSelectiveDisclosureStrategy::AllLevels,
381            None,
382            false,
383            SDJWTSerializationFormat::Compact,
384        )
385            .unwrap();
386        let presentation = SDJWTHolder::new(
387            sd_jwt.clone(),
388            SDJWTSerializationFormat::Compact,
389        )
390            .unwrap()
391            .create_presentation(
392                user_claims.as_object().unwrap().clone(),
393                None,
394                None,
395                None,
396                None,
397            )
398            .unwrap();
399        assert_eq!(sd_jwt, presentation);
400    }
401    #[test]
402    fn create_presentation_empty_object_as_disclosure_value() {
403        let mut user_claims = json!({
404            "sub": "6c5c0a49-b589-431d-bae7-219122a9ec2c",
405            "iss": "https://example.com/issuer",
406            "iat": 1683000000,
407            "exp": 1883000000,
408            "address": {
409                "street_address": "Schulstr. 12",
410                "locality": "Schulpforta",
411                "region": "Sachsen-Anhalt",
412                "country": "DE"
413            }
414        });
415        let private_issuer_bytes = PRIVATE_ISSUER_PEM.as_bytes();
416        let issuer_key = EncodingKey::from_ec_pem(private_issuer_bytes).unwrap();
417
418        let sd_jwt = SDJWTIssuer::new(issuer_key, None).issue_sd_jwt(
419            user_claims.clone(),
420            ClaimsForSelectiveDisclosureStrategy::AllLevels,
421            None,
422            false,
423            SDJWTSerializationFormat::Compact,
424        )
425            .unwrap();
426        let issued = sd_jwt.clone();
427        user_claims["address"] = Value::Object(Map::new());
428        let presentation =
429            SDJWTHolder::new(sd_jwt, SDJWTSerializationFormat::Compact)
430                .unwrap()
431                .create_presentation(
432                    user_claims.as_object().unwrap().clone(),
433                    None,
434                    None,
435                    None,
436                    None,
437                )
438                .unwrap();
439
440        let mut parts: Vec<&str> = issued
441            .split(COMBINED_SERIALIZATION_FORMAT_SEPARATOR)
442            .collect();
443        parts.remove(5);
444        parts.remove(4);
445        parts.remove(3);
446        parts.remove(2);
447        let expected = parts.join(COMBINED_SERIALIZATION_FORMAT_SEPARATOR);
448        assert_eq!(expected, presentation);
449    }
450
451    #[test]
452    fn create_presentation_for_arrayed_disclosures() {
453        let mut user_claims = json!(
454            {
455              "sub": "6c5c0a49-b589-431d-bae7-219122a9ec2c",
456              "name": "Bois",
457              "addresses": [
458                {
459                "street_address": "Schulstr. 12",
460                "locality": "Schulpforta",
461                "region": "Sachsen-Anhalt",
462                "country": "DE"
463                },
464                {
465                "street_address": "456 Main St",
466                "locality": "Anytown",
467                "region": "NY",
468                "country": "US"
469                }
470              ],
471              "nationalities": [
472                "US",
473                "CA"
474              ]
475            }
476        );
477        let strategy = ClaimsForSelectiveDisclosureStrategy::Custom(vec![
478            "$.name",
479            "$.addresses[1]",
480            "$.addresses[1].country",
481            "$.nationalities[0]",
482        ]);
483
484        let private_issuer_bytes = PRIVATE_ISSUER_PEM.as_bytes();
485        let issuer_key = EncodingKey::from_ec_pem(private_issuer_bytes).unwrap();
486        let sd_jwt = SDJWTIssuer::new(issuer_key, None).issue_sd_jwt(
487            user_claims.clone(),
488            strategy,
489            None,
490            false,
491            SDJWTSerializationFormat::Compact,
492        )
493            .unwrap();
494        // Choose what to reveal
495        user_claims["addresses"] = Value::Array(vec![Value::Bool(true), Value::Bool(false)]);
496        user_claims["nationalities"] = Value::Array(vec![Value::Bool(true), Value::Bool(true)]);
497
498        let issued = sd_jwt.clone();
499        println!("{}", issued);
500        let presentation =
501            SDJWTHolder::new(sd_jwt, SDJWTSerializationFormat::Compact)
502                .unwrap()
503                .create_presentation(
504                    user_claims.as_object().unwrap().clone(),
505                    None,
506                    None,
507                    None,
508                    None,
509                )
510                .unwrap();
511        println!("{}", presentation);
512        let mut issued_parts: HashSet<&str> = issued
513            .split(COMBINED_SERIALIZATION_FORMAT_SEPARATOR)
514            .collect();
515        issued_parts.remove("");
516
517        let mut revealed_parts: HashSet<&str> = presentation
518            .split(COMBINED_SERIALIZATION_FORMAT_SEPARATOR)
519            .collect();
520        revealed_parts.remove("");
521
522        let union: HashSet<_> = issued_parts.intersection(&revealed_parts).collect();
523        assert_eq!(union.len(), 3);
524    }
525
526    #[test]
527    fn create_presentation_for_recursive_disclosures() {
528        // Input data used to create the SD-JWT and presentation fixtures,
529        // can be used to debug in case the test fails:
530
531        // let mut user_claims = json!(
532        //     {
533        //         "foo": ["one", "two"],
534        //         "bar": {
535        //           "red": 1,
536        //           "green": 2
537        //         },
538        //         "qux": [
539        //           ["blue", "yellow"]
540        //         ],
541        //         "baz": [
542        //           ["orange", "purple"],
543        //           ["black", "white"]
544        //         ],
545        //         "animals": {
546        //           "snake": {
547        //             "name": "python",
548        //             "age": 10
549        //           },
550        //           "bird": {
551        //             "name": "eagle",
552        //             "age": 20
553        //           }
554        //         }
555        //       }
556        // );
557        // let strategy = ClaimsForSelectiveDisclosureStrategy::Custom(vec![
558        //     "$.foo[0]",
559        //     "$.foo[1]",
560        //     "$.bar.red",
561        //     "$.bar.green",
562        //     "$.qux[0]",
563        //     "$.qux[0][0]",
564        //     "$.qux[0][1]",
565        //     "$.baz[0]",
566        //     "$.baz[0][0]",
567        //     "$.baz[0][1]",
568        //     "$.baz[1]",
569        //     "$.baz[1][0]",
570        //     "$.baz[1][1]",
571        //     "$.animals.snake",
572        //     "$.animals.snake.name",
573        //     "$.animals.snake.age",
574        //     "$.animals.bird",
575        //     "$.animals.bird.name",
576        //     "$.animals.bird.age",
577        // ]);
578
579        // let private_issuer_bytes = PRIVATE_ISSUER_PEM.as_bytes();
580        // let issuer_key = EncodingKey::from_ec_pem(private_issuer_bytes).unwrap();
581        // let sd_jwt = SDJWTIssuer::new(issuer_key, None).issue_sd_jwt(
582        //     user_claims.clone(),
583        //     strategy,
584        //     None,
585        //     false,
586        //     SDJWTSerializationFormat::Compact,
587        // )
588        //     .unwrap();
589
590        let sd_jwt = String::from("eyJhbGciOiJFUzI1NiJ9.eyJmb28iOlt7Ii4uLiI6Ii1XMWROTk0tNUI3WlpxR3R4MkF6RTA3X0hpRUpOZVJtNGtEQ1VORTVDNFUifSx7Ii4uLiI6ImpuUURqUEFoclY1bjMtRW5PVEZHWTcwMkd0T3FhN3hua3pVM0E4aElSX3cifV0sImJhciI6eyJfc2QiOlsiX25yZUxad2xVYlp1SmtqS1RVdHR5YkhqUTNrY2J4cnZab1dxUmVBbG4tcyIsImhGcjdBRElQbjZvQ3lSckNBN0VtNldLaGk1UjdXMWJjYWFZUFFrelpGMXciXX0sInF1eCI6W3siLi4uIjoieHl6MkRSSDRTSkpjdFFtMDEtSzROVVllMTMzMWh6U3VkTXd3MENDODEyUSJ9XSwiYmF6IjpbeyIuLi4iOiJRMGcyVmYzNnl6TnNvUkdNb0dsODZnZ2QyWGFVTmg5bGN6STFfbmFZYUhnIn0seyIuLi4iOiJZcGpMNTJKd1BfYmFFS21OaHFLazE3TWFrMl9fSWJCNmctY0haSHd6dmwwIn1dLCJhbmltYWxzIjp7Il9zZCI6WyJyQ19LNzlObG95SkFPWXRCOW9ITFlsTVJSS1V4UTNnaTZ0Wld0Zm90TWRjIiwidjUyd3d6bzB5Ymw2U2V1MjZWYklUODh5bHk1LXVMZkdlYTdkWnMxSHBwMCJdfSwiX3NkX2FsZyI6InNoYS0yNTYifQ.piidRp0pHJYmtExCJnLExaaWMTBX50mLwM6gFVYnD72DszyjpKbAoZhyAXT-I4CqqSpiHZg-2w8s26XBraqX6w~WyJCQ2k5UXlsWVVqVEpXWWVfbzRzOWxnIiwgIm9uZSJd~WyJSLXZ0bDBmbWF6N01zR1ZWRFh3T3BnIiwgInR3byJd~WyJXNWlOQ1Z1Qlo4OW9aV2dIUkxzRWJBIiwgInJlZCIsIDFd~WyJTQW5hNUJnaHJxUXJ0amR5SGxiejJBIiwgImdyZWVuIiwgMl0~WyJENVJrNVlIUkdJVXM5enp6OFUtOTVnIiwgImJsdWUiXQ~WyI0Y2tnSjJuWVhhV21jM3pVQ253d3N3IiwgInllbGxvdyJd~WyJ2Ml8tRG5JN0lEZ1loYVMzTG9Kb013IiwgW3siLi4uIjogIkhqMUQtZE1SNXR0YmpLcl9DUENETzRuVGlkTWR1YVNpMnlnYlhtcmR4MGcifSwgeyIuLi4iOiAiUl9Sb253SFY3bzR6Y0o3TV9jcTlobVpLZ2o2RkMtdmNXTko4bzNkeTg2MCJ9XV0~WyJwRHQtcEtfaklUYXhCVENJRFNvUnhBIiwgIm9yYW5nZSJd~WyJ1b3FDS0lpZGJzQmxhczhUaU5Kakh3IiwgInB1cnBsZSJd~WyJJWWFXMzVPNzBoUWg4OGlqWVBUVXZRIiwgW3siLi4uIjogIjlWdnFSbjk1ZUN6QnVkNkhYOG1faVRNMERZSVVxN0ZheFFtanowV1llbUkifSwgeyIuLi4iOiAiTmFOeXozWEJRZVc4Z1JRd28ySlN5NmhtbnJZT1JxRjMxeUhfWkhqbkRxNCJ9XV0~WyI4cFNkNHl0TWlPdnVGaFhQSXBPbW5BIiwgImJsYWNrIl0~WyJxLWR0QXhtZzY5cWZLMFpvS1BSbWFnIiwgIndoaXRlIl0~WyIzTzY0WmVYSjF6XzJWMXdrMGhJdUdBIiwgW3siLi4uIjogImRmVnVjbkwwMC1FVFh0RGpHaDlpRHYtSE5PZmRyZ1VuTlNYRk01VUlIRVkifSwgeyIuLi4iOiAiUlRnVmxQb25RTVZJNkEzNUJic21KTThDeDVTVTN1ZXJBMENyYmpvRW02USJ9XV0~WyJNQTlSbGMwUlAxNnVJWER6blRqOWJ3IiwgIm5hbWUiLCAicHl0aG9uIl0~WyJrblRLb0lKVzZuQ1VzeW1sN3lKWTNBIiwgImFnZSIsIDEwXQ~WyJlUEdwazZjdEhOSS1HS2JKbjZrR3lBIiwgInNuYWtlIiwgeyJfc2QiOiBbIjREU0s5REpJVEhROElITFFESld6SV9yM0lheXBIek5Ma19tc3BUa2xDVzQiLCAiYy11UFhEQkZJX2FDV1BUUHlYNFV0OWdDWW1DQ1FqUEw5TnRFZGotdWZtMCJdfV0~WyJOMzgyX2xTU1dpSzZsbGdPNFFhbUdnIiwgIm5hbWUiLCAiZWFnbGUiXQ~WyJjVWZVRVBrX0pDZm1KQzhWQUp1V1pBIiwgImFnZSIsIDIwXQ~WyJSVDh5My1Odmh6QXo4Q2ctS1NDRGh3IiwgImJpcmQiLCB7Il9zZCI6IFsiUVhINU9mSF8tMGtFYkEwWDBnd0RLenphc05ZYWRWekNWRGFrYlZfWnNxNCIsICJoUmtPNjRIVXZuaEFPbDBRS1NlZDFUWUhtb0VpRW9zb0R0WmsyRVl4ejdNIl19XQ~");
591        let expected_presentation = String::from("eyJhbGciOiJFUzI1NiJ9.eyJmb28iOlt7Ii4uLiI6Ii1XMWROTk0tNUI3WlpxR3R4MkF6RTA3X0hpRUpOZVJtNGtEQ1VORTVDNFUifSx7Ii4uLiI6ImpuUURqUEFoclY1bjMtRW5PVEZHWTcwMkd0T3FhN3hua3pVM0E4aElSX3cifV0sImJhciI6eyJfc2QiOlsiX25yZUxad2xVYlp1SmtqS1RVdHR5YkhqUTNrY2J4cnZab1dxUmVBbG4tcyIsImhGcjdBRElQbjZvQ3lSckNBN0VtNldLaGk1UjdXMWJjYWFZUFFrelpGMXciXX0sInF1eCI6W3siLi4uIjoieHl6MkRSSDRTSkpjdFFtMDEtSzROVVllMTMzMWh6U3VkTXd3MENDODEyUSJ9XSwiYmF6IjpbeyIuLi4iOiJRMGcyVmYzNnl6TnNvUkdNb0dsODZnZ2QyWGFVTmg5bGN6STFfbmFZYUhnIn0seyIuLi4iOiJZcGpMNTJKd1BfYmFFS21OaHFLazE3TWFrMl9fSWJCNmctY0haSHd6dmwwIn1dLCJhbmltYWxzIjp7Il9zZCI6WyJyQ19LNzlObG95SkFPWXRCOW9ITFlsTVJSS1V4UTNnaTZ0Wld0Zm90TWRjIiwidjUyd3d6bzB5Ymw2U2V1MjZWYklUODh5bHk1LXVMZkdlYTdkWnMxSHBwMCJdfSwiX3NkX2FsZyI6InNoYS0yNTYifQ.piidRp0pHJYmtExCJnLExaaWMTBX50mLwM6gFVYnD72DszyjpKbAoZhyAXT-I4CqqSpiHZg-2w8s26XBraqX6w~WyJSLXZ0bDBmbWF6N01zR1ZWRFh3T3BnIiwgInR3byJd~WyJTQW5hNUJnaHJxUXJ0amR5SGxiejJBIiwgImdyZWVuIiwgMl0~WyI0Y2tnSjJuWVhhV21jM3pVQ253d3N3IiwgInllbGxvdyJd~WyJ2Ml8tRG5JN0lEZ1loYVMzTG9Kb013IiwgW3siLi4uIjogIkhqMUQtZE1SNXR0YmpLcl9DUENETzRuVGlkTWR1YVNpMnlnYlhtcmR4MGcifSwgeyIuLi4iOiAiUl9Sb253SFY3bzR6Y0o3TV9jcTlobVpLZ2o2RkMtdmNXTko4bzNkeTg2MCJ9XV0~WyJ1b3FDS0lpZGJzQmxhczhUaU5Kakh3IiwgInB1cnBsZSJd~WyJJWWFXMzVPNzBoUWg4OGlqWVBUVXZRIiwgW3siLi4uIjogIjlWdnFSbjk1ZUN6QnVkNkhYOG1faVRNMERZSVVxN0ZheFFtanowV1llbUkifSwgeyIuLi4iOiAiTmFOeXozWEJRZVc4Z1JRd28ySlN5NmhtbnJZT1JxRjMxeUhfWkhqbkRxNCJ9XV0~WyI4cFNkNHl0TWlPdnVGaFhQSXBPbW5BIiwgImJsYWNrIl0~WyJxLWR0QXhtZzY5cWZLMFpvS1BSbWFnIiwgIndoaXRlIl0~WyIzTzY0WmVYSjF6XzJWMXdrMGhJdUdBIiwgW3siLi4uIjogImRmVnVjbkwwMC1FVFh0RGpHaDlpRHYtSE5PZmRyZ1VuTlNYRk01VUlIRVkifSwgeyIuLi4iOiAiUlRnVmxQb25RTVZJNkEzNUJic21KTThDeDVTVTN1ZXJBMENyYmpvRW02USJ9XV0~WyJrblRLb0lKVzZuQ1VzeW1sN3lKWTNBIiwgImFnZSIsIDEwXQ~WyJlUEdwazZjdEhOSS1HS2JKbjZrR3lBIiwgInNuYWtlIiwgeyJfc2QiOiBbIjREU0s5REpJVEhROElITFFESld6SV9yM0lheXBIek5Ma19tc3BUa2xDVzQiLCAiYy11UFhEQkZJX2FDV1BUUHlYNFV0OWdDWW1DQ1FqUEw5TnRFZGotdWZtMCJdfV0~WyJjVWZVRVBrX0pDZm1KQzhWQUp1V1pBIiwgImFnZSIsIDIwXQ~WyJSVDh5My1Odmh6QXo4Q2ctS1NDRGh3IiwgImJpcmQiLCB7Il9zZCI6IFsiUVhINU9mSF8tMGtFYkEwWDBnd0RLenphc05ZYWRWekNWRGFrYlZfWnNxNCIsICJoUmtPNjRIVXZuaEFPbDBRS1NlZDFUWUhtb0VpRW9zb0R0WmsyRVl4ejdNIl19XQ~");
592
593        // Choose what to reveal
594        let revealed = json!(
595            {
596                "foo": [false, true],
597                "bar": {
598                  "red": false,
599                  "green": true
600                },
601                "qux": [
602                  [false, true]
603                ],
604                "baz": [
605                  [false, true],
606                  [true, true]
607                ],
608                "animals": {
609                  "snake": {
610                    "name": false,
611                    "age": true
612                  },
613                  "bird": {
614                    "name": false,
615                    "age": true
616                  }
617                }
618              }
619        );
620
621        let presentation =
622            SDJWTHolder::new(sd_jwt, SDJWTSerializationFormat::Compact)
623                .unwrap()
624                .create_presentation(
625                    revealed.as_object().unwrap().clone(),
626                    None,
627                    None,
628                    None,
629                    None,
630                )
631                .unwrap();
632
633        let presentation: HashSet<_> = presentation
634            .split(COMBINED_SERIALIZATION_FORMAT_SEPARATOR).map(String::from)
635            .collect();
636
637        let expected: HashSet<_> = expected_presentation
638            .split(COMBINED_SERIALIZATION_FORMAT_SEPARATOR)
639            .map(String::from).collect();
640
641        assert_eq!(presentation, expected);
642    }
643}