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

Upgrade to SD-JWT v12 & API rework #14

Merged
merged 36 commits into from
Jan 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
051c5b1
sd-jwt API rework
UMR1352 Sep 6, 2024
2862517
tests work
UMR1352 Sep 10, 2024
dd67e3a
update readme & tests
UMR1352 Sep 11, 2024
8338101
SdJwtPresentationBuilder
UMR1352 Sep 11, 2024
5973fa2
docs & tests
UMR1352 Sep 12, 2024
8cecbaf
cargo clippy
UMR1352 Sep 12, 2024
d87a37d
SdJwtBuilder::header
UMR1352 Sep 12, 2024
e70dc9a
add license, remove unused code
UMR1352 Sep 13, 2024
65d58b2
Lower dependencies constraints
UMR1352 Sep 16, 2024
cc13221
impl DerefMut for SdJwtClaims
UMR1352 Sep 17, 2024
b4d69e6
SdJwtClaims: Default, SdJwt::claims_mut
UMR1352 Sep 17, 2024
0c1364e
add more tests for builder
wulfraem Sep 19, 2024
8ba37a1
fix tests
UMR1352 Sep 23, 2024
dacd94f
use ok_or_else
UMR1352 Sep 23, 2024
5cdefd9
Update README.md
UMR1352 Sep 26, 2024
97095d9
Update README.md
UMR1352 Sep 26, 2024
9d3431f
Update README.md
UMR1352 Sep 26, 2024
38013b0
Update src/sd_jwt.rs
UMR1352 Sep 26, 2024
f730806
fix 0 disclosures bug
UMR1352 Sep 26, 2024
a492c48
_sd elements' order do not depend on the order of the claims
UMR1352 Sep 26, 2024
1c387e7
fix comment
UMR1352 Sep 26, 2024
11d0c85
fail to create presentation if missing requied KB-JWT
UMR1352 Sep 26, 2024
a343b7a
update example & readme
UMR1352 Sep 26, 2024
1f37bbe
SdJwtBuilder::insert_claim
UMR1352 Sep 27, 2024
ebc05f6
specify lifetime for SdJwtBuilder::insert_claim
UMR1352 Sep 27, 2024
9601f3f
ensure order of disclosures in SdJwtPresentationBuilder
UMR1352 Oct 11, 2024
dd24522
Update src/builder.rs
UMR1352 Oct 15, 2024
f2ad25f
Update src/sd_jwt.rs
UMR1352 Oct 15, 2024
3bebdc0
Update src/sd_jwt.rs
UMR1352 Oct 15, 2024
0300fc5
review comments
UMR1352 Oct 15, 2024
ea53c6e
lower indexmap version requirement
UMR1352 Dec 12, 2024
fcd7fcf
vendored josekit
UMR1352 Dec 13, 2024
9b74a1f
josekit dev-dependency
UMR1352 Dec 13, 2024
510ac95
signer's error is display
UMR1352 Dec 16, 2024
bcaac2f
signer !Send when targeting wasm32
UMR1352 Dec 16, 2024
a5b2991
update hasher
UMR1352 Dec 17, 2024
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
31 changes: 22 additions & 9 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,30 +1,43 @@
[package]
name = "sd-jwt-payload"
version = "0.2.1"
version = "0.3.0"
edition = "2021"
authors = ["IOTA Stiftung"]
homepage = "https://www.iota.org"
license = "Apache-2.0"
repository = "https://github.com/iotaledger/sd-jwt-payload"
rust-version = "1.65"
readme = "./README.md"
description = "Rust implementation of the Selective Disclosure for JWTs (SD-JWT)"
description = "Rust implementation of Selective Disclosure JWTs (SD-JWT)"
keywords = ["sd-jwt", "selective-disclosure", "disclosure"]

[dependencies]
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"] }
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.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 }
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"
serde_with = "3.6.1"
async-trait = "0.1.80"
anyhow = "1"
indexmap = "2"

[dev-dependencies]
josekit = "0.8.4"
tokio = { version = "1.38.1", features = ["macros", "rt-multi-thread"] }
josekit = { version = "0.8.4", features = ["vendored"] }

[[example]]
name = "sd_jwt"
Expand Down
208 changes: 129 additions & 79 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,20 @@

# SD-JWT Reference implementation

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)
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)

## Overview

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

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

```bash
[dependencies]
sd-jwt-payload = { version = "0.2.1" }
sd-jwt-payload = { version = "0.3.0" }
```

## Examples
Expand All @@ -64,153 +69,198 @@ See [sd_jwt.rs](./examples/sd_jwt.rs) for a runnable example.
## Usage

This library consists of the major structs:
1. [`SdObjectEncoder`](./src/encoder.rs): creates SD objects.
2. [`SdObjectDecoder`](./src/decoder.rs): decodes SD objects.
3. [`Disclosure`](./src/disclosure.rs): used by the `SdObjectEncoder` and `SdObjectDecoder` to represent a disclosure.
3. [`SdJwt`](./src/sd_jwt.rs): creates/parses SD-JWTs.
4. [`Hasher`](./src/hasher.rs): a trait to provide hash functions to the encoder/decoder.
1. [`SdJwtBuilder`](./src/builder.rs): creates SD-JWTs.
2. [`SdJwt`](./src/sd_jwt.rs): handles SD-JWTs.
3. [`Disclosure`](./src/disclosure.rs): used throughout the library to represent disclosure objects.
4. [`Hasher`](./src/hasher.rs): a trait to provide hash functions create and replace disclosures.
5. [`Sha256Hasher`](./src/hasher.rs): implements `Hasher` for the `Sha-256` hash function.
6. [`JwsSigner`](./src/signer.rs): a trait used to create JWS signatures.


### Encoding
Any JSON object can be encoded

### Creation
Any JSON object can be used to create an SD-JWT:

```rust
let object = json!({
"sub": "user_42",
"given_name": "John",
"family_name": "Doe",
"email": "[email protected]",
"phone_number": "+1-202-555-0101",
"phone_number_verified": true,
"address": {
"street_address": "123 Main St",
"locality": "Anytown",
"region": "Anystate",
"country": "US"
},
"phone": [
"+49 123456",
"+49 234567"
"birthdate": "1940-01-01",
"updated_at": 1570000000,
"nationalities": [
"US",
"DE"
]
});
```


```rust
let mut encoder: SdObjectEncoder = object.try_into()?;
let builder: SdJwtBuilder = SdJwtBuilder::new(object);
```
This creates a stateful encoder with `Sha-256` hash function by default to create disclosure digests.
This creates a stateful builder 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.*
*Note: `SdJwtBuilder` is generic over `Hasher` which allows custom encoding with other hash functions.*

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.
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.


```rust
let disclosure1 = encoder.conceal("/address/street_address"], None)?;
let disclosure2 = encoder.conceal("/address", None)?;
let disclosure3 = encoder.conceal("/phone/0", None)?;
builder
.make_concealable("/email")?
.make_concealable("/phone_number")?
.make_concealable("/address/street_address")?
.make_concealable("/address")?
.make_concealable("/nationalities/0")?
```

```
"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.*
*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.*


The encoder also supports adding decoys. For instance, the amount of phone numbers and the amount of claims need to be hidden.
The builder 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.
builder
.add_decoys("/nationalities", 1)? // Adds 1 decoys to the array `nationalities`.
.add_decoys("", 2)? // Adds 2 decoys to the top level object.
```

Add the hash function claim.
Through the builder an issuer can require a specific key-binding that will be verified upon validation:

```rust
encoder.add_sd_alg_property(); // This adds "_sd_alg": "sha-256"
builder
.require_key_binding(RequiredKeyBinding::Kid("key1".to_string()))
```

Now `encoder.object()?` will return the encoded object.
Internally, builder's object now looks like:

```json
UMR1352 marked this conversation as resolved.
Show resolved Hide resolved
{
"_sd": [
"5P7JOl7w5kWrMDQ71U4ts1CHaPPNTKDqOt9OaOdGMOg",
"73rQnMSG1np-GjzaM-yHfcZAIqmeaIK9Dn9N0atxHms",
"s0UiQ41MTAPnjfKk4HEYet0ksuMo0VTArCwG5ALiC84",
"v-xRCoLxbDcL5NZGX9uRFI0hgH9gx3uX1Y1EMcWeC5k",
"z7SAFTHCOGF8vXbHyIPXH6TQvo750AdGXhvqgMTA8Mw"
],
"_sd_alg": "sha-256",
"cnf": {
"kid": "key1"
},
"sub": "user_42",
"given_name": "John",
"family_name": "Doe",
"phone": [
{
"...": "eZVn0KkQm_T8x-x57VxYt-_MmNG91Sh34E-bZEnNfWY"
},
"+49 234567",
{
"...": "KAiJIx0tktQRXBxZSBVVld9298bZIp2WkpkDYDa3CWQ"
},
"nationalities": [
{
"...": "CBKARPh6sdTCJyliZ7pBOYzix7Z4Bb4yRh0EykHX2Uw"
"...": "xYpMTpfay0Rb77IWvbJU1C4JT3kvJUftZHxZuwfiS1M"
},
"DE",
{
"...": "oi1KgsYXgqBFXUXvbVaHSGYYaWhkB5RL55T90Gl_5s0"
"...": "GqcdlPi6GUDcj9VVpm8kj29jfXCdyBx2GfWP34339hI"
}
],
"_sd": [
"Jj5jBeGEawY6vRvmHDg55EjeAIP8FVhWEV2FczhUXrY",
"8eqphBPJyCBgUJhNWNP7ci-Y79N615wpZQrxi5D4ju8",
"_hOU5puJjNzSBhK0bwh3h8_b6H6nN7vd_7I0uTp80Mo",
"G_tH70MrfCkVM0HhsH9REObIt1Ei19477y6CEsS0Zlo",
"zP56MeH0ryjzqh9Kadrb5C9Z2BE2FWg8nb3g0rR3LSA",
"dgfVW11ip9OOyVi8M4h1RjXK8akw7ICeMQkjUwSI6iU",
"Bx33mOyTF5-w8gRS5yL4YQ4dig44V3lmHxk1WRss_7U"
],
"_sd_alg": "sha-256"
"phone_number_verified": true,
"updated_at": 1570000000,
"birthdate": "1940-01-01"
}
```

*Note: no JWT claims like `exp` or `iat` are added. If necessary, these need to be added and validated manually.*

### Creating SD-JWT

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.

Create SD-JWT
To create the actual SD-JWT the `finish` method must be called on the builder:

```rust
let sd_jwt: SdJwt = SdJwt::new(jwt, disclosures.clone(), None);
let sd_jwt: String = sd_jwt.presentation();
let signer = MyHS256Signer::new();
let sd_jwt = builder
// ...
.finish(&signer, "ES256")
.await?;
```

```
eyJ0eXAiOiJTRC1KV1QiLCJhbGciOiJIUzI1NiJ9.eyJnaXZlbl9uYW1lIjoiSm9obiIsImZhbWlseV9uYW1lIjoiRG9lIiwicGhvbmUiOlt7Ii4uLiI6ImVaVm4wS2tRbV9UOHgteDU3VnhZdC1fTW1ORzkxU2gzNEUtYlpFbk5mV1kifSwiKzQ5IDIzNDU2NyIseyIuLi4iOiJLQWlKSXgwdGt0UVJYQnhaU0JWVmxkOTI5OGJaSXAyV2twa0RZRGEzQ1dRIn0seyIuLi4iOiJDQktBUlBoNnNkVENKeWxpWjdwQk9Zeml4N1o0QmI0eVJoMEV5a0hYMlV3In0seyIuLi4iOiJvaTFLZ3NZWGdxQkZYVVh2YlZhSFNHWVlhV2hrQjVSTDU1VDkwR2xfNXMwIn1dLCJfc2QiOlsiSmo1akJlR0Vhd1k2dlJ2bUhEZzU1RWplQUlQOEZWaFdFVjJGY3poVVhyWSIsIjhlcXBoQlBKeUNCZ1VKaE5XTlA3Y2ktWTc5TjYxNXdwWlFyeGk1RDRqdTgiLCJfaE9VNXB1SmpOelNCaEswYndoM2g4X2I2SDZuTjd2ZF83STB1VHA4ME1vIiwiR190SDcwTXJmQ2tWTTBIaHNIOVJFT2JJdDFFaTE5NDc3eTZDRXNTMFpsbyIsInpQNTZNZUgwcnlqenFoOUthZHJiNUM5WjJCRTJGV2c4bmIzZzByUjNMU0EiLCJkZ2ZWVzExaXA5T095Vmk4TTRoMVJqWEs4YWt3N0lDZU1Ra2pVd1NJNmlVIiwiQngzM21PeVRGNS13OGdSUzV5TDRZUTRkaWc0NFYzbG1IeGsxV1Jzc183VSJdLCJfc2RfYWxnIjoic2hhLTI1NiJ9.knTqw4FMCplHoMu7mfiix7dv4lIjYgRIn-tmuemAhbY~WyJHaGpUZVYwV2xlUHE1bUNrVUtPVTkzcXV4WURjTzIiLCAic3RyZWV0X2FkZHJlc3MiLCAiMTIzIE1haW4gU3QiXQ~WyJVVXVBelg5RDdFV1g0c0FRVVM5aURLYVp3cU13blUiLCAiYWRkcmVzcyIsIHsicmVnaW9uIjoiQW55c3RhdGUiLCJfc2QiOlsiaHdiX2d0eG01SnhVbzJmTTQySzc3Q194QTUxcmkwTXF0TVVLZmI0ZVByMCJdfV0~WyJHRDYzSTYwUFJjb3dvdXJUUmg4OG5aM1JNbW14YVMiLCAiKzQ5IDEyMzQ1NiJd~
```

### Decoding
### Handling

Parse the SD-JWT string to extract the JWT and the disclosures in order to decode the claims and construct the disclosed values.
Once an SD-JWT is obtained, any concealable property can be omitted from it by creating a presentation and calling the
`conceal` method:

*Note: Validating the signature of the JWT and extracting the claim set is outside the scope of this library.
```rust
let mut sd_jwt = SdJwt::parse("...")?;
let hasher = Sha256Hasher::new();
let (presented_sd_jwt, removed_disclosures) = sd_jwt
.into_presentation(&hasher)?
.conceal("/email")?
.conceal("/nationalities/0")?
.finish()?;
```

To attach a key-binding JWT (KB-JWT) the `KeyBindingJwtBuilder` struct can be used:

```rust
let mut sd_jwt = SdJwt::parse("...")?;
// Can be used to check which key is required - if any.
let requird_kb: Option<&RequiredKeyBinding> = sd_jwt.required_key_binding();

let signer = MyJwkSigner::new();
let hasher = Sha256Hasher::new();
let kb_jwt = KeyBindingJwtBuilder::new()
.nonce("abcd-efgh-ijkl-mnop")
.iat(time::now())
.finish(&sd_jwt, &hasher, "ES256", &signer)
.await?;

let (sd_jwt, _) = sd_jwt.into_presentation(&hasher)?
.attach_key_binding_jwt(kb_jwt)
.finish()?;
```

### Verifying

The SD-JWT can be turned into a JSON object of its disclosed values by calling the `into_disclosed_object` method:

```rust
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)?;
let mut sd_jwt = SdJwt::parse("...")?;
let disclosed_object = sd_jwt.into_disclosed_object(&hasher)?;
```
`decoded_object`:
`disclosed_object`:

```json
{
"given_name": "John",
"family_name": "Doe",
"phone": [
"+49 123456",
"+49 234567"
],
"address": {
"country": "US",
"locality": "Anytown",
"region": "Anystate",
"street_address": "123 Main St"
}
},
"phone_number": "+1-202-555-0101",
"cnf": {
"kid": "key1"
},
"sub": "user_42",
"given_name": "John",
"family_name": "Doe",
"nationalities": [
"DE"
],
"phone_number_verified": true,
"updated_at": 1570000000,
"birthdate": "1940-01-01"
}

```

Note:
* `street_address` and `address` are recursively decoded.
* `_sd_alg` property was removed.


Expand Down
Loading
Loading