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

Open
wants to merge 30 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
30 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
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
30 changes: 21 additions & 9 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,30 +1,42 @@
[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"
josekit = "0.8.4"
async-trait = "0.1.80"
anyhow = "1"

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

[[example]]
name = "sd_jwt"
Expand Down
138 changes: 84 additions & 54 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-07.html)
UMR1352 marked this conversation as resolved.
Show resolved Hide resolved

## 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,17 +69,16 @@ 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!({
Expand All @@ -93,45 +97,47 @@ Any JSON object can be encoded


```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` should be selectively disclosed as well as the value of `address` and the first `phone` value.
UMR1352 marked this conversation as resolved.
Show resolved Hide resolved


```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("/address/street_address")?
.make_concealable("/address")?
.make_concealable("/phone/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("/phone", 3)? //Adds 3 decoys to the array `phone`.
UMR1352 marked this conversation as resolved.
Show resolved Hide resolved
.add_decoys("", 6)? // Adds 6 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("key-42".to_string()))
```

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

```json

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(nitpicking)

Should we use align the example calls with the example output to keep them talking about the same data? E.g. the example calls only hide /phone/0 but the example shows more phone entries as hidden, and the _sd array has 7 entries, though it should have 6?

We could also re-use the example code for the readme and use output from it, might be easier to keep in sync. ^^

{
"cnf": {
"kid": "key-42"
},
"given_name": "John",
"family_name": "Doe",
"phone": [
Expand Down Expand Up @@ -164,34 +170,63 @@ Now `encoder.object()?` will return the encoded object.

*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("/address", &hasher)?
.finish();
```

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

```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("...")?;
// 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();
```
`decoded_object`:

### Verifying

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

```rust
let mut sd_jwt = SdJwt::parse("...")?;
let disclosed_object = sd_jwt.into_disclosed_object(&hasher)?;
```
`disclosed_object`:

```json
{
Expand All @@ -200,17 +235,12 @@ Parse the SD-JWT string to extract the JWT and the disclosures in order to decod
"phone": [
"+49 123456",
"+49 234567"
],
"address": {
"region": "Anystate",
"street_address": "123 Main St"
}
]
}

```

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


Expand Down
97 changes: 53 additions & 44 deletions examples/sd_jwt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,35 @@

use std::error::Error;

use async_trait::async_trait;
use josekit::jws::alg::hmac::HmacJwsSigner;
use josekit::jws::JwsHeader;
use josekit::jws::HS256;
use josekit::jwt::JwtPayload;
use josekit::jwt::{self};
use sd_jwt_payload::Disclosure;
use sd_jwt_payload::JsonObject;
use sd_jwt_payload::JwsSigner;
use sd_jwt_payload::SdJwt;
use sd_jwt_payload::SdObjectDecoder;
use sd_jwt_payload::SdObjectEncoder;
use sd_jwt_payload::HEADER_TYP;
use sd_jwt_payload::SdJwtBuilder;
use sd_jwt_payload::Sha256Hasher;
use serde_json::json;

fn main() -> Result<(), Box<dyn Error>> {
struct HmacSignerAdapter(HmacJwsSigner);

#[async_trait]
impl JwsSigner for HmacSignerAdapter {
type Error = josekit::JoseError;
async fn sign(&self, header: &JsonObject, payload: &JsonObject) -> Result<Vec<u8>, Self::Error> {
let header = JwsHeader::from_map(header.clone())?;
let payload = JwtPayload::from_map(payload.clone())?;
let jws = jwt::encode_with_signer(&payload, &header, &self.0)?;

Ok(jws.into_bytes())
}
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let object = json!({
"sub": "user_42",
"given_name": "John",
Expand All @@ -36,50 +53,42 @@ 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("/nationalities/0", None)?,
];

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

encoder.add_sd_alg_property();
let key = b"0123456789ABCDEF0123456789ABCDEF";
let signer = HmacSignerAdapter(HS256.signer_from_bytes(key)?);
let sd_jwt = SdJwtBuilder::new(object)?
.make_concealable("/email")?
.make_concealable("/phone_number")?
.make_concealable("/address/street_address")?
.make_concealable("/address")?
.make_concealable("/nationalities/0")?
.add_decoys("/nationalities", 1)?
.add_decoys("", 2)?
.require_key_binding(sd_jwt_payload::RequiredKeyBinding::Kid("key1".to_string()))
.finish(&signer, "HS256")
.await?;

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

// 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(HEADER_TYP);
// Issuer sends the SD-JWT with all its disclosures to its holder.
let received_sd_jwt = sd_jwt.presentation();
let sd_jwt = received_sd_jwt.parse::<SdJwt>()?;

// Use the encoded object as a payload for the JWT.
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)?;
// The holder can withhold from a verifier any concealable claim by calling `conceal`.
let hasher = Sha256Hasher::new();
let (presented_sd_jwt, _removed_disclosures) = sd_jwt
.into_presentation(&hasher)?
.conceal("/email")?
.conceal("/nationalities/0")?
.finish();

// Create an SD_JWT by collecting the disclosures and creating an `SdJwt` instance.
let disclosures: Vec<String> = disclosures
.into_iter()
.map(|disclosure| disclosure.to_string())
.collect();
let sd_jwt: SdJwt = SdJwt::new(jwt, disclosures.clone(), None);
let sd_jwt: String = sd_jwt.presentation();
// The holder send its token to a verifier.
let received_sd_jwt = presented_sd_jwt.presentation();
let sd_jwt = received_sd_jwt.parse::<SdJwt>()?;

// Decoding the SD-JWT
// Extract the payload from the JWT of the SD-JWT after verifying the signature.
let sd_jwt: SdJwt = SdJwt::parse(&sd_jwt)?;
let verifier = HS256.verifier_from_bytes(key)?;
let (payload, _header) = jwt::decode_with_verifier(&sd_jwt.jwt, &verifier)?;
println!(
"object to verify: {}",
serde_json::to_string_pretty(&sd_jwt.into_disclosed_object(&hasher)?)?
);

// Decode the payload by providing the disclosures that were parsed from the SD-JWT.
let decoder = SdObjectDecoder::new_with_sha256();
let decoded = decoder.decode(payload.claims_set(), &sd_jwt.disclosures)?;
println!("decoded object: {}", serde_json::to_string_pretty(&decoded)?);
Ok(())
}
Loading
Loading