Skip to main content

bh_sd_jwt/models/
disclosure.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
16use core::fmt;
17use std::collections::{HashMap, HashSet};
18
19use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
20use bherror::{
21    traits::{ErrorContext, ForeignError},
22    Error,
23};
24
25use super::{error::DecodingResult, path_map::PathMapObject, JsonNodePath, Value};
26use crate::{
27    error::FormatError,
28    utils::{self},
29    DecodingError,
30};
31
32/// A disclosure for a JSON node in the VC, in both parsed form and the original
33/// serialized form.
34#[derive(Debug, PartialEq, Eq, Hash, Clone)]
35pub struct Disclosure {
36    pub(crate) data: DisclosureData,
37    // serialized-as-hashed
38    serialized: String,
39}
40
41impl TryFrom<String> for Disclosure {
42    type Error = Error<FormatError>;
43
44    fn try_from(serialized: String) -> Result<Self, Self::Error> {
45        let decoded = URL_SAFE_NO_PAD
46            .decode(&serialized)
47            .foreign_err(|| {
48                FormatError::InvalidDisclosure("provided string is not base64 ".to_string())
49            })
50            .ctx(|| serialized.clone())?;
51
52        let array: Vec<Value> = serde_json::from_slice(&decoded)
53            .foreign_err(|| {
54                FormatError::InvalidDisclosure(
55                    "serde json could not parse decoded base64 string ".to_string(),
56                )
57            })
58            .ctx(|| serialized.clone())?;
59
60        let data = match array.len() {
61            3 => {
62                let [salt, key, value] = array.try_into().unwrap();
63                create_disclosure_data_key_value(salt, key, value)
64            }
65            2 => {
66                let [salt, value] = array.try_into().unwrap();
67                create_disclosure_data_array_element(salt, value)
68            }
69            _ => Err(Error::root(FormatError::InvalidDisclosure(format!(
70                "deserialized disclosure array has invalid length {}",
71                array.len(),
72            )))),
73        }
74        .ctx(|| "error while creating a disclosure from base64 serialized string ".to_string())
75        .ctx(|| serialized.clone())?;
76
77        Ok(Self { data, serialized })
78    }
79}
80
81fn create_disclosure_data_key_value(
82    salt: Value,
83    key: Value,
84    value: Value,
85) -> crate::Result<DisclosureData, FormatError> {
86    let Value::String(salt) = salt else {
87        return Err(Error::root(FormatError::InvalidDisclosure(
88            "salt value is not a string".to_string(),
89        )));
90    };
91    let Value::String(key) = key else {
92        return Err(Error::root(FormatError::InvalidDisclosure(
93            "key value is not a string".to_string(),
94        )));
95    };
96
97    Ok(DisclosureData::KeyValue { salt, key, value })
98}
99
100fn create_disclosure_data_array_element(
101    salt: Value,
102    value: Value,
103) -> crate::Result<DisclosureData, FormatError> {
104    let Value::String(salt) = salt else {
105        return Err(Error::root(FormatError::InvalidDisclosure(
106            "salt value is not a string".to_string(),
107        )));
108    };
109
110    Ok(DisclosureData::ArrayElement { salt, value })
111}
112
113impl fmt::Display for Disclosure {
114    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115        match &self.data {
116            DisclosureData::KeyValue { salt, key, value } => {
117                write!(f, "[{}, {}, {}]", salt, key, value)
118            }
119            DisclosureData::ArrayElement { salt, value } => write!(f, "[{}, {}]", salt, value),
120        }
121    }
122}
123
124impl Disclosure {
125    /// Construct a new [`Disclosure`] from the given `salt`, `claim_name` and `claim_value`.
126    pub fn new(salt: String, claim_name: Option<String>, claim_value: Value) -> Self {
127        let input = if let Some(name) = &claim_name {
128            format!("[\"{}\", \"{}\", {}]", &salt, &name, &claim_value)
129        } else {
130            format!("[\"{}\", {}]", &salt, &claim_value)
131        };
132
133        let encoded = bh_jws_utils::base64_url_encode(input);
134
135        let data = if let Some(name) = claim_name {
136            DisclosureData::KeyValue {
137                salt,
138                key: name,
139                value: claim_value,
140            }
141        } else {
142            DisclosureData::ArrayElement {
143                salt,
144                value: claim_value,
145            }
146        };
147
148        Self {
149            data,
150            serialized: encoded,
151        }
152    }
153
154    /// Disclosure data value.
155    pub fn value(&self) -> &Value {
156        match &self.data {
157            DisclosureData::KeyValue { value, .. } => value,
158            DisclosureData::ArrayElement { value, .. } => value,
159        }
160    }
161
162    /// Disclosure data key, i.e. claim name.
163    pub fn claim_name(&self) -> Option<&str> {
164        match &self.data {
165            DisclosureData::KeyValue { key, .. } => Some(key),
166            _ => None,
167        }
168    }
169
170    /// Serialized form of [`Self`]
171    pub fn as_str(&self) -> &str {
172        &self.serialized
173    }
174
175    /// Serialize [`Self`] into an owned [`String`].
176    pub fn into_string(self) -> String {
177        self.serialized
178    }
179}
180
181/// Parsed form of a disclosure.
182#[derive(Debug, PartialEq, Eq, Hash, Clone)]
183pub enum DisclosureData {
184    /// A key-value pair disclosure data.
185    KeyValue {
186        /// Disclosure hash salt.
187        salt: Salt,
188        /// Key (claim name) of the disclosure.
189        key: String,
190        /// Value of the disclosure.
191        value: Value,
192    },
193    /// An array element disclosure data.
194    ArrayElement {
195        /// Disclosure hash salt.
196        salt: Salt,
197        /// Value of the disclosure.
198        value: Value,
199    },
200}
201
202/// Base64url encoded disclosure hash salt.
203pub type Salt = String;
204
205/// Base64url encoded hash value.
206pub type Digest = String;
207
208#[derive(Debug)]
209pub(crate) struct DisclosureByDigestTable<'a>(pub(crate) HashMap<Digest, &'a Disclosure>);
210
211impl<'a> DisclosureByDigestTable<'a> {
212    pub(crate) fn new(
213        disclosures: &'a [Disclosure],
214        hasher: impl crate::Hasher,
215    ) -> DecodingResult<Self> {
216        let mut disclosure_by_digest = HashMap::new();
217        for disclosure in disclosures {
218            let digest = utils::base64_url_digest(disclosure.as_str().as_bytes(), &hasher);
219            if disclosure_by_digest.insert(digest, disclosure).is_some() {
220                return Err(Error::root(DecodingError::DisclosureDigestCollision));
221            }
222        }
223        Ok(Self(disclosure_by_digest))
224    }
225}
226
227/// Table of disclosures by the path of the JSON node (i.e. key of an object or
228/// element of an array) they conceal. Useful for computing required sets of
229/// disclosures for presentations.
230///
231/// It MAY be sparse - i.e. it could not contain subtrees of the original model
232/// where no disclosures were present.
233#[derive(Debug, yoke::Yokeable)]
234pub(crate) struct DisclosureByPathTable<'model>(PathMapObject<&'model Disclosure>);
235
236impl<'model> DisclosureByPathTable<'model> {
237    pub(crate) fn new(inner: PathMapObject<&'model Disclosure>) -> Self {
238        Self(inner)
239    }
240
241    /// Return an iterator over disclosures that "cover" the provided set of
242    /// paths. Useful for creating presentations.
243    ///
244    /// A disclosure _covers_ a given path iff the path of the node the
245    /// disclosure conceals is a prefix of the given path, i.e. the given path
246    /// is such that when taken from the root of the JWT it passes through the
247    /// hash pointer to the disclosure. Such disclosures are the ones which need
248    /// to be presented so that the given set of paths would be present in the
249    /// reconstructed JSON.
250    ///
251    /// **Note that paths are not checked for existence!** Nonexistent paths are
252    /// simply ignored.
253    ///
254    /// There is no particular guaranteed order in which the disclosures are yielded.
255    pub(crate) fn disclosures_covering_paths(
256        &self,
257        paths: &[&JsonNodePath],
258    ) -> impl Iterator<Item = &'model Disclosure> {
259        let mut set = HashSet::new();
260
261        for path in paths {
262            // For every non-empty prefix of the path, if the node at that path is
263            // behind a disclosure, this disclosure covers the path and must be included
264            // when trying to disclose the node at the end of the whole path.
265
266            // Ignore the error in case the path is non-existent, since that
267            // could be simply because the table is sparse.
268            let _result = self.0.traverse_path(path.iter().copied(), |disclosure| {
269                set.insert(*disclosure);
270            });
271        }
272
273        set.into_iter()
274    }
275}
276
277#[cfg(test)]
278mod tests {
279
280    use bh_jws_utils::base64_url_encode;
281    use serde_json::{json, Value};
282
283    use crate::{error::FormatError, Disclosure};
284
285    type Result = std::result::Result<(), Box<dyn std::error::Error>>;
286
287    fn test_disclosure_encode_and_parse(
288        salt: &str,
289        claim_name: Option<&str>,
290        claim_value: Value,
291        encoded: &str,
292    ) -> Result {
293        let disclosure =
294            Disclosure::new(salt.to_owned(), claim_name.map(str::to_owned), claim_value);
295
296        assert_eq!(disclosure.as_str(), encoded);
297
298        let parsed = Disclosure::try_from(encoded.to_owned()).unwrap();
299
300        assert_eq!(parsed, disclosure);
301
302        Ok(())
303    }
304
305    /// Example taken from [here].
306    ///
307    /// [here]: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-selective-disclosure-jwt-07#section-5.2.1-5
308    #[test]
309    fn test_disclosure_encode_and_parse_object_property() -> Result {
310        test_disclosure_encode_and_parse(
311            "_26bc4LT-ac6q2KI6cBW5es",
312            Some("family_name"),
313            Value::String("Möbius".to_owned()),
314            "WyJfMjZiYzRMVC1hYzZxMktJNmNCVzVlcyIsICJmYW1pbHlfbmFtZSIsICJNw7ZiaXVzIl0",
315        )
316    }
317
318    /// Example taken from [here].
319    ///
320    /// [here]: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-selective-disclosure-jwt-07#section-5.2.2-4
321    #[test]
322    fn test_disclosure_encode_array_element() -> Result {
323        test_disclosure_encode_and_parse(
324            "lklxF5jMYlGTPUovMNIvCA",
325            None,
326            Value::String("FR".to_owned()),
327            "WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIkZSIl0",
328        )
329    }
330
331    #[test]
332    fn invalid_disclosure_not_a_base64_string() {
333        let invalid_base64 = "bla";
334
335        let decoded = Disclosure::try_from(invalid_base64.to_string());
336
337        assert_eq!(
338            decoded.unwrap_err().error,
339            FormatError::InvalidDisclosure("provided string is not base64 ".to_string())
340        )
341    }
342
343    #[test]
344    fn invalid_disclosure_too_few_elements_in_deserialized_array() {
345        let input = json!(["bla"]);
346        let encoded = base64_url_encode(input.to_string());
347
348        let decoded = Disclosure::try_from(encoded.clone());
349
350        assert_eq!(
351            decoded.unwrap_err().error,
352            FormatError::InvalidDisclosure(
353                "deserialized disclosure array has invalid length 1".to_string(),
354            )
355        );
356    }
357
358    #[test]
359    fn invalid_disclosure_too_many_elements_in_deserialized_array() {
360        let input = json!(["bla", "bla", 5, "bla"]);
361        let encoded = base64_url_encode(input.to_string());
362
363        let decoded = Disclosure::try_from(encoded.clone());
364
365        assert_eq!(
366            decoded.unwrap_err().error,
367            FormatError::InvalidDisclosure(
368                "deserialized disclosure array has invalid length 4".to_string()
369            )
370        );
371    }
372
373    #[test]
374    fn invalid_disclosure_salt_not_a_string() {
375        let input = json!([{"bla": "bla"}, 10.0]);
376
377        let encoded = base64_url_encode(input.to_string());
378
379        let decoded = Disclosure::try_from(encoded.clone());
380
381        assert_eq!(
382            decoded.unwrap_err().error,
383            FormatError::InvalidDisclosure("salt value is not a string".to_string())
384        );
385    }
386
387    #[test]
388    fn invalid_disclosure_key_is_not_a_string() {
389        let input = json!(["bla", {"bla": "bla"}, 10.0]);
390
391        let encoded = base64_url_encode(input.to_string());
392
393        let decoded = Disclosure::try_from(encoded.clone());
394
395        assert_eq!(
396            decoded.unwrap_err().error,
397            FormatError::InvalidDisclosure("key value is not a string".to_string())
398        );
399    }
400}