Skip to content

Commit 8d086a0

Browse files
Abdulboisjovfer
authored andcommitted
feat: Enable supporting "json" serialization format
Signed-off-by: Abdulbois <[email protected]>
1 parent 7fbd974 commit 8d086a0

File tree

6 files changed

+119
-78
lines changed

6 files changed

+119
-78
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ jsonwebtoken = "9.2"
1515
lazy_static = "1.4"
1616
log = "0.4"
1717
rand = "0.8"
18+
serde = { version = "1.0.193", features = ["derive"] }
1819
serde_json = { version = "1.0", features = ["preserve_order"] }
1920
sha2 = "0.10"
2021
thiserror = "1.0.50"

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ TBD
5151
## External Dependencies
5252

5353
Dual license (MIT/Apache 2.0)
54-
dependencies: [base64](https://crates.io/crates/base64), [lazy_static](https://crates.io/crates/lazy_static) [log](https://crates.io/crates/log), [serde_json](https://crates.io/crates/serde_json), [sha2](https://crates.io/crates/sha2), [rand](https://crates.io/crates/rand), [hmac](https://crates.io/crates/hmac), [thiserror](https://crates.io/crates/thiserror).
54+
dependencies: [base64](https://crates.io/crates/base64), [lazy_static](https://crates.io/crates/lazy_static) [log](https://crates.io/crates/log), [serde](https://crates.io/crates/serde), [serde_json](https://crates.io/crates/serde_json), [sha2](https://crates.io/crates/sha2), [rand](https://crates.io/crates/rand), [hmac](https://crates.io/crates/hmac), [thiserror](https://crates.io/crates/thiserror).
5555
MIT license dependencies: [jsonwebtoken](https://crates.io/crates/jsonwebtoken), [strum](https://crates.io/crates/strum)
5656

5757
Note: the list of dependencies may be changed in the future.

src/holder.rs

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::error;
1+
use crate::{error, SDJWTJson};
22
use error::{Error, Result};
33
use jsonwebtoken::{Algorithm, EncodingKey, Header};
44
use serde_json::{Map, Value};
@@ -21,7 +21,7 @@ pub struct SDJWTHolder {
2121
serialized_key_binding_jwt: String,
2222
sd_jwt_payload: Map<String, Value>,
2323
serialized_sd_jwt: String,
24-
sd_jwt: String,
24+
sd_jwt_json: Option<SDJWTJson>,
2525
}
2626

2727
impl SDJWTHolder {
@@ -36,7 +36,7 @@ impl SDJWTHolder {
3636

3737
let mut holder = SDJWTHolder {
3838
sd_jwt_engine: SDJWTCommon {
39-
serialization_format,
39+
serialization_format: serialization_format.clone(),
4040
..Default::default()
4141
},
4242
hs_disclosures: Vec::new(),
@@ -45,10 +45,12 @@ impl SDJWTHolder {
4545
serialized_key_binding_jwt: "".to_string(),
4646
sd_jwt_payload: Map::new(),
4747
serialized_sd_jwt: "".to_string(),
48-
sd_jwt: "".to_string(),
48+
sd_jwt_json: None,
4949
};
5050

51-
holder.sd_jwt_engine.parse_sd_jwt(sd_jwt_with_disclosures)?;
51+
holder
52+
.sd_jwt_engine
53+
.parse_sd_jwt(sd_jwt_with_disclosures.clone())?;
5254

5355
//TODO Verify signature before accepting the JWT
5456
holder.sd_jwt_payload = holder
@@ -61,6 +63,7 @@ impl SDJWTHolder {
6163
.unverified_sd_jwt
6264
.take()
6365
.ok_or(Error::InvalidState("Cannot take jwt".to_string()))?;
66+
holder.sd_jwt_json = holder.sd_jwt_engine.unverified_sd_jwt_json.clone();
6467

6568
holder.sd_jwt_engine.create_hash_mappings()?;
6669

@@ -100,19 +103,15 @@ impl SDJWTHolder {
100103
let joined = combined.join(COMBINED_SERIALIZATION_FORMAT_SEPARATOR);
101104
joined.to_string()
102105
} else {
103-
let mut sd_jwt_parsed: Map<String, Value> = serde_json::from_str(&self.sd_jwt)
104-
.map_err(|e| Error::DeserializationError(e.to_string()))?;
105-
sd_jwt_parsed.insert(
106-
crate::JWS_KEY_DISCLOSURES.to_owned(),
107-
self.hs_disclosures.clone().into(),
108-
);
106+
let mut sd_jwt_json = self
107+
.sd_jwt_json
108+
.take()
109+
.ok_or(Error::InvalidState("Cannot take SDJWTJson".to_string()))?;
110+
sd_jwt_json.disclosures = self.hs_disclosures.clone();
109111
if !self.serialized_key_binding_jwt.is_empty() {
110-
sd_jwt_parsed.insert(
111-
crate::JWS_KEY_KB_JWT.to_owned(),
112-
self.serialized_key_binding_jwt.clone().into(),
113-
);
112+
sd_jwt_json.kb_jwt = Some(self.serialized_key_binding_jwt.clone());
114113
}
115-
serde_json::to_string(&sd_jwt_parsed)
114+
serde_json::to_string(&sd_jwt_json)
116115
.map_err(|e| Error::DeserializationError(e.to_string()))?
117116
};
118117

src/issuer.rs

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::error;
1+
use crate::{error, SDJWTJson};
22
use error::Result;
33
use std::collections::{HashMap, VecDeque};
44
use std::str::FromStr;
@@ -279,13 +279,6 @@ impl SDJWTIssuer {
279279
self.signed_sd_jwt = jsonwebtoken::encode(&header, &self.sd_jwt_payload, &self.issuer_key)
280280
.map_err(|e| Error::DeserializationError(e.to_string()))?;
281281

282-
if self.inner.serialization_format == "json" {
283-
unimplemented!("json serialization is not supported for issuance");
284-
// let jws_content = serde_json::from_str(&self.serialized_sd_jwt).unwrap();
285-
// jws_content.insert(JWS_KEY_DISCLOSURES.to_string(), self.ii_disclosures.iter().map(|d| d.b64.to_string()).collect());
286-
// self.serialized_sd_jwt = serde_json::to_string(&jws_content).unwrap();
287-
}
288-
289282
Ok(())
290283
}
291284

@@ -306,7 +299,26 @@ impl SDJWTIssuer {
306299
COMBINED_SERIALIZATION_FORMAT_SEPARATOR,
307300
);
308301
} else if self.inner.serialization_format == "json" {
309-
self.serialized_sd_jwt = self.signed_sd_jwt.clone();
302+
let jwt: Vec<&str> = self.signed_sd_jwt.split('.').collect();
303+
if jwt.len() != 3 {
304+
return Err(Error::InvalidInput(format!(
305+
"Invalid JWT, JWT must contain three parts after splitting with \".\": jwt {}",
306+
self.signed_sd_jwt
307+
)));
308+
}
309+
let sd_jwt_json = SDJWTJson {
310+
protected: jwt[0].to_owned(),
311+
payload: jwt[1].to_owned(),
312+
signature: jwt[2].to_owned(),
313+
kb_jwt: None,
314+
disclosures: self
315+
.all_disclosures
316+
.iter()
317+
.map(|d| d.raw_b64.to_string())
318+
.collect(),
319+
};
320+
self.serialized_sd_jwt = serde_json::to_string(&sd_jwt_json)
321+
.map_err(|e| Error::DeserializationError(e.to_string()))?;
310322
} else {
311323
return Err(Error::InvalidInput(
312324
format!("Unknown serialization format {}, only \"compact\" or \"json\" formats are supported", self.inner.serialization_format)

src/lib.rs

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use crate::error::Error;
22
use crate::utils::{base64_hash, base64url_decode, jwt_payload_decode};
3+
34
use error::Result;
5+
use serde::{Deserialize, Serialize};
46
use serde_json::{Map, Value};
57
use std::collections::HashMap;
68
pub use {holder::SDJWTHolder, issuer::SDJWTIssuer, verifier::SDJWTVerifier};
@@ -20,8 +22,6 @@ const SD_LIST_PREFIX: &str = "...";
2022
const _SD_JWT_TYP_HEADER: &str = "sd+jwt";
2123
const KB_JWT_TYP_HEADER: &str = "kb+jwt";
2224
const KB_DIGEST_KEY: &str = "_sd_hash";
23-
const JWS_KEY_DISCLOSURES: &str = "disclosures";
24-
const JWS_KEY_KB_JWT: &str = "kb_jwt";
2525
pub const COMBINED_SERIALIZATION_FORMAT_SEPARATOR: &str = "~";
2626
const JWT_SEPARATOR: &str = ".";
2727
const CNF_KEY: &str = "cnf";
@@ -38,12 +38,22 @@ pub(crate) struct SDJWTCommon {
3838
serialization_format: String,
3939
unverified_input_key_binding_jwt: Option<String>,
4040
unverified_sd_jwt: Option<String>,
41+
unverified_sd_jwt_json: Option<SDJWTJson>,
4142
unverified_input_sd_jwt_payload: Option<Map<String, Value>>,
4243
hash_to_decoded_disclosure: HashMap<String, Value>,
4344
hash_to_disclosure: HashMap<String, String>,
4445
input_disclosures: Vec<String>,
4546
}
4647

48+
#[derive(Default, Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
49+
pub struct SDJWTJson {
50+
protected: String,
51+
payload: String,
52+
signature: String,
53+
pub disclosures: Vec<String>,
54+
pub kb_jwt: Option<String>,
55+
}
56+
4757
// Define the SDJWTCommon struct to hold common properties.
4858
impl SDJWTCommon {
4959
fn create_hash_mappings(&mut self) -> Result<()> {
@@ -150,25 +160,19 @@ impl SDJWTCommon {
150160
self.unverified_input_sd_jwt_payload = Some(jwt_payload_decode(jwt_body)?);
151161
Ok(())
152162
} else {
153-
// If the SD-JWT is in JSON format, parse the JSON and extract the disclosures.
154-
let unverified_input_sd_jwt_parsed: Value =
155-
serde_json::from_str(&sd_jwt_with_disclosures)
156-
.map_err(|e| Error::DeserializationError(e.to_string()))?;
157-
self.unverified_input_key_binding_jwt = unverified_input_sd_jwt_parsed
158-
.get(JWS_KEY_KB_JWT)
159-
.map(Value::to_string);
160-
self.input_disclosures = unverified_input_sd_jwt_parsed[JWS_KEY_DISCLOSURES]
161-
.as_array()
162-
.ok_or(Error::ConversionError(
163-
"Cannot convert `disclosures` to array".to_string(),
164-
))?
165-
.iter()
166-
.map(Value::to_string)
167-
.collect();
168-
let payload = unverified_input_sd_jwt_parsed["payload"]
169-
.as_str()
170-
.ok_or(Error::KeyNotFound("payload".to_string()))?;
171-
self.unverified_input_sd_jwt_payload = Some(jwt_payload_decode(payload)?);
163+
let parsed_sd_jwt_json: SDJWTJson = serde_json::from_str(&sd_jwt_with_disclosures)
164+
.map_err(|e| Error::DeserializationError(e.to_string()))?;
165+
self.unverified_sd_jwt_json = Some(parsed_sd_jwt_json.clone());
166+
self.unverified_input_key_binding_jwt = parsed_sd_jwt_json.kb_jwt;
167+
self.input_disclosures = parsed_sd_jwt_json.disclosures;
168+
self.unverified_input_sd_jwt_payload =
169+
Some(jwt_payload_decode(&parsed_sd_jwt_json.payload)?);
170+
self.unverified_sd_jwt = Some(format!(
171+
"{}.{}.{}",
172+
parsed_sd_jwt_json.protected,
173+
parsed_sd_jwt_json.payload,
174+
parsed_sd_jwt_json.signature
175+
));
172176
Ok(())
173177
}
174178
}

tests/demos.rs

Lines changed: 56 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use jsonwebtoken::jwk::Jwk;
88
use jsonwebtoken::{DecodingKey, EncodingKey};
99
use rstest::{fixture, rstest};
1010
use sd_jwt_rs::issuer::SDJWTClaimsStrategy;
11-
use sd_jwt_rs::{SDJWTHolder, SDJWTIssuer, SDJWTVerifier};
11+
use sd_jwt_rs::{SDJWTHolder, SDJWTIssuer, SDJWTJson, SDJWTVerifier};
1212
use sd_jwt_rs::{COMBINED_SERIALIZATION_FORMAT_SEPARATOR, DEFAULT_SIGNING_ALG};
1313
use serde_json::{json, Map, Value};
1414
use std::collections::HashSet;
@@ -287,7 +287,7 @@ fn demo_positive_cases(
287287
Option<EncodingKey>,
288288
Option<Jwk>,
289289
),
290-
#[values("compact".to_string())] format: String,
290+
#[values("compact".to_string(), "json".to_string())] format: String,
291291
#[values(None, Some(DEFAULT_SIGNING_ALG.to_owned()))] sign_algo: Option<String>,
292292
#[values(true, false)] add_decoy: bool,
293293
) {
@@ -300,38 +300,62 @@ fn demo_positive_cases(
300300
holder_jwk.clone(),
301301
add_decoy,
302302
format.clone(),
303-
).unwrap();
303+
)
304+
.unwrap();
304305
let issued = sd_jwt.clone();
305306
// Holder creates presentation
306307
let mut holder = SDJWTHolder::new(sd_jwt.clone(), format.clone()).unwrap();
307-
let presentation = holder.create_presentation(
308-
holder_disclosed_claims,
309-
nonce.clone(),
310-
aud.clone(),
311-
holder_key,
312-
sign_algo,
313-
).unwrap();
314-
315-
let mut issued_parts: HashSet<&str> = issued
316-
.split(COMBINED_SERIALIZATION_FORMAT_SEPARATOR)
317-
.collect();
318-
issued_parts.remove("");
319-
320-
let mut revealed_parts: HashSet<&str> = presentation
321-
.split(COMBINED_SERIALIZATION_FORMAT_SEPARATOR)
322-
.collect();
323-
revealed_parts.remove("");
324-
325-
let intersected_parts: HashSet<_> = issued_parts.intersection(&revealed_parts).collect();
326-
// Compare that number of disclosed parts are equal
327-
let mut revealed_parts_number = revealed_parts.len();
328-
if holder_jwk.is_some() {
329-
// Remove KB
330-
revealed_parts_number -= 1;
308+
let presentation = holder
309+
.create_presentation(
310+
holder_disclosed_claims,
311+
nonce.clone(),
312+
aud.clone(),
313+
holder_key,
314+
sign_algo,
315+
)
316+
.unwrap();
317+
318+
if format == "compact" {
319+
let mut issued_parts: HashSet<&str> = issued
320+
.split(COMBINED_SERIALIZATION_FORMAT_SEPARATOR)
321+
.collect();
322+
issued_parts.remove("");
323+
324+
let mut revealed_parts: HashSet<&str> = presentation
325+
.split(COMBINED_SERIALIZATION_FORMAT_SEPARATOR)
326+
.collect();
327+
revealed_parts.remove("");
328+
329+
let intersected_parts: HashSet<_> = issued_parts.intersection(&revealed_parts).collect();
330+
// Compare that number of disclosed parts are equal
331+
let mut revealed_parts_number = revealed_parts.len();
332+
if holder_jwk.is_some() {
333+
// Remove KB
334+
revealed_parts_number -= 1;
335+
}
336+
assert_eq!(intersected_parts.len(), revealed_parts_number);
337+
// here `+1` means adding issued jwt part also
338+
assert_eq!(number_of_revealed_sds + 1, revealed_parts_number);
339+
} else {
340+
let mut issued: SDJWTJson = serde_json::from_str(&issued).unwrap();
341+
let mut revealed: SDJWTJson = serde_json::from_str(&presentation).unwrap();
342+
let disclosures: Vec<String> = revealed
343+
.disclosures
344+
.clone()
345+
.into_iter()
346+
.filter(|d| issued.disclosures.contains(d))
347+
.collect();
348+
assert_eq!(number_of_revealed_sds, disclosures.len());
349+
350+
if holder_jwk.is_some() {
351+
assert!(revealed.kb_jwt.is_some());
352+
}
353+
354+
issued.disclosures = disclosures;
355+
revealed.kb_jwt = None;
356+
assert_eq!(revealed, issued);
331357
}
332-
assert_eq!(intersected_parts.len(), revealed_parts_number);
333-
// here `+1` means adding issued jwt part also
334-
assert_eq!(number_of_revealed_sds + 1, revealed_parts_number);
358+
335359
// Verify presentation
336360
let _verified = SDJWTVerifier::new(
337361
presentation.clone(),
@@ -342,5 +366,6 @@ fn demo_positive_cases(
342366
aud,
343367
nonce,
344368
format,
345-
).unwrap();
369+
)
370+
.unwrap();
346371
}

0 commit comments

Comments
 (0)