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

Support Selective Disclosure SD-JWT #1268

Merged
merged 35 commits into from
Jan 18, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
7586ed8
first encoder/decoder impl
abdulmth Oct 15, 2023
e1a3f3e
implement poc validator
abdulmth Oct 17, 2023
b1ecf88
kb-jwt validator poc
abdulmth Nov 27, 2023
0c24017
fix issuance date validation
abdulmth Nov 27, 2023
a6c0938
improve validator
abdulmth Nov 30, 2023
ba1be31
improvements
abdulmth Dec 6, 2023
95f4b60
improve example
abdulmth Dec 7, 2023
033792f
tst
abdulmth Dec 7, 2023
348484b
add tests
abdulmth Dec 11, 2023
37ea17f
kb tests
abdulmth Dec 11, 2023
67b0881
fmt
abdulmth Dec 11, 2023
a6b0951
clippy
abdulmth Dec 11, 2023
67d3dde
struct for key binding options
abdulmth Dec 11, 2023
7dd781a
clippy
abdulmth Dec 11, 2023
344a0c3
Merge branch 'main' into feat/poc-sd-jwt
abdulmth Dec 11, 2023
14349c5
fix example
abdulmth Dec 12, 2023
aca2fff
dprint fmt
abdulmth Dec 12, 2023
6a5cc5a
fix license
abdulmth Dec 12, 2023
6459d98
add wasm bindings
abdulmth Jan 10, 2024
7800866
decouple JwtValidator and SdJwtValidator
abdulmth Jan 11, 2024
68622e7
binding for validator
abdulmth Jan 11, 2024
1106490
wasm bindings for validator
abdulmth Jan 15, 2024
d365a92
Merge branch 'main' into feat/poc-sd-jwt
abdulmth Jan 15, 2024
3bbb4f0
improvements
abdulmth Jan 15, 2024
56e6bb7
clippy
abdulmth Jan 15, 2024
4bf9990
fmt
abdulmth Jan 15, 2024
8a215bc
skip serialization of none
abdulmth Jan 15, 2024
6c30f93
improvements
abdulmth Jan 15, 2024
7573000
api reference
abdulmth Jan 15, 2024
b2fe9d7
fix wasm clippy issues
abdulmth Jan 15, 2024
5b29f16
dprint
abdulmth Jan 15, 2024
979c224
make `sd-jwt-payload` optional
abdulmth Jan 15, 2024
7cf7585
add example to CI, improvements
abdulmth Jan 17, 2024
d28a27d
fmt
abdulmth Jan 17, 2024
ecddea3
fix cypress test
abdulmth Jan 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
2 changes: 1 addition & 1 deletion examples/0_basic/5_create_vc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
//! This Verifiable Credential can be verified by anyone, allowing Alice to take control of it and share it with
//! whomever they please.
//!
//! cargo run --example 5_create_vc
//! cargo run --release --example 5_create_vc

use examples::create_did;
use examples::MemStorage;
Expand Down
2 changes: 1 addition & 1 deletion examples/0_basic/6_create_vp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
//! A Verifiable Presentation is the format in which a (collection of) Verifiable Credential(s) gets shared.
//! It is signed by the subject, to prove control over the Verifiable Credential with a nonce or timestamp.
//!
//! cargo run --example 6_create_vp
//! cargo run --release --example 6_create_vp

use std::collections::HashMap;

Expand Down
2 changes: 1 addition & 1 deletion examples/0_basic/7_revoke_vc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
//!
//! Note: make sure `API_ENDPOINT` and `FAUCET_ENDPOINT` are set to the correct network endpoints.
//!
//! cargo run --example 7_revoke_vc
//! cargo run --release --example 7_revoke_vc

use anyhow::anyhow;
use examples::create_did;
Expand Down
222 changes: 222 additions & 0 deletions examples/1_advanced/7_sd_jwt.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
// Copyright 2020-2023 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

//! This example shows how to create a selective disclosure verifiable credential and validate it
//! using the standard [Selective Disclosure for JWTs (SD-JWT)](https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-06.html).
//!
//! cargo run --release --example 7_sd_jwt

use examples::create_did;
use examples::pretty_print_json;
use examples::random_stronghold_path;
use examples::MemStorage;
use examples::API_ENDPOINT;
use identity_eddsa_verifier::EdDSAJwsVerifier;
use identity_iota::core::json;
use identity_iota::core::FromJson;
use identity_iota::core::Object;
use identity_iota::core::ToJson;
use identity_iota::core::Url;
use identity_iota::credential::Credential;
use identity_iota::credential::CredentialBuilder;
use identity_iota::credential::FailFast;
use identity_iota::credential::Jws;
use identity_iota::credential::JwtCredentialValidationOptions;
use identity_iota::credential::KeyBindingJWTValidationOptions;
use identity_iota::credential::SdJwtValidator;
use identity_iota::credential::Subject;
use identity_iota::did::DID;
use identity_iota::iota::IotaDocument;
use identity_iota::storage::JwkDocumentExt;
use identity_iota::storage::JwkMemStore;
use identity_iota::storage::JwsSignatureOptions;
use identity_iota::storage::KeyIdMemstore;
use iota_sdk::client::secret::stronghold::StrongholdSecretManager;
use iota_sdk::client::secret::SecretManager;
use iota_sdk::client::Client;
use iota_sdk::client::Password;
use iota_sdk::types::block::address::Address;
use sd_jwt_payload::KeyBindingJwtClaims;
use sd_jwt_payload::SdJwt;
use sd_jwt_payload::SdObjectDecoder;
use sd_jwt_payload::SdObjectEncoder;
use sd_jwt_payload::Sha256Hasher;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
// ===========================================================================
// Step 1: Create identities for the issuer and the holder.
// ===========================================================================

// Create a new client to interact with the IOTA ledger.
let client: Client = Client::builder()
.with_primary_node(API_ENDPOINT, None)?
.finish()
.await?;

// Create an identity for the issuer with one verification method `key-1`.
let mut secret_manager_issuer: SecretManager = SecretManager::Stronghold(
StrongholdSecretManager::builder()
.password(Password::from("secure_password_1".to_owned()))
.build(random_stronghold_path())?,
);
let issuer_storage: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new());
let (_, issuer_document, fragment): (Address, IotaDocument, String) =
create_did(&client, &mut secret_manager_issuer, &issuer_storage).await?;

// Create an identity for the holder, in this case also the subject.
let mut secret_manager_alice: SecretManager = SecretManager::Stronghold(
StrongholdSecretManager::builder()
.password(Password::from("secure_password_2".to_owned()))
.build(random_stronghold_path())?,
);
let alice_storage: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new());
let (_, alice_document, alice_fragment): (Address, IotaDocument, String) =
create_did(&client, &mut secret_manager_alice, &alice_storage).await?;

// ===========================================================================
// Step 2: Issuer creates and signs a selectively disclosable JWT verifiable credential.
// ===========================================================================

// Create a credential subject indicating the degree earned by Alice.
let subject: Subject = Subject::from_json_value(json!({
"id": alice_document.id().as_str(),
"name": "Alice",
"address": {
"locality": "Maxstadt",
"postal_code": "12344",
"country": "DE",
"street_address": "Weidenstraße 22"
}
}))?;

// Build credential using subject above and issuer.
let credential: Credential = CredentialBuilder::default()
.id(Url::parse("https://example.edu/credentials/3732")?)
.issuer(Url::parse(issuer_document.id().as_str())?)
.type_("AddressCredential")
.subject(subject)
.build()?;

// In Order to create an selective disclosure JWT, the plain text JWT
// claims set must be created first.
let payload = credential.serialize_jwt(None)?;
pretty_print_json("Claims set in plain text", &payload);

// Using the crate `sd-jwt` properties of the claims can be made selectively disclosable.
// The default sha-256 hasher will be used to create the digests.
// Read more in https://github.com/iotaledger/sd-jwt .
let mut encoder = SdObjectEncoder::new(&payload)?;
// Make "locality", "postal_code" and "street_address" selectively disclosable while keeping
// other properties in plain text.
let disclosures = vec![
encoder.conceal(&["vc", "credentialSubject", "address", "locality"], None)?,
encoder.conceal(&["vc", "credentialSubject", "address", "postal_code"], None)?,
encoder.conceal(&["vc", "credentialSubject", "address", "street_address"], None)?,
];
encoder.add_sd_alg_property();
let encoded_payload = encoder.try_to_string()?;

pretty_print_json("Claims set with disclosure digests", &encoded_payload);

// Create the signed JWT.
let jwt: Jws = issuer_document
.create_jws(
&issuer_storage,
&fragment,
encoded_payload.as_bytes(),
&JwsSignatureOptions::default(),
)
.await?;

// ===========================================================================
// Step 3: Issuer sends the JWT and the disclosures to the holder.
// ===========================================================================

// One way to send the JWT and the disclosures, is by creating an SD-JWT with all the
// disclosures.
let disclosures: Vec<String> = disclosures
.into_iter()
.map(|disclosure| disclosure.to_string())
.collect();
let sd_jwt_str = SdJwt::new(jwt.into(), disclosures, None).presentation();

// ===========================================================================
// Step 4: Verifier sends the holder a challenge and requests a signed Verifiable Presentation.
// ===========================================================================

const VERIFIER_DID: &str = "did:example:verifier";
// A unique random challenge generated by the requester per presentation can mitigate replay attacks.
let nonce: &str = "475a7984-1bb5-4c4c-a56f-822bccd46440";

// ===========================================================================
// Step 5: Holder creates an SD-JWT to be presented to a verifier.
// ===========================================================================

let sd_jwt = SdJwt::parse(&sd_jwt_str)?;

// The holder only wants to present "locality" and "postal_code" but not "street_address".
let disclosures = vec![
sd_jwt.disclosures.get(0).unwrap().clone(),
sd_jwt.disclosures.get(1).unwrap().clone(),
];

// Optionally, the holder can add a Key Binding JWT (KB-JWT). This is dependent on the verifier's policy.
// Issuing the KB-JWT is done by creating the claims set and setting the header `typ` value
// with the help of `KeyBindingJwtClaims`.
let binding_claims = KeyBindingJwtClaims::new(
&Sha256Hasher::new(),
sd_jwt.jwt.as_str().to_string(),
disclosures.clone(),
nonce.to_string(),
VERIFIER_DID.to_string(),
None,
)
.to_json()?;

// Setting the `typ` in the header is required.
let options = JwsSignatureOptions::new().typ(KeyBindingJwtClaims::KB_JWT_HEADER_TYP);

// Create the KB-JWT.
let kb_jwt: Jws = alice_document
.create_jws(&alice_storage, &alice_fragment, binding_claims.as_bytes(), &options)
.await?;

// Create the final SD-JWT.
let sd_jwt_obj = SdJwt::new(sd_jwt.jwt, disclosures, Some(kb_jwt.into()));

// ===========================================================================
// Step 6: Holder presents the SD-JWT to the verifier.
// ===========================================================================

let sd_jwt_presentation: String = sd_jwt_obj.presentation();

// ===========================================================================
// Step 7: Verifier receives the SD-JWT and verifies it.
// ===========================================================================

let sd_jwt = SdJwt::parse(&sd_jwt_presentation)?;

// Verify the JWT.
let decoder = SdObjectDecoder::new_with_sha256();
let validator = SdJwtValidator::new(EdDSAJwsVerifier::default(), decoder);
let validation = validator
.validate_credential::<_, Object>(
&sd_jwt,
&issuer_document,
&JwtCredentialValidationOptions::default(),
FailFast::FirstError,
)
.unwrap();

println!("JWT successfully validated");
pretty_print_json("Decoded Credential", &validation.credential.to_string());

// Verify the Key Binding JWT.
let options = KeyBindingJWTValidationOptions::new().nonce(nonce).aud(VERIFIER_DID);
let _kb_validation = validator.validate_key_binding_jwt(&sd_jwt_obj, &alice_document, &options)?;

println!("Key Binding JWT successfully validated");

Ok(())
}
6 changes: 6 additions & 0 deletions examples/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ identity_stronghold = { path = "../identity_stronghold", default-features = fals
iota-sdk = { version = "1.0", default-features = false, features = ["tls", "client", "stronghold"] }
primitive-types = "0.12.1"
rand = "0.8.5"
sd-jwt-payload = { version = "0.1.0", default-features = false, features = ["sha"] }
serde_json = { version = "1.0", default-features = false }
tokio = { version = "1.29", default-features = false, features = ["rt"] }

[lib]
Expand Down Expand Up @@ -81,3 +83,7 @@ name = "5_custom_resolution"
[[example]]
path = "1_advanced/6_domain_linkage.rs"
name = "6_domain_linkage"

[[example]]
path = "1_advanced/7_sd_jwt.rs"
name = "7_sd_jwt"
6 changes: 5 additions & 1 deletion examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@ cargo run --example <example_name>
For instance, to run the example `0_create_did`, use:

```rust
cargo run --example 0_create_did
cargo run --release --example 0_create_did
```

### Note: Running the examples with the release flag will be significantly faster due to stronghold performance issues in debug mode.


## Basic Examples

The following basic CRUD (Create, Read, Update, Delete) examples are available:
Expand Down Expand Up @@ -44,3 +47,4 @@ The following advanced examples are available:
| [4_alias_output_history](./1_advanced/4_alias_output_history.rs) | Demonstrates fetching the history of an Alias Output. |
| [5_custom_resolution](./1_advanced/5_custom_resolution.rs) | Demonstrates how to set up a resolver using custom handlers. |
| [6_domain_linkage](./1_advanced/6_domain_linkage) | Demonstrates how to link a domain and a DID and verify the linkage. |
| [7_sd_jwt](./1_advanced/7_sd_jwt) | Demonstrates how to create and verify selective disclosure verifiable credentials. |
10 changes: 10 additions & 0 deletions examples/utils/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ use iota_sdk::types::block::address::Address;
use iota_sdk::types::block::address::Bech32Address;
use iota_sdk::types::block::address::Hrp;
use rand::distributions::DistString;
use serde_json::Value;

pub static API_ENDPOINT: &str = "http://localhost:14265";
pub static FAUCET_ENDPOINT: &str = "http://localhost:8091/api/enqueue";
Expand Down Expand Up @@ -175,3 +176,12 @@ pub fn random_stronghold_path() -> PathBuf {
file.set_extension("stronghold");
file.to_owned()
}

pub fn pretty_print_json(label: &str, value: &str) {
let data: Value = serde_json::from_str(value).unwrap();
let pretty_json = serde_json::to_string_pretty(&data).unwrap();
println!("--------------------------------------");
println!("{}:", label);
println!("--------------------------------------");
println!("{} \n", pretty_json);
}
14 changes: 8 additions & 6 deletions identity_credential/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ description = "An implementation of the Verifiable Credentials standard."

[dependencies]
dataurl = { version = "0.1.2", default-features = false, optional = true }
flate2 = { version = "1.0.23", default-features = false, features = ["rust_backend"], optional = true }
flate2 = { version = "1.0.28", default-features = false, features = ["rust_backend"], optional = true }
futures = { version = "0.3", default-features = false, optional = true }
identity_core = { version = "=1.0.0", path = "../identity_core", default-features = false }
identity_did = { version = "=1.0.0", path = "../identity_did", default-features = false }
Expand All @@ -24,18 +24,19 @@ itertools = { version = "0.11", default-features = false, features = ["use_std"]
once_cell = { version = "1.18", default-features = false, features = ["std"] }
reqwest = { version = "0.11", default-features = false, features = ["default-tls", "json", "stream"], optional = true }
roaring = { version = "0.10", default-features = false, optional = true }
sd-jwt-payload = { version = "0.1.0", default-features = false, features = ["sha"] }
serde.workspace = true
serde_json.workspace = true
serde_repr = { version = "0.1", default-features = false, optional = true }
strum.workspace = true
thiserror.workspace = true
url = { version = "2.4", default-features = false }
url = { version = "2.5", default-features = false }

[dev-dependencies]
identity_eddsa_verifier = { version = "=1.0.0", path = "../identity_eddsa_verifier", default-features = false, features = ["ed25519"] }
iota-crypto = { version = "0.23", default-features = false, features = ["ed25519", "std", "random"] }
proptest = { version = "1.0.0", default-features = false, features = ["std"] }
serde_json.workspace = true
tokio = { version = "1.29.0", default-features = false, features = ["rt-multi-thread", "macros"] }
proptest = { version = "1.4.0", default-features = false, features = ["std"] }
tokio = { version = "1.35.0", default-features = false, features = ["rt-multi-thread", "macros"] }

[package.metadata.docs.rs]
# To build locally:
Expand All @@ -44,10 +45,11 @@ all-features = true
rustdoc-args = ["--cfg", "docsrs"]

[features]
default = ["revocation-bitmap", "validator", "credential", "presentation", "domain-linkage-fetch"]
default = ["revocation-bitmap", "validator", "credential", "presentation", "domain-linkage-fetch", "sd-jwt"]
credential = []
presentation = ["credential"]
revocation-bitmap = ["dep:dataurl", "dep:flate2", "dep:roaring"]
validator = ["dep:itertools", "dep:serde_repr", "credential", "presentation"]
domain-linkage = ["validator"]
domain-linkage-fetch = ["domain-linkage", "dep:reqwest", "dep:futures"]
sd-jwt = ["credential", "validator"]
3 changes: 3 additions & 0 deletions identity_credential/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,6 @@ pub mod validator;

pub use error::Error;
pub use error::Result;

#[cfg(feature = "sd-jwt")]
pub use sd_jwt_payload;
Loading
Loading