Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improvements #4

Merged
merged 13 commits into from
Dec 6, 2023
5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ rand = { version = "0.8.5", default-features = false, features = ["std", "std_rn
thiserror = { version = "1.0", default-features = false }
strum = { version = "0.25", default-features = false, features = ["std", "derive"] }
itertools = { version = "0.12", default-features = false, features = ["use_std"] }
iota-crypto = { version = "0.23", default-features = false, features = ["std", "sha"] }
iota-crypto = { version = "0.23", default-features = false, features = ["sha"], optional = true }
serde = { version = "1.0", default-features = false, features = ["derive"] }

[dev-dependencies]
Expand All @@ -27,3 +27,6 @@ josekit = "0.8.4"
[[example]]
name = "sd_jwt"

[features]
default = ["sha"]
sha = ["iota-crypto"]
40 changes: 20 additions & 20 deletions examples/sd_jwt.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright 2020-2023 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

use std::error::Error;

use josekit::jws::JwsHeader;
use josekit::jws::HS256;
use josekit::jwt::JwtPayload;
Expand All @@ -11,7 +13,7 @@ use sd_jwt::SdObjectDecoder;
use sd_jwt::SdObjectEncoder;
use serde_json::json;

fn main() {
fn main() -> Result<(), Box<dyn Error>> {
let object = json!({
"sub": "user_42",
"given_name": "John",
Expand All @@ -34,34 +36,31 @@ fn main() {
});

let mut disclosures: Vec<Disclosure> = vec![];
let mut encoder: SdObjectEncoder = object.try_into().unwrap();
let disclosure = encoder.conceal(&["email"], None).unwrap();
let mut encoder: SdObjectEncoder = object.try_into()?;
let disclosure = encoder.conceal(&["email"], None)?;
disclosures.push(disclosure);
let disclosure = encoder.conceal(&["phone_number"], None);
disclosures.push(disclosure.unwrap());
disclosures.push(disclosure?);
let disclosure = encoder.conceal(&["address", "street_address"], None);
disclosures.push(disclosure.unwrap());
disclosures.push(disclosure?);
let disclosure = encoder.conceal(&["address"], None);
disclosures.push(disclosure.unwrap());
disclosures.push(disclosure?);
let disclosure = encoder.conceal_array_entry(&["nationalities"], 0, None);
disclosures.push(disclosure.unwrap());
disclosures.push(disclosure?);
encoder.add_sd_alg_property();

println!(
"encoded object: {}",
serde_json::to_string_pretty(encoder.object()).unwrap()
);
println!("encoded object: {}", serde_json::to_string_pretty(encoder.object())?);

// Create the JWT.
// Creating JWTs is out of the scope of this library, josekit is used here as an example
let mut header = JwsHeader::new();
header.set_token_type("sd-jwt");

// Use the encoded object as a payload for the JWT.
let payload = JwtPayload::from_map(encoder.object().clone()).unwrap();
let payload = JwtPayload::from_map(encoder.object().clone())?;
let key = b"0123456789ABCDEF0123456789ABCDEF";
let signer = HS256.signer_from_bytes(key).unwrap();
let jwt = jwt::encode_with_signer(&payload, &header, &signer).unwrap();
let signer = HS256.signer_from_bytes(key)?;
let jwt = jwt::encode_with_signer(&payload, &header, &signer)?;

// Create an SD_JWT by collecting the disclosures and creating an `SdJwt` instance.
let disclosures: Vec<String> = disclosures
Expand All @@ -73,12 +72,13 @@ fn main() {

// Decoding the SD-JWT
// Extract the payload from the JWT of the SD-JWT after verifying the signature.
let sd_jwt: SdJwt = SdJwt::parse(&sd_jwt).unwrap();
let verifier = HS256.verifier_from_bytes(key).unwrap();
let (payload, _header) = jwt::decode_with_verifier(&sd_jwt.jwt, &verifier).unwrap();
let sd_jwt: SdJwt = SdJwt::parse(&sd_jwt)?;
let verifier = HS256.verifier_from_bytes(key)?;
let (payload, _header) = jwt::decode_with_verifier(&sd_jwt.jwt, &verifier)?;

// Decode the payload by providing the disclosures that were parsed from the SD-JWT.
let decoder = SdObjectDecoder::new();
let decoded = decoder.decode(payload.claims_set(), &sd_jwt.disclosures).unwrap();
println!("decoded object: {}", serde_json::to_string_pretty(&decoded).unwrap());
let decoder = SdObjectDecoder::new_with_sha256();
let decoded = decoder.decode(payload.claims_set(), &sd_jwt.disclosures)?;
println!("decoded object: {}", serde_json::to_string_pretty(&decoded)?);
Ok(())
}
64 changes: 62 additions & 2 deletions src/api_test.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
// Copyright 2020-2023 IOTA Stiftung
abdulmth marked this conversation as resolved.
Show resolved Hide resolved
// SPDX-License-Identifier: Apache-2.0

use josekit::jws::JwsAlgorithm;
use josekit::jws::JwsHeader;
use josekit::jws::JwsVerifier;
use josekit::jws::HS256;
use josekit::jwt::JwtPayload;
use josekit::jwt::{self};
use serde_json::json;
use serde_json::Map;
use serde_json::Value;

use crate::Disclosure;
Expand Down Expand Up @@ -115,7 +118,7 @@ fn test_complex_structure() {
let (payload, _header) = jwt::decode_with_verifier(&sd_jwt.jwt, &verifier).unwrap();

// Decode the payload by providing the disclosures that were parsed from the SD-JWT.
let decoder = SdObjectDecoder::new();
let decoder = SdObjectDecoder::new_with_sha256();
let decoded = decoder.decode(payload.claims_set(), &sd_jwt.disclosures).unwrap();
println!("decoded object: {}", serde_json::to_string_pretty(&decoded).unwrap());
assert_eq!(Value::Object(decoded), object);
Expand Down Expand Up @@ -156,7 +159,64 @@ fn concealed_object_in_array() {
.into_iter()
.map(|disclosure| disclosure.to_string())
.collect();
let decoder = SdObjectDecoder::new();
let decoder = SdObjectDecoder::new_with_sha256();
let decoded = decoder.decode(encoder.object(), &disclosures).unwrap();
assert_eq!(Value::Object(decoded), expected);
}

#[test]
fn decode() {
// Values taken from https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-06.html#name-example-2-handling-structur
let sd_jwt = "eyJhbGciOiAiRVMyNTYifQ.eyJfc2QiOiBbIkM5aW5wNllvUmFFWFI0Mjd6WUpQN1FyazFXSF84YmR3T0FfWVVyVW5HUVUiLCAiS3VldDF5QWEwSElRdlluT1ZkNTloY1ZpTzlVZzZKMmtTZnFZUkJlb3d2RSIsICJNTWxkT0ZGekIyZDB1bWxtcFRJYUdlcmhXZFVfUHBZZkx2S2hoX2ZfOWFZIiwgIlg2WkFZT0lJMnZQTjQwVjd4RXhad1Z3ejd5Um1MTmNWd3Q1REw4Ukx2NGciLCAiWTM0em1JbzBRTExPdGRNcFhHd2pCZ0x2cjE3eUVoaFlUMEZHb2ZSLWFJRSIsICJmeUdwMFdUd3dQdjJKRFFsbjFsU2lhZW9iWnNNV0ExMGJRNTk4OS05RFRzIiwgIm9tbUZBaWNWVDhMR0hDQjB1eXd4N2ZZdW8zTUhZS08xNWN6LVJaRVlNNVEiLCAiczBCS1lzTFd4UVFlVTh0VmxsdE03TUtzSVJUckVJYTFQa0ptcXhCQmY1VSJdLCAiaXNzIjogImh0dHBzOi8vaXNzdWVyLmV4YW1wbGUuY29tIiwgImlhdCI6IDE2ODMwMDAwMDAsICJleHAiOiAxODgzMDAwMDAwLCAiYWRkcmVzcyI6IHsiX3NkIjogWyI2YVVoelloWjdTSjFrVm1hZ1FBTzN1MkVUTjJDQzFhSGhlWnBLbmFGMF9FIiwgIkF6TGxGb2JrSjJ4aWF1cFJFUHlvSnotOS1OU2xkQjZDZ2pyN2ZVeW9IemciLCAiUHp6Y1Z1MHFiTXVCR1NqdWxmZXd6a2VzRDl6dXRPRXhuNUVXTndrclEtayIsICJiMkRrdzBqY0lGOXJHZzhfUEY4WmN2bmNXN3p3Wmo1cnlCV3ZYZnJwemVrIiwgImNQWUpISVo4VnUtZjlDQ3lWdWIyVWZnRWs4anZ2WGV6d0sxcF9KbmVlWFEiLCAiZ2xUM2hyU1U3ZlNXZ3dGNVVEWm1Xd0JUdzMyZ25VbGRJaGk4aEdWQ2FWNCIsICJydkpkNmlxNlQ1ZWptc0JNb0d3dU5YaDlxQUFGQVRBY2k0MG9pZEVlVnNBIiwgInVOSG9XWWhYc1poVkpDTkUyRHF5LXpxdDd0NjlnSkt5NVFhRnY3R3JNWDQiXX0sICJfc2RfYWxnIjogInNoYS0yNTYifQ.IjE4EfnYu1RZ1uz6yqtFh5Lppq36VC4VeSr-hLDFpZ9zqBNmMrT5JHLLXTuMJqKQp3NIzDsLaft4GK5bYyfqhg~WyJHMDJOU3JRZmpGWFE3SW8wOXN5YWpBIiwgInJlZ2lvbiIsICJcdTZlMmZcdTUzM2EiXQ~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgImNvdW50cnkiLCAiSlAiXQ~";
let sd_jwt: SdJwt = SdJwt::parse(sd_jwt).unwrap();
let (payload, _header) = jwt::decode_with_verifier(&sd_jwt.jwt, &DeocyJwsVerifier {}).unwrap();
let decoder = SdObjectDecoder::new_with_sha256();
let decoded: Map<String, Value> = decoder.decode(payload.claims_set(), &sd_jwt.disclosures).unwrap();
let expected_object = json!({
"address": {
"country": "JP",
"region": "港区"
},
"iss": "https://issuer.example.com",
"iat": 1683000000,
"exp": 1883000000
}
)
.as_object()
.unwrap()
.clone();
assert_eq!(expected_object, decoded);
}

// Boilerplate to allow extracting JWS payload without verifying the signature.
#[derive(Debug, Clone)]
struct DecoyJwsAlgorithm;
impl JwsAlgorithm for DecoyJwsAlgorithm {
fn name(&self) -> &str {
"ES256"
}

fn box_clone(&self) -> Box<dyn JwsAlgorithm> {
Box::new(self.clone())
}
}

#[derive(Debug, Clone)]
struct DeocyJwsVerifier;
abdulmth marked this conversation as resolved.
Show resolved Hide resolved
abdulmth marked this conversation as resolved.
Show resolved Hide resolved
impl JwsVerifier for DeocyJwsVerifier {
fn algorithm(&self) -> &dyn josekit::jws::JwsAlgorithm {
&DecoyJwsAlgorithm {}
}

fn key_id(&self) -> Option<&str> {
None
}

fn verify(&self, _message: &[u8], _signature: &[u8]) -> Result<(), josekit::JoseError> {
Ok(())
}

fn box_clone(&self) -> Box<dyn JwsVerifier> {
Box::new(self.clone())
}
}
28 changes: 19 additions & 9 deletions src/decoder.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
// Copyright 2020-2023 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

use crate::Utils;
use crate::ARRAY_DIGEST_KEY;
use crate::DIGESTS_KEY;
use crate::SD_ALG;
use crate::SHA_ALG_NAME;

use super::Disclosure;
use super::Hasher;
#[cfg(feature = "sha")]
use super::Sha256Hasher;
use crate::Error;
use serde_json::Map;
Expand All @@ -20,13 +22,20 @@ pub struct SdObjectDecoder {

impl SdObjectDecoder {
/// Creates a new [`SdObjectDecoder`] with `sha-256` hasher.
pub fn new() -> Self {
#[cfg(feature = "sha")]
pub fn new_with_sha256() -> Self {
let hashers: BTreeMap<String, Box<dyn Hasher>> = BTreeMap::new();
let mut hasher = Self { hashers };
hasher.add_hasher(Box::new(Sha256Hasher::new()));
hasher
}

/// Creates a new [`SdObjectDecoder`] without any hashers.
pub fn new() -> Self {
let hashers: BTreeMap<String, Box<dyn Hasher>> = BTreeMap::new();
Self { hashers }
}

/// Adds a hasher.
///
/// If a hasher for the same algorithm [`Hasher::alg_name`] already exists, it will be replaced and
Expand Down Expand Up @@ -64,7 +73,7 @@ impl SdObjectDecoder {
let mut disclosures_map: BTreeMap<String, Disclosure> = BTreeMap::new();
for disclosure in disclosures {
let parsed_disclosure = Disclosure::parse(disclosure.to_string())?;
let digest = Utils::digest_b64_url_only_ascii(hasher, disclosure.as_str());
let digest = hasher.encoded_digest(disclosure.as_str());
disclosures_map.insert(digest, parsed_disclosure);
}

Expand All @@ -76,18 +85,18 @@ impl SdObjectDecoder {
let mut decoded = self.decode_object(object, &disclosures_map, &mut processed_digests)?;

// Remove `_sd_alg` in case it exists.
decoded.remove("_sd_alg");
decoded.remove(SD_ALG);
Ok(decoded)
}

pub fn determine_hasher(&self, object: &Map<String, Value>) -> Result<&dyn Hasher, Error> {
//If the _sd_alg claim is not present at the top level, a default value of sha-256 MUST be used.
let alg: &str = if let Some(alg) = object.get("_sd_alg") {
let alg: &str = if let Some(alg) = object.get(SD_ALG) {
alg.as_str().ok_or(Error::DataTypeMismatch(
"the value of `_sd_alg` is not a string".to_string(),
))?
} else {
Sha256Hasher::ALG_NAME
SHA_ALG_NAME
};
self
.hashers
Expand Down Expand Up @@ -227,9 +236,10 @@ impl SdObjectDecoder {
}
}

#[cfg(feature = "sha")]
impl Default for SdObjectDecoder {
fn default() -> Self {
Self::new()
Self::new_with_sha256()
}
}

Expand All @@ -251,7 +261,7 @@ mod test {
encoder
.object_mut()
.insert("id".to_string(), Value::String("id-value".to_string()));
let decoder = SdObjectDecoder::new();
let decoder = SdObjectDecoder::new_with_sha256();
let decoded = decoder.decode(encoder.object(), &vec![dis.to_string()]).unwrap_err();
assert!(matches!(decoded, Error::ClaimCollisionError(_)));
}
Expand All @@ -267,7 +277,7 @@ mod test {
let mut encoder = SdObjectEncoder::try_from(object).unwrap();
encoder.add_sd_alg_property();
assert_eq!(encoder.object().get("_sd_alg").unwrap(), "sha-256");
let decoder = SdObjectDecoder::new();
let decoder = SdObjectDecoder::new_with_sha256();
let decoded = decoder.decode(encoder.object(), &vec![]).unwrap();
assert!(decoded.get("_sd_alg").is_none());
}
Expand Down
Loading
Loading