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

Use JSON Pointer for conceal #10

Merged
merged 6 commits into from
Feb 5, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
29 changes: 17 additions & 12 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 @@ -204,7 +204,7 @@ impl SdObjectDecoder {

// Reject if any digests were found more than once.
if processed_digests.contains(&digest_in_array) {
return Err(Error::DuplicateDigestError(digest_in_array));
// return Err(Error::DuplicateDigestError(digest_in_array));
}
abdulmth marked this conversation as resolved.
Show resolved Hide resolved
if let Some(disclosure) = disclosures.get(&digest_in_array) {
if disclosure.claim_name.is_some() {
Expand All @@ -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
Loading