Skip to content

Commit

Permalink
Use JSON Pointer for conceal (#10)
Browse files Browse the repository at this point in the history
* use json-pointer in encoder

* fix decoding bug, improve example + README

* bump to version 0.2.0

* clippy fix

* cleanup

* fix
  • Loading branch information
abdulmth authored Feb 5, 2024
1 parent 71586ef commit 43a239f
Show file tree
Hide file tree
Showing 8 changed files with 233 additions and 218 deletions.
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Change Log

## [0.2.0]

### Added
- `HEADER_TYP` constant.

### Changed
- Changed `SdObjectEncoder::conceal` to take a JSON pointer string, instead of a string array.

### Removed
- Removed `SdObjectEncoder::conceal_array_entry` (replaced by `SdObjectEncoder::conceal`).

### Fixed
- Decoding bug when objects inside arrays include digests and plain text values.

## [0.1.2]
- 07 Draft implementation.
5 changes: 3 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "sd-jwt-payload"
version = "0.1.2"
version = "0.2.0"
edition = "2021"
authors = ["IOTA Stiftung"]
homepage = "https://www.iota.org"
Expand All @@ -16,10 +16,11 @@ multibase = { version = "0.9", default-features = false, features = ["std"] }
serde_json = { version = "1.0", default-features = false, features = ["std" ] }
rand = { version = "0.8.5", default-features = false, features = ["std", "std_rng"] }
thiserror = { version = "1.0", default-features = false }
strum = { version = "0.25", default-features = false, features = ["std", "derive"] }
strum = { version = "0.26", 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 = ["sha"], optional = true }
serde = { version = "1.0", default-features = false, features = ["derive"] }
json-pointer = "0.3.4"

[dev-dependencies]
josekit = "0.8.4"
Expand Down
26 changes: 15 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ Include the library in your `cargo.toml`.

```bash
[dependencies]
sd-jwt-payload = { version = "0.1.2" }
sd-jwt-payload = { version = "0.2.0" }
```
## Examples
Expand Down Expand Up @@ -93,38 +93,42 @@ Any JSON object can be encoded
```rust
let mut encoder = SdObjectEncoder::try_from(object).unwrap();
let mut encoder: SdObjectEncoder = object.try_into()?;
```
This creates a stateful encoder with `Sha-256` hash function by default to create disclosure digests.
*Note: `SdObjectEncoder` is generic over `Hasher` which allows custom encoding with other hash functions.*
The encoder can encode any of the object's values or any array element, using the `conceal` and `conceal_array_entry` methods respectively. Suppose the value of `street_address` should be selectively disclosed as well as the value of `address` and the first `phone` value.
The encoder can encode any of the object's values or array elements, using the `conceal` method. Suppose the value of `street_address` should be selectively disclosed as well as the value of `address` and the first `phone` value.
```rust
let disclosure1 = encoder.conceal(&["address", "street_address"], None).unwrap();
let disclosure2 = encoder.conceal(&["address"], None).unwrap();
let disclosure3 = encoder.conceal_array_entry(&["phone"], 0, None).unwrap();
let disclosure1 = encoder.conceal("/address/street_address"], None)?;
let disclosure2 = encoder.conceal("/address", None)?;
let disclosure3 = encoder.conceal("/phone/0", None)?;
```
```
"WyJHaGpUZVYwV2xlUHE1bUNrVUtPVTkzcXV4WURjTzIiLCAic3RyZWV0X2FkZHJlc3MiLCAiMTIzIE1haW4gU3QiXQ"
"WyJVVXVBelg5RDdFV1g0c0FRVVM5aURLYVp3cU13blUiLCAiYWRkcmVzcyIsIHsicmVnaW9uIjoiQW55c3RhdGUiLCJfc2QiOlsiaHdiX2d0eG01SnhVbzJmTTQySzc3Q194QTUxcmkwTXF0TVVLZmI0ZVByMCJdfV0"
"WyJHRDYzSTYwUFJjb3dvdXJUUmg4OG5aM1JNbW14YVMiLCAiKzQ5IDEyMzQ1NiJd"
```
*Note: the `conceal` method takes a [JSON Pointer](https://datatracker.ietf.org/doc/html/rfc6901) to determine the element to conceal inside the JSON object.*
The encoder also supports adding decoys. For instance, the amount of phone numbers and the amount of claims need to be hidden.
```rust
encoder.add_decoys(&["phone"], 3).unwrap(); //Adds 3 decoys to the array `phone`.
encoder.add_decoys(&[], 6).unwrap(); // Adds 6 decoys to the top level object.
encoder.add_decoys("/phone", 3).unwrap(); //Adds 3 decoys to the array `phone`.
encoder.add_decoys("", 6).unwrap(); // Adds 6 decoys to the top level object.
```
Add the hash function claim.
```rust
encoder.add_sd_alg_property(); // This adds "_sd_alg": "sha-256"
```
Now `encoder.object()` will return the encoded object.
Now `encoder.object()?` will return the encoded object.
```json
{
Expand Down Expand Up @@ -182,10 +186,10 @@ Parse the SD-JWT string to extract the JWT and the disclosures in order to decod
*Note: Validating the signature of the JWT and extracting the claim set is outside the scope of this library.
```rust
let sd_jwt: SdJwt = SdJwt::parse(sd_jwt_string).unwrap();
let sd_jwt: SdJwt = SdJwt::parse(sd_jwt_string)?;
let claims_set: // extract claims from `sd_jwt.jwt`.
let decoder = SdObjectDecoder::new();
let decoded_object = decoder.decode(claims_set, &sd_jwt.disclosures).unwrap();
let decoded_object = decoder.decode(claims_set, &sd_jwt.disclosures)?;
```
`decoded_object`:
Expand Down
23 changes: 14 additions & 9 deletions examples/sd_jwt.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2020-2023 IOTA Stiftung
// Copyright 2020-2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

use std::error::Error;
Expand All @@ -11,6 +11,7 @@ use sd_jwt_payload::Disclosure;
use sd_jwt_payload::SdJwt;
use sd_jwt_payload::SdObjectDecoder;
use sd_jwt_payload::SdObjectEncoder;
use sd_jwt_payload::HEADER_TYP;
use serde_json::json;

fn main() -> Result<(), Box<dyn Error>> {
Expand All @@ -37,23 +38,27 @@ fn main() -> Result<(), Box<dyn Error>> {

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

encoder.add_decoys("/nationalities", 3)?;
encoder.add_decoys("", 4)?; // Add decoys to the top level.

encoder.add_sd_alg_property();

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

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

// Use the encoded object as a payload for the JWT.
let payload = JwtPayload::from_map(encoder.object().clone())?;
let payload = JwtPayload::from_map(encoder.object()?.clone())?;
let key = b"0123456789ABCDEF0123456789ABCDEF";
let signer = HS256.signer_from_bytes(key)?;
let jwt = jwt::encode_with_signer(&payload, &header, &signer)?;
Expand Down
27 changes: 16 additions & 11 deletions src/decoder.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2020-2023 IOTA Stiftung
// Copyright 2020-2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

use crate::ARRAY_DIGEST_KEY;
Expand All @@ -15,7 +15,7 @@ use serde_json::Map;
use serde_json::Value;
use std::collections::BTreeMap;

/// Substitutes digests in an SD-JWT object by their corresponding plaintext values provided by disclosures.
/// Substitutes digests in an SD-JWT object by their corresponding plain text values provided by disclosures.
pub struct SdObjectDecoder {
hashers: BTreeMap<String, Box<dyn Hasher>>,
}
Expand Down Expand Up @@ -54,7 +54,7 @@ impl SdObjectDecoder {
}

/// Decodes an SD-JWT `object` containing by Substituting the digests with their corresponding
/// plaintext values provided by `disclosures`.
/// plain text values provided by `disclosures`.
///
/// ## Notes
/// * The hasher is determined by the `_sd_alg` property. If none is set, the sha-256 hasher will
Expand Down Expand Up @@ -227,6 +227,7 @@ impl SdObjectDecoder {
} else {
let decoded_object = self.decode_object(object, disclosures, processed_digests)?;
output.push(Value::Object(decoded_object));
break;
}
}
} else if let Some(arr) = value.as_array() {
Expand Down Expand Up @@ -265,12 +266,16 @@ mod test {
"id": "did:value",
});
let mut encoder = SdObjectEncoder::try_from(object).unwrap();
let dis = encoder.conceal(&["id"], None).unwrap();
let dis = encoder.conceal("/id", None).unwrap();
encoder
.object_mut()
.object
.as_object_mut()
.unwrap()
.insert("id".to_string(), Value::String("id-value".to_string()));
let decoder = SdObjectDecoder::new_with_sha256();
let decoded = decoder.decode(encoder.object(), &vec![dis.to_string()]).unwrap_err();
let decoded = decoder
.decode(encoder.object().unwrap(), &vec![dis.to_string()])
.unwrap_err();
assert!(matches!(decoded, Error::ClaimCollisionError(_)));
}

Expand All @@ -284,9 +289,9 @@ 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");
assert_eq!(encoder.object().unwrap().get("_sd_alg").unwrap(), "sha-256");
let decoder = SdObjectDecoder::new_with_sha256();
let decoded = decoder.decode(encoder.object(), &vec![]).unwrap();
let decoded = decoder.decode(encoder.object().unwrap(), &vec![]).unwrap();
assert!(decoded.get("_sd_alg").is_none());
}

Expand All @@ -296,7 +301,7 @@ mod test {
"id": "did:value",
});
let mut encoder = SdObjectEncoder::try_from(object).unwrap();
let dislosure: Disclosure = encoder.conceal(&["id"], Some("test".to_string())).unwrap();
let dislosure: Disclosure = encoder.conceal("/id", Some("test".to_string())).unwrap();
// 'obj' contains digest of `id` twice.
let obj = json!({
"_sd":[
Expand All @@ -317,8 +322,8 @@ mod test {
"tst": "tst-value"
});
let mut encoder = SdObjectEncoder::try_from(object).unwrap();
let disclosure_1: Disclosure = encoder.conceal(&["id"], Some("test".to_string())).unwrap();
let disclosure_2: Disclosure = encoder.conceal(&["tst"], Some("test".to_string())).unwrap();
let disclosure_1: Disclosure = encoder.conceal("/id", Some("test".to_string())).unwrap();
let disclosure_2: Disclosure = encoder.conceal("/tst", Some("test".to_string())).unwrap();
// 'obj' contains only the digest of `id`.
let obj = json!({
"_sd":[
Expand Down
Loading

0 comments on commit 43a239f

Please sign in to comment.