1use crate::{utils::is_url_safe_base64_char, DecodeError};
2use base64::{prelude::BASE64_URL_SAFE_NO_PAD, Engine};
3use serde_json::Value;
4use std::{
5 borrow::{Borrow, Cow},
6 fmt,
7};
8
9#[derive(Debug, thiserror::Error)]
11#[error("invalid SD-JWT disclosure: `{0}`")]
12pub struct InvalidDisclosure<T>(pub T);
13
14#[macro_export]
16macro_rules! disclosure {
17 ($s:literal) => {
18 match $crate::Disclosure::from_str_const($s) {
19 Ok(d) => d,
20 Err(_) => panic!("invalid disclosure"),
21 }
22 };
23}
24
25#[derive(PartialEq)]
32pub struct Disclosure([u8]);
33
34impl Disclosure {
35 pub fn new<T: ?Sized + AsRef<[u8]>>(disclosure: &T) -> Result<&Self, InvalidDisclosure<&T>> {
40 let bytes = disclosure.as_ref();
41 if bytes.iter().copied().all(is_url_safe_base64_char) {
42 Ok(unsafe { Self::new_unchecked(bytes) })
43 } else {
44 Err(InvalidDisclosure(disclosure))
45 }
46 }
47
48 pub const fn from_str_const(disclosure: &str) -> Result<&Self, InvalidDisclosure<&str>> {
56 let bytes = disclosure.as_bytes();
57 let mut i = 0;
58
59 while i < bytes.len() {
60 if !is_url_safe_base64_char(bytes[i]) {
61 return Err(InvalidDisclosure(disclosure));
62 }
63
64 i += 1
65 }
66
67 Ok(unsafe { Self::new_unchecked(bytes) })
68 }
69
70 pub const unsafe fn new_unchecked(bytes: &[u8]) -> &Self {
77 std::mem::transmute(bytes)
78 }
79
80 pub fn as_bytes(&self) -> &[u8] {
82 &self.0
83 }
84
85 pub fn as_str(&self) -> &str {
87 unsafe {
88 std::str::from_utf8_unchecked(&self.0)
90 }
91 }
92}
93
94impl AsRef<[u8]> for Disclosure {
95 fn as_ref(&self) -> &[u8] {
96 self.as_bytes()
97 }
98}
99
100impl AsRef<str> for Disclosure {
101 fn as_ref(&self) -> &str {
102 self.as_str()
103 }
104}
105
106impl fmt::Display for Disclosure {
107 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108 self.as_str().fmt(f)
109 }
110}
111
112impl fmt::Debug for Disclosure {
113 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114 self.as_str().fmt(f)
115 }
116}
117
118impl ToOwned for Disclosure {
119 type Owned = DisclosureBuf;
120
121 fn to_owned(&self) -> Self::Owned {
122 DisclosureBuf(self.as_bytes().to_owned())
123 }
124}
125
126pub struct DisclosureBuf(Vec<u8>);
128
129impl DisclosureBuf {
130 pub fn encode_from_parts(salt: &str, kind: &DisclosureDescription) -> Self {
132 Self(
133 BASE64_URL_SAFE_NO_PAD
134 .encode(kind.to_value(salt).to_string())
135 .into_bytes(),
136 )
137 }
138
139 pub fn as_disclosure(&self) -> &Disclosure {
141 unsafe {
142 Disclosure::new_unchecked(&self.0)
144 }
145 }
146}
147
148impl Borrow<Disclosure> for DisclosureBuf {
149 fn borrow(&self) -> &Disclosure {
150 self.as_disclosure()
151 }
152}
153
154impl fmt::Display for DisclosureBuf {
155 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156 self.as_disclosure().fmt(f)
157 }
158}
159
160impl fmt::Debug for DisclosureBuf {
161 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
162 self.as_disclosure().fmt(f)
163 }
164}
165
166#[derive(Debug, Clone, PartialEq)]
168pub struct DecodedDisclosure<'a> {
169 pub encoded: Cow<'a, Disclosure>,
171
172 pub salt: String,
174
175 pub desc: DisclosureDescription,
177}
178
179impl<'a> DecodedDisclosure<'a> {
180 pub fn new(encoded: &'a (impl ?Sized + AsRef<[u8]>)) -> Result<Self, DecodeError> {
182 let base64 = encoded.as_ref();
183 let bytes = BASE64_URL_SAFE_NO_PAD
184 .decode(base64)
185 .map_err(|_| DecodeError::DisclosureMalformed)?;
186
187 let encoded = unsafe {
188 Disclosure::new_unchecked(base64)
190 };
191
192 let json: serde_json::Value = serde_json::from_slice(&bytes)?;
193
194 match json {
195 serde_json::Value::Array(values) => match values.as_slice() {
196 [salt, name, value] => Ok(DecodedDisclosure {
197 encoded: Cow::Borrowed(encoded),
198 salt: salt
199 .as_str()
200 .ok_or(DecodeError::DisclosureMalformed)?
201 .to_owned(),
202 desc: DisclosureDescription::ObjectEntry {
203 key: name
204 .as_str()
205 .ok_or(DecodeError::DisclosureMalformed)?
206 .to_owned(),
207 value: value.clone(),
208 },
209 }),
210 [salt, value] => Ok(DecodedDisclosure {
211 encoded: Cow::Borrowed(encoded),
212 salt: salt
213 .as_str()
214 .ok_or(DecodeError::DisclosureMalformed)?
215 .to_owned(),
216 desc: DisclosureDescription::ArrayItem(value.clone()),
217 }),
218 _ => Err(DecodeError::DisclosureMalformed),
219 },
220 _ => Err(DecodeError::DisclosureMalformed),
221 }
222 }
223
224 pub fn from_parts(salt: String, kind: DisclosureDescription) -> Self {
229 Self {
230 encoded: Cow::Owned(DisclosureBuf::encode_from_parts(&salt, &kind)),
231 salt,
232 desc: kind,
233 }
234 }
235
236 pub fn into_owned(self) -> DecodedDisclosure<'static> {
238 DecodedDisclosure {
239 encoded: Cow::Owned(self.encoded.into_owned()),
240 salt: self.salt,
241 desc: self.desc,
242 }
243 }
244}
245
246#[derive(Debug, Clone, PartialEq)]
248pub enum DisclosureDescription {
249 ObjectEntry {
251 key: String,
253
254 value: serde_json::Value,
256 },
257
258 ArrayItem(serde_json::Value),
260}
261
262impl DisclosureDescription {
263 pub fn to_value(&self, salt: &str) -> Value {
265 match self {
266 Self::ObjectEntry { key, value } => {
267 Value::Array(vec![salt.into(), key.to_owned().into(), value.clone()])
268 }
269 Self::ArrayItem(value) => Value::Array(vec![salt.into(), value.clone()]),
270 }
271 }
272}
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277 use crate::SdAlg;
278
279 fn verify_sd_disclosures_array(
280 digest_algo: SdAlg,
281 disclosures: &[&str],
282 sd_claim: &[&str],
283 ) -> Result<serde_json::Value, DecodeError> {
284 let mut verfied_claims = serde_json::Map::new();
285
286 for disclosure in disclosures {
287 let disclosure_hash = digest_algo.hash(Disclosure::new(disclosure).unwrap());
288
289 if !disclosure_hash_exists_in_sd_claims(&disclosure_hash, sd_claim) {
290 continue;
291 }
292
293 let decoded = DecodedDisclosure::new(disclosure)?;
294
295 match decoded.desc {
296 DisclosureDescription::ObjectEntry { key: name, value } => {
297 let orig = verfied_claims.insert(name, value);
298
299 if orig.is_some() {
300 return Err(DecodeError::DisclosureUsedMultipleTimes);
301 }
302 }
303 DisclosureDescription::ArrayItem(_) => {
304 return Err(DecodeError::ArrayDisclosureWhenExpectingProperty);
305 }
306 }
307 }
308
309 Ok(serde_json::Value::Object(verfied_claims))
310 }
311
312 fn disclosure_hash_exists_in_sd_claims(disclosure_hash: &str, sd_claim: &[&str]) -> bool {
313 for sd_claim_item in sd_claim {
314 if &disclosure_hash == sd_claim_item {
315 return true;
316 }
317 }
318
319 false
320 }
321
322 #[test]
323 fn test_verify_disclosures() {
324 const DISCLOSURES: [&str; 7] = [
325 "WyJyU0x1em5oaUxQQkRSWkUxQ1o4OEtRIiwgInN1YiIsICJqb2huX2RvZV80MiJd",
326 "WyJhYTFPYmdlUkJnODJudnpMYnRQTklRIiwgImdpdmVuX25hbWUiLCAiSm9obiJd",
327 "WyI2VWhsZU5HUmJtc0xDOFRndTh2OFdnIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd",
328 "WyJ2S0t6alFSOWtsbFh2OWVkNUJ1ZHZRIiwgImVtYWlsIiwgImpvaG5kb2VAZXhhbXBsZS5jb20iXQ",
329 "WyJVZEVmXzY0SEN0T1BpZDRFZmhPQWNRIiwgInBob25lX251bWJlciIsICIrMS0yMDItNTU1LTAxMDEiXQ",
330 "WyJOYTNWb0ZGblZ3MjhqT0FyazdJTlZnIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0",
331 "WyJkQW9mNHNlZTFGdDBXR2dHanVjZ2pRIiwgImJpcnRoZGF0ZSIsICIxOTQwLTAxLTAxIl0",
332 ];
333
334 const SD_CLAIM: [&str; 7] = [
335 "5nXy0Z3QiEba1V1lJzeKhAOGQXFlKLIWCLlhf_O-cmo",
336 "9gZhHAhV7LZnOFZq_q7Fh8rzdqrrNM-hRWsVOlW3nuw",
337 "S-JPBSkvqliFv1__thuXt3IzX5B_ZXm4W2qs4BoNFrA",
338 "bviw7pWAkbzI078ZNVa_eMZvk0tdPa5w2o9R3Zycjo4",
339 "o-LBCDrFF6tC9ew1vAlUmw6Y30CHZF5jOUFhpx5mogI",
340 "pzkHIM9sv7oZH6YKDsRqNgFGLpEKIj3c5G6UKaTsAjQ",
341 "rnAzCT6DTy4TsX9QCDv2wwAE4Ze20uRigtVNQkA52X0",
342 ];
343
344 let expected_claims: serde_json::Value = serde_json::json!({
345 "sub": "john_doe_42",
346 "given_name": "John",
347 "family_name": "Doe",
348 "email": "johndoe@example.com",
349 "phone_number": "+1-202-555-0101",
350 "address": {"street_address": "123 Main St", "locality": "Anytown", "region": "Anystate", "country": "US"},
351 "birthdate": "1940-01-01"
352 });
353
354 let verified_claims =
355 verify_sd_disclosures_array(SdAlg::Sha256, &DISCLOSURES, &SD_CLAIM).unwrap();
356
357 assert_eq!(verified_claims, expected_claims)
358 }
359
360 #[test]
361 fn test_verify_subset_of_disclosures() {
362 const DISCLOSURES: [&str; 2] = [
363 "WyJhYTFPYmdlUkJnODJudnpMYnRQTklRIiwgImdpdmVuX25hbWUiLCAiSm9obiJd",
364 "WyI2VWhsZU5HUmJtc0xDOFRndTh2OFdnIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd",
365 ];
366
367 const SD_CLAIM: [&str; 7] = [
368 "5nXy0Z3QiEba1V1lJzeKhAOGQXFlKLIWCLlhf_O-cmo",
369 "9gZhHAhV7LZnOFZq_q7Fh8rzdqrrNM-hRWsVOlW3nuw",
370 "S-JPBSkvqliFv1__thuXt3IzX5B_ZXm4W2qs4BoNFrA",
371 "bviw7pWAkbzI078ZNVa_eMZvk0tdPa5w2o9R3Zycjo4",
372 "o-LBCDrFF6tC9ew1vAlUmw6Y30CHZF5jOUFhpx5mogI",
373 "pzkHIM9sv7oZH6YKDsRqNgFGLpEKIj3c5G6UKaTsAjQ",
374 "rnAzCT6DTy4TsX9QCDv2wwAE4Ze20uRigtVNQkA52X0",
375 ];
376
377 let expected_claims: serde_json::Value = serde_json::json!({
378 "given_name": "John",
379 "family_name": "Doe",
380 });
381
382 let verified_claims =
383 verify_sd_disclosures_array(SdAlg::Sha256, &DISCLOSURES, &SD_CLAIM).unwrap();
384
385 assert_eq!(verified_claims, expected_claims)
386 }
387
388 #[test]
389 fn decode_array_disclosure() {
390 assert_eq!(
391 DecodedDisclosure::from_parts(
392 "nPuoQnkRFq3BIeAm7AnXFA".to_owned(),
393 DisclosureDescription::ArrayItem(serde_json::json!("DE"))
394 ),
395 DecodedDisclosure::new("WyJuUHVvUW5rUkZxM0JJZUFtN0FuWEZBIiwiREUiXQ").unwrap()
396 )
397 }
398}