Skip to main content

identity_kit/sd_jwt_vc/
builder.rs

1//! Issuing an SD-JWT: choosing which claims are selectively disclosable (RFC 9901 §4.2).
2
3use core::fmt;
4
5use serde_json::{Map, Value};
6
7use super::sd_jwt::{ELLIPSIS_KEY, SD_KEY};
8use super::{ClaimPath, Disclosure, HashAlgorithm, SdJwt, SdJwtError};
9use crate::jose::{JwsHeader, Signer, base64url, encode_compact};
10
11/// Turns a claims set into an SD-JWT payload and its disclosures: the claims at the paths
12/// given to [`conceal`](Self::conceal) are replaced by digests, children before parents, so
13/// that concealing a claim and a claim inside it yields recursive disclosures.
14///
15/// Salts are 128 bits from the operating system's random number generator unless
16/// [`with_salt_generator`](Self::with_salt_generator) substitutes a source; decoy digests,
17/// when asked for, are added to every `_sd` array, which is then sorted.
18///
19/// # Examples
20///
21/// The structure of RFC 9901 §6.3: `address` is selectively disclosable, and so is each of its
22/// members.
23///
24/// ```
25/// use identity_kit::sd_jwt_vc::SdJwtBuilder;
26/// use serde_json::json;
27///
28/// let claims = json!({
29///   "sub": "6c5c0a49-b589-431d-bae7-219122a9ec2c",
30///   "address": {
31///     "street_address": "Schulstr. 12",
32///     "locality": "Schulpforta",
33///     "region": "Sachsen-Anhalt",
34///     "country": "DE"
35///   }
36/// });
37/// let encoded = SdJwtBuilder::new(claims.as_object().unwrap().clone())
38///     .conceal(["address"])
39///     .conceal(["address", "street_address"])
40///     .conceal(["address", "locality"])
41///     .conceal(["address", "region"])
42///     .conceal(["address", "country"])
43///     .encode()?;
44/// assert_eq!(encoded.disclosures.len(), 5);
45/// assert_eq!(encoded.payload["_sd"].as_array().unwrap().len(), 1);
46/// assert_eq!(encoded.payload["_sd_alg"], "sha-256");
47/// assert!(!encoded.payload.contains_key("address"));
48/// let address = encoded.disclosures.iter().find(|d| d.name() == Some("address")).unwrap();
49/// assert_eq!(address.value()["_sd"].as_array().unwrap().len(), 4);
50/// # Ok::<(), identity_kit::sd_jwt_vc::SdJwtError>(())
51/// ```
52pub struct SdJwtBuilder {
53    claims: Map<String, Value>,
54    concealed: Vec<ClaimPath>,
55    decoys: usize,
56    alg: HashAlgorithm,
57    salts: Box<dyn FnMut() -> String + Send>,
58}
59
60impl SdJwtBuilder {
61    /// The deepest nesting of objects and arrays [`encode`](Self::encode) accepts, counting the
62    /// claims set itself: a claim value may sit inside 63 more containers.
63    ///
64    /// The parsers of this crate read at most 128 nested containers (serde_json's default),
65    /// and concealment, disclosures and the payload add a few around a claim value, so a
66    /// claims set within this limit always processes back; one beyond it might not.
67    pub const MAX_DEPTH: usize = 64;
68
69    /// A builder over `claims`, nothing concealed yet.
70    pub fn new(claims: Map<String, Value>) -> Self {
71        Self {
72            claims,
73            concealed: Vec::new(),
74            decoys: 0,
75            alg: HashAlgorithm::default(),
76            salts: Box::new(random_salt),
77        }
78    }
79
80    /// This builder concealing the claim at `path`: an object property or an array element,
81    /// with `()` in the path standing for every element of an array.
82    #[must_use]
83    pub fn conceal(mut self, path: impl Into<ClaimPath>) -> Self {
84        self.concealed.push(path.into());
85        self
86    }
87
88    /// This builder adding `count` decoy digests to every `_sd` array.
89    #[must_use]
90    pub fn with_decoys(mut self, count: usize) -> Self {
91        self.decoys = count;
92        self
93    }
94
95    /// This builder hashing with `alg`.
96    #[must_use]
97    pub fn with_hash_algorithm(mut self, alg: HashAlgorithm) -> Self {
98        self.alg = alg;
99        self
100    }
101
102    /// This builder taking its salts from `salts`, one call per disclosure and decoy, in
103    /// document order with the members of an object or array before the object or array
104    /// itself.
105    #[must_use]
106    pub fn with_salt_generator(mut self, salts: impl FnMut() -> String + Send + 'static) -> Self {
107        self.salts = Box::new(salts);
108        self
109    }
110
111    /// The claims.
112    pub fn claims(&self) -> &Map<String, Value> {
113        &self.claims
114    }
115
116    /// The paths to conceal.
117    pub fn concealed(&self) -> &[ClaimPath] {
118        &self.concealed
119    }
120
121    /// The payload and the disclosures, unsigned.
122    ///
123    /// # Errors
124    ///
125    /// [`SdJwtError::NoSuchClaim`] when a path selects nothing, [`SdJwtError::NotConcealable`]
126    /// for `_sd` or `...`, [`SdJwtError::Payload`] when the claims already use `_sd` or
127    /// `_sd_alg`, [`SdJwtError::TooDeep`] when they nest deeper than
128    /// [`MAX_DEPTH`](Self::MAX_DEPTH).
129    pub fn encode(mut self) -> Result<Encoded, SdJwtError> {
130        if self.claims.contains_key(HashAlgorithm::CLAIM) {
131            return Err(SdJwtError::Payload(
132                "the claims already carry `_sd_alg`".into(),
133            ));
134        }
135        if self
136            .claims
137            .values()
138            .any(|value| nests_deeper_than(value, Self::MAX_DEPTH - 1))
139        {
140            return Err(SdJwtError::TooDeep {
141                max: Self::MAX_DEPTH,
142            });
143        }
144        let mut matched = vec![false; self.concealed.len()];
145        let mut disclosures = Vec::new();
146        let mut payload = self.object(
147            self.claims.clone(),
148            &ClaimPath::root(),
149            &mut matched,
150            &mut disclosures,
151        )?;
152        if let Some(unmatched) = matched.iter().position(|m| !m) {
153            return Err(SdJwtError::NoSuchClaim(self.concealed[unmatched].clone()));
154        }
155        if !disclosures.is_empty() || payload.contains_key(SD_KEY) {
156            payload.insert(
157                HashAlgorithm::CLAIM.to_owned(),
158                Value::String(self.alg.name().to_owned()),
159            );
160        }
161        Ok(Encoded {
162            payload,
163            disclosures,
164        })
165    }
166
167    /// The SD-JWT: the payload signed under `header` with `signer`, with every disclosure.
168    ///
169    /// # Errors
170    ///
171    /// As [`encode`](Self::encode), or [`SdJwtError::Jws`] when signing fails.
172    pub async fn sign(self, header: &JwsHeader, signer: &dyn Signer) -> Result<SdJwt, SdJwtError> {
173        let encoded = self.encode()?;
174        let payload = serde_json::to_vec(&encoded.payload).expect("a JSON object serializes");
175        let jwt = encode_compact(header, &payload, signer).await?;
176        Ok(SdJwt::new(
177            crate::vc::Jwt::new(jwt).expect("a freshly encoded JWS decodes"),
178            encoded.disclosures,
179        ))
180    }
181
182    /// Whether `path` is to be concealed, marking the pattern that matched it.
183    fn is_concealed(&self, path: &ClaimPath, matched: &mut [bool]) -> bool {
184        let mut concealed = false;
185        for (i, pattern) in self.concealed.iter().enumerate() {
186            if pattern.matches(path) {
187                matched[i] = true;
188                concealed = true;
189            }
190        }
191        concealed
192    }
193
194    fn value(
195        &mut self,
196        value: Value,
197        path: &ClaimPath,
198        matched: &mut [bool],
199        disclosures: &mut Vec<Disclosure>,
200    ) -> Result<Value, SdJwtError> {
201        match value {
202            Value::Object(object) => self
203                .object(object, path, matched, disclosures)
204                .map(Value::Object),
205            Value::Array(array) => self
206                .array(array, path, matched, disclosures)
207                .map(Value::Array),
208            other => Ok(other),
209        }
210    }
211
212    fn object(
213        &mut self,
214        object: Map<String, Value>,
215        path: &ClaimPath,
216        matched: &mut [bool],
217        disclosures: &mut Vec<Disclosure>,
218    ) -> Result<Map<String, Value>, SdJwtError> {
219        if object.contains_key(SD_KEY) {
220            return Err(SdJwtError::Payload(format!(
221                "the object at {path} already carries `_sd`"
222            )));
223        }
224        let mut out = Map::new();
225        let mut digests = Vec::new();
226        for (name, value) in object {
227            let child = path.join(name.as_str());
228            let value = self.value(value, &child, matched, disclosures)?;
229            if !self.is_concealed(&child, matched) {
230                out.insert(name, value);
231                continue;
232            }
233            if name == SD_KEY || name == ELLIPSIS_KEY {
234                return Err(SdJwtError::NotConcealable {
235                    path: child,
236                    reason: format!("`{name}` is reserved"),
237                });
238            }
239            let disclosure = Disclosure::property((self.salts)(), name, value);
240            digests.push(disclosure.digest(self.alg));
241            disclosures.push(disclosure);
242        }
243        if !digests.is_empty() {
244            for _ in 0..self.decoys {
245                digests.push(self.alg.digest((self.salts)().as_bytes()));
246            }
247            digests.sort_unstable();
248            out.insert(
249                SD_KEY.to_owned(),
250                Value::Array(digests.into_iter().map(Value::String).collect()),
251            );
252        }
253        Ok(out)
254    }
255
256    fn array(
257        &mut self,
258        array: Vec<Value>,
259        path: &ClaimPath,
260        matched: &mut [bool],
261        disclosures: &mut Vec<Disclosure>,
262    ) -> Result<Vec<Value>, SdJwtError> {
263        let mut out = Vec::with_capacity(array.len());
264        for (i, element) in array.into_iter().enumerate() {
265            let child = path.join(i);
266            let element = self.value(element, &child, matched, disclosures)?;
267            if !self.is_concealed(&child, matched) {
268                out.push(element);
269                continue;
270            }
271            let disclosure = Disclosure::element((self.salts)(), element);
272            let mut placeholder = Map::new();
273            placeholder.insert(
274                ELLIPSIS_KEY.to_owned(),
275                Value::String(disclosure.digest(self.alg)),
276            );
277            disclosures.push(disclosure);
278            out.push(Value::Object(placeholder));
279        }
280        Ok(out)
281    }
282}
283
284impl fmt::Debug for SdJwtBuilder {
285    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
286        f.debug_struct("SdJwtBuilder")
287            .field("claims", &self.claims)
288            .field("concealed", &self.concealed)
289            .field("decoys", &self.decoys)
290            .field("alg", &self.alg)
291            .finish_non_exhaustive()
292    }
293}
294
295/// 128 bits of operating system randomness, base64url-encoded.
296pub(super) fn random_salt() -> String {
297    let mut bytes = [0u8; 16];
298    getrandom::fill(&mut bytes).expect("the operating system provides randomness");
299    base64url::encode(&bytes)
300}
301
302/// The output of [`SdJwtBuilder::encode`].
303#[derive(Debug, Clone, PartialEq, Eq)]
304#[non_exhaustive]
305pub struct Encoded {
306    /// The issuer-signed JWT payload, digests in place, with `_sd_alg` when anything is
307    /// concealed.
308    pub payload: Map<String, Value>,
309    /// The disclosures, in the order their claims were concealed.
310    pub disclosures: Vec<Disclosure>,
311}
312
313/// Whether `value` nests more than `limit` containers deep, descending no further than that:
314/// a value built in memory may nest far deeper than any parser of this crate reads, and the
315/// walk must not be what overflows the stack on it.
316fn nests_deeper_than(value: &Value, limit: usize) -> bool {
317    match value {
318        Value::Array(items) => {
319            limit == 0 || items.iter().any(|item| nests_deeper_than(item, limit - 1))
320        }
321        Value::Object(members) => {
322            limit == 0
323                || members
324                    .values()
325                    .any(|member| nests_deeper_than(member, limit - 1))
326        }
327        _ => false,
328    }
329}