Skip to content

Commit 2d8e9c7

Browse files
authored
Upgrade to SD-JWT v12 & API rework (#14)
1 parent 1469117 commit 2d8e9c7

15 files changed

+1591
-770
lines changed

Cargo.toml

+22-9
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,43 @@
11
[package]
22
name = "sd-jwt-payload"
3-
version = "0.2.1"
3+
version = "0.3.0"
44
edition = "2021"
55
authors = ["IOTA Stiftung"]
66
homepage = "https://www.iota.org"
77
license = "Apache-2.0"
88
repository = "https://github.com/iotaledger/sd-jwt-payload"
9-
rust-version = "1.65"
109
readme = "./README.md"
11-
description = "Rust implementation of the Selective Disclosure for JWTs (SD-JWT)"
10+
description = "Rust implementation of Selective Disclosure JWTs (SD-JWT)"
1211
keywords = ["sd-jwt", "selective-disclosure", "disclosure"]
1312

1413
[dependencies]
1514
multibase = { version = "0.9", default-features = false, features = ["std"] }
16-
serde_json = { version = "1.0", default-features = false, features = ["std" ] }
17-
rand = { version = "0.8.5", default-features = false, features = ["std", "std_rng"] }
15+
serde_json = { version = "1.0", default-features = false, features = ["std"] }
16+
rand = { version = "0.8.5", default-features = false, features = [
17+
"std",
18+
"std_rng",
19+
] }
1820
thiserror = { version = "1.0", default-features = false }
19-
strum = { version = "0.26", default-features = false, features = ["std", "derive"] }
20-
itertools = { version = "0.12", default-features = false, features = ["use_std"] }
21-
iota-crypto = { version = "0.23", default-features = false, features = ["sha"], optional = true }
21+
strum = { version = "0.26", default-features = false, features = [
22+
"std",
23+
"derive",
24+
] }
25+
itertools = { version = "0.12", default-features = false, features = [
26+
"use_std",
27+
] }
28+
iota-crypto = { version = "0.23", default-features = false, features = [
29+
"sha",
30+
], optional = true }
2231
serde = { version = "1.0", default-features = false, features = ["derive"] }
2332
json-pointer = "0.3.4"
2433
serde_with = "3.6.1"
34+
async-trait = "0.1.80"
35+
anyhow = "1"
36+
indexmap = "2"
2537

2638
[dev-dependencies]
27-
josekit = "0.8.4"
39+
tokio = { version = "1.38.1", features = ["macros", "rt-multi-thread"] }
40+
josekit = { version = "0.8.4", features = ["vendored"] }
2841

2942
[[example]]
3043
name = "sd_jwt"

README.md

+129-79
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,20 @@
3636

3737
# SD-JWT Reference implementation
3838

39-
Rust implementation of the [Selective Disclosure for JWTs (SD-JWT) **version 07**](https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-07.html)
39+
Rust implementation of the [Selective Disclosure for JWTs (SD-JWT) **version 12**](https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-12.html)
4040

4141
## Overview
4242

4343
This library supports
44-
* **Encoding**:
45-
- creating disclosers and replacing values in objects and arrays with the digest of their disclosure.
46-
- Adding decoys to objects and arrays.
47-
* **Decoding**
44+
* **Issuing SD-JWTs**:
45+
- Create a selectively disclosable JWT by choosing which properties can be concealed from a verifier.
46+
Concealable claims are replaced with their disclosure's digest.
47+
- Adding decoys to both JSON objects and arrays.
48+
- Requiring an holder's key-bind.
49+
* **Managing SD-JWTs**
50+
- Conceal with ease any concealable property.
51+
- Insert a key-bind.
52+
* **Verifying SD-JWTs**
4853
- Recursively replace digests in objects and arrays with their corresponding disclosure value.
4954

5055
`Sha-256` hash function is shipped by default, encoding/decoding with other hash functions is possible.
@@ -54,7 +59,7 @@ Include the library in your `cargo.toml`.
5459

5560
```bash
5661
[dependencies]
57-
sd-jwt-payload = { version = "0.2.1" }
62+
sd-jwt-payload = { version = "0.3.0" }
5863
```
5964
6065
## Examples
@@ -64,153 +69,198 @@ See [sd_jwt.rs](./examples/sd_jwt.rs) for a runnable example.
6469
## Usage
6570
6671
This library consists of the major structs:
67-
1. [`SdObjectEncoder`](./src/encoder.rs): creates SD objects.
68-
2. [`SdObjectDecoder`](./src/decoder.rs): decodes SD objects.
69-
3. [`Disclosure`](./src/disclosure.rs): used by the `SdObjectEncoder` and `SdObjectDecoder` to represent a disclosure.
70-
3. [`SdJwt`](./src/sd_jwt.rs): creates/parses SD-JWTs.
71-
4. [`Hasher`](./src/hasher.rs): a trait to provide hash functions to the encoder/decoder.
72+
1. [`SdJwtBuilder`](./src/builder.rs): creates SD-JWTs.
73+
2. [`SdJwt`](./src/sd_jwt.rs): handles SD-JWTs.
74+
3. [`Disclosure`](./src/disclosure.rs): used throughout the library to represent disclosure objects.
75+
4. [`Hasher`](./src/hasher.rs): a trait to provide hash functions create and replace disclosures.
7276
5. [`Sha256Hasher`](./src/hasher.rs): implements `Hasher` for the `Sha-256` hash function.
77+
6. [`JwsSigner`](./src/signer.rs): a trait used to create JWS signatures.
7378
7479
75-
### Encoding
76-
Any JSON object can be encoded
77-
80+
### Creation
81+
Any JSON object can be used to create an SD-JWT:
7882
7983
```rust
8084
let object = json!({
85+
"sub": "user_42",
8186
"given_name": "John",
8287
"family_name": "Doe",
88+
"email": "johndoe@example.com",
89+
"phone_number": "+1-202-555-0101",
90+
"phone_number_verified": true,
8391
"address": {
8492
"street_address": "123 Main St",
93+
"locality": "Anytown",
8594
"region": "Anystate",
95+
"country": "US"
8696
},
87-
"phone": [
88-
"+49 123456",
89-
"+49 234567"
97+
"birthdate": "1940-01-01",
98+
"updated_at": 1570000000,
99+
"nationalities": [
100+
"US",
101+
"DE"
90102
]
91103
});
92104
```
93105
94106
95107
```rust
96-
let mut encoder: SdObjectEncoder = object.try_into()?;
108+
let builder: SdJwtBuilder = SdJwtBuilder::new(object);
97109
```
98-
This creates a stateful encoder with `Sha-256` hash function by default to create disclosure digests.
110+
This creates a stateful builder with `Sha-256` hash function by default to create disclosure digests.
99111
100-
*Note: `SdObjectEncoder` is generic over `Hasher` which allows custom encoding with other hash functions.*
112+
*Note: `SdJwtBuilder` is generic over `Hasher` which allows custom encoding with other hash functions.*
101113
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.
114+
The builder can encode any of the object's values or array elements, using the `make_concealable` method. Suppose the value of `street_address` in 'address' should be selectively disclosed as well as the entire value of `address` and the first `phone` value.
103115
104116
105117
```rust
106-
let disclosure1 = encoder.conceal("/address/street_address"], None)?;
107-
let disclosure2 = encoder.conceal("/address", None)?;
108-
let disclosure3 = encoder.conceal("/phone/0", None)?;
118+
builder
119+
.make_concealable("/email")?
120+
.make_concealable("/phone_number")?
121+
.make_concealable("/address/street_address")?
122+
.make_concealable("/address")?
123+
.make_concealable("/nationalities/0")?
109124
```
110125
111-
```
112-
"WyJHaGpUZVYwV2xlUHE1bUNrVUtPVTkzcXV4WURjTzIiLCAic3RyZWV0X2FkZHJlc3MiLCAiMTIzIE1haW4gU3QiXQ"
113-
"WyJVVXVBelg5RDdFV1g0c0FRVVM5aURLYVp3cU13blUiLCAiYWRkcmVzcyIsIHsicmVnaW9uIjoiQW55c3RhdGUiLCJfc2QiOlsiaHdiX2d0eG01SnhVbzJmTTQySzc3Q194QTUxcmkwTXF0TVVLZmI0ZVByMCJdfV0"
114-
"WyJHRDYzSTYwUFJjb3dvdXJUUmg4OG5aM1JNbW14YVMiLCAiKzQ5IDEyMzQ1NiJd"
115-
```
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.*
126+
*Note: the `make_concealable` method takes a [JSON Pointer](https://datatracker.ietf.org/doc/html/rfc6901) to determine the element to conceal inside the JSON object.*
117127
118128
119-
The encoder also supports adding decoys. For instance, the amount of phone numbers and the amount of claims need to be hidden.
129+
The builder also supports adding decoys. For instance, the amount of phone numbers and the amount of claims need to be hidden.
120130
121131
```rust
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.
132+
builder
133+
.add_decoys("/nationalities", 1)? // Adds 1 decoys to the array `nationalities`.
134+
.add_decoys("", 2)? // Adds 2 decoys to the top level object.
124135
```
125136
126-
Add the hash function claim.
137+
Through the builder an issuer can require a specific key-binding that will be verified upon validation:
138+
127139
```rust
128-
encoder.add_sd_alg_property(); // This adds "_sd_alg": "sha-256"
140+
builder
141+
.require_key_binding(RequiredKeyBinding::Kid("key1".to_string()))
129142
```
130143
131-
Now `encoder.object()?` will return the encoded object.
144+
Internally, builder's object now looks like:
132145
133146
```json
134147
{
148+
"_sd": [
149+
"5P7JOl7w5kWrMDQ71U4ts1CHaPPNTKDqOt9OaOdGMOg",
150+
"73rQnMSG1np-GjzaM-yHfcZAIqmeaIK9Dn9N0atxHms",
151+
"s0UiQ41MTAPnjfKk4HEYet0ksuMo0VTArCwG5ALiC84",
152+
"v-xRCoLxbDcL5NZGX9uRFI0hgH9gx3uX1Y1EMcWeC5k",
153+
"z7SAFTHCOGF8vXbHyIPXH6TQvo750AdGXhvqgMTA8Mw"
154+
],
155+
"_sd_alg": "sha-256",
156+
"cnf": {
157+
"kid": "key1"
158+
},
159+
"sub": "user_42",
135160
"given_name": "John",
136161
"family_name": "Doe",
137-
"phone": [
138-
{
139-
"...": "eZVn0KkQm_T8x-x57VxYt-_MmNG91Sh34E-bZEnNfWY"
140-
},
141-
"+49 234567",
142-
{
143-
"...": "KAiJIx0tktQRXBxZSBVVld9298bZIp2WkpkDYDa3CWQ"
144-
},
162+
"nationalities": [
145163
{
146-
"...": "CBKARPh6sdTCJyliZ7pBOYzix7Z4Bb4yRh0EykHX2Uw"
164+
"...": "xYpMTpfay0Rb77IWvbJU1C4JT3kvJUftZHxZuwfiS1M"
147165
},
166+
"DE",
148167
{
149-
"...": "oi1KgsYXgqBFXUXvbVaHSGYYaWhkB5RL55T90Gl_5s0"
168+
"...": "GqcdlPi6GUDcj9VVpm8kj29jfXCdyBx2GfWP34339hI"
150169
}
151170
],
152-
"_sd": [
153-
"Jj5jBeGEawY6vRvmHDg55EjeAIP8FVhWEV2FczhUXrY",
154-
"8eqphBPJyCBgUJhNWNP7ci-Y79N615wpZQrxi5D4ju8",
155-
"_hOU5puJjNzSBhK0bwh3h8_b6H6nN7vd_7I0uTp80Mo",
156-
"G_tH70MrfCkVM0HhsH9REObIt1Ei19477y6CEsS0Zlo",
157-
"zP56MeH0ryjzqh9Kadrb5C9Z2BE2FWg8nb3g0rR3LSA",
158-
"dgfVW11ip9OOyVi8M4h1RjXK8akw7ICeMQkjUwSI6iU",
159-
"Bx33mOyTF5-w8gRS5yL4YQ4dig44V3lmHxk1WRss_7U"
160-
],
161-
"_sd_alg": "sha-256"
171+
"phone_number_verified": true,
172+
"updated_at": 1570000000,
173+
"birthdate": "1940-01-01"
162174
}
163175
```
164176
165177
*Note: no JWT claims like `exp` or `iat` are added. If necessary, these need to be added and validated manually.*
166178
167-
### Creating SD-JWT
168-
169-
Since creating JWTs is outside the scope of this library, see [sd_jwt.rs example](./examples/sd_jwt.rs) where `josekit` is used to create `jwt` with the object above as the claim set.
170-
171-
Create SD-JWT
179+
To create the actual SD-JWT the `finish` method must be called on the builder:
172180
173181
```rust
174-
let sd_jwt: SdJwt = SdJwt::new(jwt, disclosures.clone(), None);
175-
let sd_jwt: String = sd_jwt.presentation();
182+
let signer = MyHS256Signer::new();
183+
let sd_jwt = builder
184+
// ...
185+
.finish(&signer, "ES256")
186+
.await?;
176187
```
177188
178189
```
179190
eyJ0eXAiOiJTRC1KV1QiLCJhbGciOiJIUzI1NiJ9.eyJnaXZlbl9uYW1lIjoiSm9obiIsImZhbWlseV9uYW1lIjoiRG9lIiwicGhvbmUiOlt7Ii4uLiI6ImVaVm4wS2tRbV9UOHgteDU3VnhZdC1fTW1ORzkxU2gzNEUtYlpFbk5mV1kifSwiKzQ5IDIzNDU2NyIseyIuLi4iOiJLQWlKSXgwdGt0UVJYQnhaU0JWVmxkOTI5OGJaSXAyV2twa0RZRGEzQ1dRIn0seyIuLi4iOiJDQktBUlBoNnNkVENKeWxpWjdwQk9Zeml4N1o0QmI0eVJoMEV5a0hYMlV3In0seyIuLi4iOiJvaTFLZ3NZWGdxQkZYVVh2YlZhSFNHWVlhV2hrQjVSTDU1VDkwR2xfNXMwIn1dLCJfc2QiOlsiSmo1akJlR0Vhd1k2dlJ2bUhEZzU1RWplQUlQOEZWaFdFVjJGY3poVVhyWSIsIjhlcXBoQlBKeUNCZ1VKaE5XTlA3Y2ktWTc5TjYxNXdwWlFyeGk1RDRqdTgiLCJfaE9VNXB1SmpOelNCaEswYndoM2g4X2I2SDZuTjd2ZF83STB1VHA4ME1vIiwiR190SDcwTXJmQ2tWTTBIaHNIOVJFT2JJdDFFaTE5NDc3eTZDRXNTMFpsbyIsInpQNTZNZUgwcnlqenFoOUthZHJiNUM5WjJCRTJGV2c4bmIzZzByUjNMU0EiLCJkZ2ZWVzExaXA5T095Vmk4TTRoMVJqWEs4YWt3N0lDZU1Ra2pVd1NJNmlVIiwiQngzM21PeVRGNS13OGdSUzV5TDRZUTRkaWc0NFYzbG1IeGsxV1Jzc183VSJdLCJfc2RfYWxnIjoic2hhLTI1NiJ9.knTqw4FMCplHoMu7mfiix7dv4lIjYgRIn-tmuemAhbY~WyJHaGpUZVYwV2xlUHE1bUNrVUtPVTkzcXV4WURjTzIiLCAic3RyZWV0X2FkZHJlc3MiLCAiMTIzIE1haW4gU3QiXQ~WyJVVXVBelg5RDdFV1g0c0FRVVM5aURLYVp3cU13blUiLCAiYWRkcmVzcyIsIHsicmVnaW9uIjoiQW55c3RhdGUiLCJfc2QiOlsiaHdiX2d0eG01SnhVbzJmTTQySzc3Q194QTUxcmkwTXF0TVVLZmI0ZVByMCJdfV0~WyJHRDYzSTYwUFJjb3dvdXJUUmg4OG5aM1JNbW14YVMiLCAiKzQ5IDEyMzQ1NiJd~
180191
```
181192
182-
### Decoding
193+
### Handling
183194
184-
Parse the SD-JWT string to extract the JWT and the disclosures in order to decode the claims and construct the disclosed values.
195+
Once an SD-JWT is obtained, any concealable property can be omitted from it by creating a presentation and calling the
196+
`conceal` method:
185197
186-
*Note: Validating the signature of the JWT and extracting the claim set is outside the scope of this library.
198+
```rust
199+
let mut sd_jwt = SdJwt::parse("...")?;
200+
let hasher = Sha256Hasher::new();
201+
let (presented_sd_jwt, removed_disclosures) = sd_jwt
202+
.into_presentation(&hasher)?
203+
.conceal("/email")?
204+
.conceal("/nationalities/0")?
205+
.finish()?;
206+
```
207+
208+
To attach a key-binding JWT (KB-JWT) the `KeyBindingJwtBuilder` struct can be used:
209+
210+
```rust
211+
let mut sd_jwt = SdJwt::parse("...")?;
212+
// Can be used to check which key is required - if any.
213+
let requird_kb: Option<&RequiredKeyBinding> = sd_jwt.required_key_binding();
214+
215+
let signer = MyJwkSigner::new();
216+
let hasher = Sha256Hasher::new();
217+
let kb_jwt = KeyBindingJwtBuilder::new()
218+
.nonce("abcd-efgh-ijkl-mnop")
219+
.iat(time::now())
220+
.finish(&sd_jwt, &hasher, "ES256", &signer)
221+
.await?;
222+
223+
let (sd_jwt, _) = sd_jwt.into_presentation(&hasher)?
224+
.attach_key_binding_jwt(kb_jwt)
225+
.finish()?;
226+
```
227+
228+
### Verifying
229+
230+
The SD-JWT can be turned into a JSON object of its disclosed values by calling the `into_disclosed_object` method:
187231
188232
```rust
189-
let sd_jwt: SdJwt = SdJwt::parse(sd_jwt_string)?;
190-
let claims_set: // extract claims from `sd_jwt.jwt`.
191-
let decoder = SdObjectDecoder::new();
192-
let decoded_object = decoder.decode(claims_set, &sd_jwt.disclosures)?;
233+
let mut sd_jwt = SdJwt::parse("...")?;
234+
let disclosed_object = sd_jwt.into_disclosed_object(&hasher)?;
193235
```
194-
`decoded_object`:
236+
`disclosed_object`:
195237
196238
```json
197239
{
198-
"given_name": "John",
199-
"family_name": "Doe",
200-
"phone": [
201-
"+49 123456",
202-
"+49 234567"
203-
],
204240
"address": {
241+
"country": "US",
242+
"locality": "Anytown",
205243
"region": "Anystate",
206244
"street_address": "123 Main St"
207-
}
245+
},
246+
"phone_number": "+1-202-555-0101",
247+
"cnf": {
248+
"kid": "key1"
249+
},
250+
"sub": "user_42",
251+
"given_name": "John",
252+
"family_name": "Doe",
253+
"nationalities": [
254+
"DE"
255+
],
256+
"phone_number_verified": true,
257+
"updated_at": 1570000000,
258+
"birthdate": "1940-01-01"
208259
}
209260

210261
```
211262
212263
Note:
213-
* `street_address` and `address` are recursively decoded.
214264
* `_sd_alg` property was removed.
215265
216266

0 commit comments

Comments
 (0)