Skip to content

Commit 43a239f

Browse files
authored
Use JSON Pointer for conceal (#10)
* use json-pointer in encoder * fix decoding bug, improve example + README * bump to version 0.2.0 * clippy fix * cleanup * fix
1 parent 71586ef commit 43a239f

File tree

8 files changed

+233
-218
lines changed

8 files changed

+233
-218
lines changed

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Change Log
2+
3+
## [0.2.0]
4+
5+
### Added
6+
- `HEADER_TYP` constant.
7+
8+
### Changed
9+
- Changed `SdObjectEncoder::conceal` to take a JSON pointer string, instead of a string array.
10+
11+
### Removed
12+
- Removed `SdObjectEncoder::conceal_array_entry` (replaced by `SdObjectEncoder::conceal`).
13+
14+
### Fixed
15+
- Decoding bug when objects inside arrays include digests and plain text values.
16+
17+
## [0.1.2]
18+
- 07 Draft implementation.

Cargo.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "sd-jwt-payload"
3-
version = "0.1.2"
3+
version = "0.2.0"
44
edition = "2021"
55
authors = ["IOTA Stiftung"]
66
homepage = "https://www.iota.org"
@@ -16,10 +16,11 @@ multibase = { version = "0.9", default-features = false, features = ["std"] }
1616
serde_json = { version = "1.0", default-features = false, features = ["std" ] }
1717
rand = { version = "0.8.5", default-features = false, features = ["std", "std_rng"] }
1818
thiserror = { version = "1.0", default-features = false }
19-
strum = { version = "0.25", default-features = false, features = ["std", "derive"] }
19+
strum = { version = "0.26", default-features = false, features = ["std", "derive"] }
2020
itertools = { version = "0.12", default-features = false, features = ["use_std"] }
2121
iota-crypto = { version = "0.23", default-features = false, features = ["sha"], optional = true }
2222
serde = { version = "1.0", default-features = false, features = ["derive"] }
23+
json-pointer = "0.3.4"
2324

2425
[dev-dependencies]
2526
josekit = "0.8.4"

README.md

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ Include the library in your `cargo.toml`.
5454

5555
```bash
5656
[dependencies]
57-
sd-jwt-payload = { version = "0.1.2" }
57+
sd-jwt-payload = { version = "0.2.0" }
5858
```
5959
6060
## Examples
@@ -93,38 +93,42 @@ Any JSON object can be encoded
9393
9494
9595
```rust
96-
let mut encoder = SdObjectEncoder::try_from(object).unwrap();
96+
let mut encoder: SdObjectEncoder = object.try_into()?;
9797
```
9898
This creates a stateful encoder with `Sha-256` hash function by default to create disclosure digests.
9999
100100
*Note: `SdObjectEncoder` is generic over `Hasher` which allows custom encoding with other hash functions.*
101101
102-
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.
102+
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.
103+
103104
104105
```rust
105-
let disclosure1 = encoder.conceal(&["address", "street_address"], None).unwrap();
106-
let disclosure2 = encoder.conceal(&["address"], None).unwrap();
107-
let disclosure3 = encoder.conceal_array_entry(&["phone"], 0, None).unwrap();
106+
let disclosure1 = encoder.conceal("/address/street_address"], None)?;
107+
let disclosure2 = encoder.conceal("/address", None)?;
108+
let disclosure3 = encoder.conceal("/phone/0", None)?;
108109
```
109110
110111
```
111112
"WyJHaGpUZVYwV2xlUHE1bUNrVUtPVTkzcXV4WURjTzIiLCAic3RyZWV0X2FkZHJlc3MiLCAiMTIzIE1haW4gU3QiXQ"
112113
"WyJVVXVBelg5RDdFV1g0c0FRVVM5aURLYVp3cU13blUiLCAiYWRkcmVzcyIsIHsicmVnaW9uIjoiQW55c3RhdGUiLCJfc2QiOlsiaHdiX2d0eG01SnhVbzJmTTQySzc3Q194QTUxcmkwTXF0TVVLZmI0ZVByMCJdfV0"
113114
"WyJHRDYzSTYwUFJjb3dvdXJUUmg4OG5aM1JNbW14YVMiLCAiKzQ5IDEyMzQ1NiJd"
114115
```
116+
*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.*
117+
118+
115119
The encoder also supports adding decoys. For instance, the amount of phone numbers and the amount of claims need to be hidden.
116120
117121
```rust
118-
encoder.add_decoys(&["phone"], 3).unwrap(); //Adds 3 decoys to the array `phone`.
119-
encoder.add_decoys(&[], 6).unwrap(); // Adds 6 decoys to the top level object.
122+
encoder.add_decoys("/phone", 3).unwrap(); //Adds 3 decoys to the array `phone`.
123+
encoder.add_decoys("", 6).unwrap(); // Adds 6 decoys to the top level object.
120124
```
121125
122126
Add the hash function claim.
123127
```rust
124128
encoder.add_sd_alg_property(); // This adds "_sd_alg": "sha-256"
125129
```
126130
127-
Now `encoder.object()` will return the encoded object.
131+
Now `encoder.object()?` will return the encoded object.
128132
129133
```json
130134
{
@@ -182,10 +186,10 @@ Parse the SD-JWT string to extract the JWT and the disclosures in order to decod
182186
*Note: Validating the signature of the JWT and extracting the claim set is outside the scope of this library.
183187
184188
```rust
185-
let sd_jwt: SdJwt = SdJwt::parse(sd_jwt_string).unwrap();
189+
let sd_jwt: SdJwt = SdJwt::parse(sd_jwt_string)?;
186190
let claims_set: // extract claims from `sd_jwt.jwt`.
187191
let decoder = SdObjectDecoder::new();
188-
let decoded_object = decoder.decode(claims_set, &sd_jwt.disclosures).unwrap();
192+
let decoded_object = decoder.decode(claims_set, &sd_jwt.disclosures)?;
189193
```
190194
`decoded_object`:
191195

examples/sd_jwt.rs

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2020-2023 IOTA Stiftung
1+
// Copyright 2020-2024 IOTA Stiftung
22
// SPDX-License-Identifier: Apache-2.0
33

44
use std::error::Error;
@@ -11,6 +11,7 @@ use sd_jwt_payload::Disclosure;
1111
use sd_jwt_payload::SdJwt;
1212
use sd_jwt_payload::SdObjectDecoder;
1313
use sd_jwt_payload::SdObjectEncoder;
14+
use sd_jwt_payload::HEADER_TYP;
1415
use serde_json::json;
1516

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

3839
let mut encoder: SdObjectEncoder = object.try_into()?;
3940
let disclosures: Vec<Disclosure> = vec![
40-
encoder.conceal(&["email"], None)?,
41-
encoder.conceal(&["phone_number"], None)?,
42-
encoder.conceal(&["address", "street_address"], None)?,
43-
encoder.conceal(&["address"], None)?,
44-
encoder.conceal_array_entry(&["nationalities"], 0, None)?,
41+
encoder.conceal("/email", None)?,
42+
encoder.conceal("/phone_number", None)?,
43+
encoder.conceal("/address/street_address", None)?,
44+
encoder.conceal("/address", None)?,
45+
encoder.conceal("/nationalities/0", None)?,
4546
];
47+
48+
encoder.add_decoys("/nationalities", 3)?;
49+
encoder.add_decoys("", 4)?; // Add decoys to the top level.
50+
4651
encoder.add_sd_alg_property();
4752

48-
println!("encoded object: {}", serde_json::to_string_pretty(encoder.object())?);
53+
println!("encoded object: {}", serde_json::to_string_pretty(encoder.object()?)?);
4954

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

5560
// Use the encoded object as a payload for the JWT.
56-
let payload = JwtPayload::from_map(encoder.object().clone())?;
61+
let payload = JwtPayload::from_map(encoder.object()?.clone())?;
5762
let key = b"0123456789ABCDEF0123456789ABCDEF";
5863
let signer = HS256.signer_from_bytes(key)?;
5964
let jwt = jwt::encode_with_signer(&payload, &header, &signer)?;

src/decoder.rs

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2020-2023 IOTA Stiftung
1+
// Copyright 2020-2024 IOTA Stiftung
22
// SPDX-License-Identifier: Apache-2.0
33

44
use crate::ARRAY_DIGEST_KEY;
@@ -15,7 +15,7 @@ use serde_json::Map;
1515
use serde_json::Value;
1616
use std::collections::BTreeMap;
1717

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

5656
/// Decodes an SD-JWT `object` containing by Substituting the digests with their corresponding
57-
/// plaintext values provided by `disclosures`.
57+
/// plain text values provided by `disclosures`.
5858
///
5959
/// ## Notes
6060
/// * The hasher is determined by the `_sd_alg` property. If none is set, the sha-256 hasher will
@@ -227,6 +227,7 @@ impl SdObjectDecoder {
227227
} else {
228228
let decoded_object = self.decode_object(object, disclosures, processed_digests)?;
229229
output.push(Value::Object(decoded_object));
230+
break;
230231
}
231232
}
232233
} else if let Some(arr) = value.as_array() {
@@ -265,12 +266,16 @@ mod test {
265266
"id": "did:value",
266267
});
267268
let mut encoder = SdObjectEncoder::try_from(object).unwrap();
268-
let dis = encoder.conceal(&["id"], None).unwrap();
269+
let dis = encoder.conceal("/id", None).unwrap();
269270
encoder
270-
.object_mut()
271+
.object
272+
.as_object_mut()
273+
.unwrap()
271274
.insert("id".to_string(), Value::String("id-value".to_string()));
272275
let decoder = SdObjectDecoder::new_with_sha256();
273-
let decoded = decoder.decode(encoder.object(), &vec![dis.to_string()]).unwrap_err();
276+
let decoded = decoder
277+
.decode(encoder.object().unwrap(), &vec![dis.to_string()])
278+
.unwrap_err();
274279
assert!(matches!(decoded, Error::ClaimCollisionError(_)));
275280
}
276281

@@ -284,9 +289,9 @@ mod test {
284289
});
285290
let mut encoder = SdObjectEncoder::try_from(object).unwrap();
286291
encoder.add_sd_alg_property();
287-
assert_eq!(encoder.object().get("_sd_alg").unwrap(), "sha-256");
292+
assert_eq!(encoder.object().unwrap().get("_sd_alg").unwrap(), "sha-256");
288293
let decoder = SdObjectDecoder::new_with_sha256();
289-
let decoded = decoder.decode(encoder.object(), &vec![]).unwrap();
294+
let decoded = decoder.decode(encoder.object().unwrap(), &vec![]).unwrap();
290295
assert!(decoded.get("_sd_alg").is_none());
291296
}
292297

@@ -296,7 +301,7 @@ mod test {
296301
"id": "did:value",
297302
});
298303
let mut encoder = SdObjectEncoder::try_from(object).unwrap();
299-
let dislosure: Disclosure = encoder.conceal(&["id"], Some("test".to_string())).unwrap();
304+
let dislosure: Disclosure = encoder.conceal("/id", Some("test".to_string())).unwrap();
300305
// 'obj' contains digest of `id` twice.
301306
let obj = json!({
302307
"_sd":[
@@ -317,8 +322,8 @@ mod test {
317322
"tst": "tst-value"
318323
});
319324
let mut encoder = SdObjectEncoder::try_from(object).unwrap();
320-
let disclosure_1: Disclosure = encoder.conceal(&["id"], Some("test".to_string())).unwrap();
321-
let disclosure_2: Disclosure = encoder.conceal(&["tst"], Some("test".to_string())).unwrap();
325+
let disclosure_1: Disclosure = encoder.conceal("/id", Some("test".to_string())).unwrap();
326+
let disclosure_2: Disclosure = encoder.conceal("/tst", Some("test".to_string())).unwrap();
322327
// 'obj' contains only the digest of `id`.
323328
let obj = json!({
324329
"_sd":[

0 commit comments

Comments
 (0)