From 1106490bc3afd6d7180c89fc9553cc4cf85461fa Mon Sep 17 00:00:00 2001 From: Abdulrahim Al Methiab Date: Mon, 15 Jan 2024 17:08:28 +0100 Subject: [PATCH] wasm bindings for validator --- .../wasm/examples/src/1_advanced/6_sd_jwt.ts | 200 ++++++++++++++++++ bindings/wasm/examples/src/main.ts | 3 + bindings/wasm/src/credential/credential.rs | 23 ++ .../kb_validation_options.rs | 83 ++++++++ .../jwt_credential_validation/mod.rs | 2 + .../sd_jwt_validator.rs | 41 ++-- bindings/wasm/src/error.rs | 3 +- bindings/wasm/src/iota/iota_document.rs | 32 +++ bindings/wasm/src/sd_jwt/disclosure.rs | 4 +- bindings/wasm/src/sd_jwt/encoder.rs | 10 +- .../wasm/src/sd_jwt/key_binding_jwt_claims.rs | 12 +- examples/1_advanced/7_sd_jwt.rs | 11 +- examples/Cargo.toml | 2 +- identity_credential/Cargo.toml | 2 +- .../src/validator/sd_jwt/validator.rs | 4 +- identity_iota/src/lib.rs | 1 + identity_storage/src/storage/tests/kb_jwt.rs | 12 +- 17 files changed, 410 insertions(+), 35 deletions(-) create mode 100644 bindings/wasm/examples/src/1_advanced/6_sd_jwt.ts create mode 100644 bindings/wasm/src/credential/jwt_credential_validation/kb_validation_options.rs diff --git a/bindings/wasm/examples/src/1_advanced/6_sd_jwt.ts b/bindings/wasm/examples/src/1_advanced/6_sd_jwt.ts new file mode 100644 index 0000000000..ff96f5f064 --- /dev/null +++ b/bindings/wasm/examples/src/1_advanced/6_sd_jwt.ts @@ -0,0 +1,200 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { + Credential, + DecodedJwtCredential, + EdDSAJwsVerifier, + FailFast, + JwkMemStore, + JwsSignatureOptions, + JwsVerificationOptions, + JwtCredentialValidationOptions, + KeyBindingJwtClaims, + KeyBindingJWTValidationOptions, + KeyIdMemStore, + SdJwt, + SdJwtCredentialValidator, + SdObjectEncoder, + Storage, + Timestamp, +} from "@iota/identity-wasm/node"; +import { Client, MnemonicSecretManager, Utils } from "@iota/sdk-wasm/node"; +import { API_ENDPOINT, createDid } from "../util"; + +/** + * Demonstrates 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-07.html). + */ +export async function sdJwt() { + // =========================================================================== + // Step 1: Create identities for the issuer and the holder. + // =========================================================================== + + const client = new Client({ + primaryNode: API_ENDPOINT, + localPow: true, + }); + + // Creates a new wallet and identity (see "0_create_did" example). + const issuerSecretManager: MnemonicSecretManager = { + mnemonic: Utils.generateMnemonic(), + }; + const issuerStorage: Storage = new Storage( + new JwkMemStore(), + new KeyIdMemStore(), + ); + let { document: issuerDocument, fragment: issuerFragment } = await createDid( + client, + issuerSecretManager, + issuerStorage, + ); + + // Create an identity for the holder, in this case also the subject. + const aliceSecretManager: MnemonicSecretManager = { + mnemonic: Utils.generateMnemonic(), + }; + const aliceStorage: Storage = new Storage( + new JwkMemStore(), + new KeyIdMemStore(), + ); + let { document: aliceDocument, fragment: aliceFragment } = await createDid( + client, + aliceSecretManager, + aliceStorage, + ); + + // =========================================================================== + // Step 2: Issuer creates and signs a selectively disclosable JWT verifiable credential. + // =========================================================================== + + // Create an address credential subject. + const subject = { + id: aliceDocument.id(), + name: "Alice", + address: { + locality: "Maxstadt", + postal_code: "12344", + country: "DE", + street_address: "Weidenstraße 22" + } + }; + + // Build credential using subject above and issuer. + const credential = new Credential({ + id: "https://example.com/credentials/3732", + type: "AddressCredential", + issuer: issuerDocument.id(), + credentialSubject: subject, + }); + + + // In Order to create an selective disclosure JWT, the plain text JWT + // claims set must be created first. + let payload = credential.toJwtClaims(); + + // 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-payload . + let encoder = new SdObjectEncoder(payload); + + // Make "locality", "postal_code" and "street_address" selectively disclosable while keeping + // other properties in plain text. + let disclosures = [ + encoder.conceal(["vc", "credentialSubject", "address", "locality"]), + encoder.conceal(["vc", "credentialSubject", "address", "postal_code"]), + encoder.conceal(["vc", "credentialSubject", "address", "street_address"]), + ]; + encoder.addSdAlgProperty(); + const encodedPayload = encoder.encodeToString(); + + // Create the signed JWT. + let jws = await issuerDocument.createJws(issuerStorage, issuerFragment, encodedPayload, new JwsSignatureOptions()); + + // =========================================================================== + // 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. + const strDisclosures = disclosures.map(disclosure => disclosure.toEncodedString()); + + let sdJwt = new SdJwt(jws.toString(), strDisclosures, undefined).presentation(); + + console.log(sdJwt) + + // =========================================================================== + // Step 4: Verifier sends the holder a challenge and requests a signed Verifiable Presentation. + // =========================================================================== + const VERIFIER_DID = "did:example:verifier"; + // A unique random challenge generated by the requester per presentation can mitigate replay attacks. + let nonce = "475a7984-1bb5-4c4c-a56f-822bccd46440"; + + // =========================================================================== + // Step 5: Holder creates an SD-JWT to be presented to a verifier. + // =========================================================================== + + const sdJwtReceived = SdJwt.parse(sdJwt); + + // The holder only wants to present "locality" and "postal_code" but not "street_address". + const toBeDisclosed = [ + strDisclosures[0], + strDisclosures[1] + ] + + // 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`. + const bindingClaims = new KeyBindingJwtClaims( + sdJwtReceived.jwt(), + toBeDisclosed, + nonce, + VERIFIER_DID, + Timestamp.nowUTC(), + ); + + // Setting the `typ` in the header is required. + const options = new JwsSignatureOptions({ + typ: KeyBindingJwtClaims.keyBindingJwtHeaderTyp() + }) + const KbJwt = await aliceDocument.createJws(aliceStorage, aliceFragment, bindingClaims.toString(), options); + + // Create the final SD-JWT. + let finalSdJwt = new SdJwt(sdJwtReceived.jwt().toString(), toBeDisclosed, KbJwt.toString()); + + // =========================================================================== + // Step 6: Holder presents the SD-JWT to the verifier. + // =========================================================================== + + let sdJwtPresentation = finalSdJwt.presentation(); + + // =========================================================================== + // Step 7: Verifier receives the SD-JWT and verifies it. + // =========================================================================== + + const sdJwtObj = SdJwt.parse(sdJwtPresentation); + + // Verify the JWT. + let validator = new SdJwtCredentialValidator(new EdDSAJwsVerifier()); + let decodedCredential: DecodedJwtCredential = validator.validateCredential( + sdJwtObj, + issuerDocument, + new JwtCredentialValidationOptions(), + FailFast.FirstError + ); + + console.log("JWT successfully validated") + console.log("credentialjwt validation", decodedCredential.credential()); + + // Verify the Key Binding JWT. + let kbValidationOptions = new KeyBindingJWTValidationOptions( + { + aud: VERIFIER_DID, + nonce: nonce, + jwsOptions: new JwsVerificationOptions() + + }); + validator.validateKeyBindingJwt(sdJwtObj, aliceDocument, kbValidationOptions); + + console.log("Key Binding JWT successfully validated"); +} diff --git a/bindings/wasm/examples/src/main.ts b/bindings/wasm/examples/src/main.ts index a49cd0f9dd..bf71211e3e 100644 --- a/bindings/wasm/examples/src/main.ts +++ b/bindings/wasm/examples/src/main.ts @@ -15,6 +15,7 @@ import { nftOwnsDid } from "./1_advanced/2_nft_owns_did"; import { didIssuesTokens } from "./1_advanced/3_did_issues_tokens"; import { customResolution } from "./1_advanced/4_custom_resolution"; import { domainLinkage } from "./1_advanced/5_domain_linkage"; +import { sdJwt } from "./1_advanced/6_sd_jwt"; async function main() { // Extract example name. @@ -52,6 +53,8 @@ async function main() { return await customResolution(); case "5_domain_linkage": return await domainLinkage(); + case "6_sd_jwt": + return await sdJwt(); default: throw "Unknown example name: '" + argument + "'"; } diff --git a/bindings/wasm/src/credential/credential.rs b/bindings/wasm/src/credential/credential.rs index aeef53a25c..98158bd97e 100644 --- a/bindings/wasm/src/credential/credential.rs +++ b/bindings/wasm/src/credential/credential.rs @@ -6,11 +6,14 @@ use identity_iota::core::Object; use identity_iota::credential::Credential; use identity_iota::credential::CredentialBuilder; use identity_iota::credential::DomainLinkageCredentialBuilder; +use serde_json::Value; +use std::collections::BTreeMap; use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; use crate::common::ArrayString; use crate::common::MapStringAny; +use crate::common::RecordStringAny; use crate::common::WasmTimestamp; use crate::credential::domain_linkage_credential_builder::IDomainLinkageCredential; use crate::credential::ArrayContext; @@ -215,6 +218,26 @@ impl WasmCredential { pub fn set_proof(&mut self, proof: Option) { self.0.set_proof(proof.map(|wasm_proof| wasm_proof.0)) } + + /// Serializes the `Credential` as a JWT claims set + /// in accordance with [VC Data Model v1.1](https://www.w3.org/TR/vc-data-model/#json-web-token). + /// + /// The resulting object can be used as the payload of a JWS when issuing the credential. + #[wasm_bindgen(js_name = "toJwtClaims")] + pub fn to_jwt_claims(&self, custom_claims: Option) -> Result { + let serialized: String = if let Some(object) = custom_claims { + let object: BTreeMap = object.into_serde().wasm_result()?; + self.0.serialize_jwt(Some(object)).wasm_result()? + } else { + self.0.serialize_jwt(None).wasm_result()? + }; + let serialized: BTreeMap = serde_json::from_str(&serialized).wasm_result()?; + Ok( + JsValue::from_serde(&serialized) + .wasm_result()? + .unchecked_into::(), + ) + } } impl_wasm_json!(WasmCredential, Credential); diff --git a/bindings/wasm/src/credential/jwt_credential_validation/kb_validation_options.rs b/bindings/wasm/src/credential/jwt_credential_validation/kb_validation_options.rs new file mode 100644 index 0000000000..aea8dfe247 --- /dev/null +++ b/bindings/wasm/src/credential/jwt_credential_validation/kb_validation_options.rs @@ -0,0 +1,83 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::Result; +use crate::error::WasmResult; +use identity_iota::credential::KeyBindingJWTValidationOptions; +use wasm_bindgen::prelude::*; + +/// Options to declare validation criteria when validating credentials. +#[wasm_bindgen(js_name = KeyBindingJWTValidationOptions)] +pub struct WasmKeyBindingJWTValidationOptions(pub(crate) KeyBindingJWTValidationOptions); + +#[wasm_bindgen(js_class = KeyBindingJWTValidationOptions)] +impl WasmKeyBindingJWTValidationOptions { + #[wasm_bindgen(constructor)] + pub fn new(options: Option) -> Result { + if let Some(opts) = options { + let options: KeyBindingJWTValidationOptions = opts.into_serde().wasm_result()?; + Ok(WasmKeyBindingJWTValidationOptions::from(options)) + } else { + Ok(WasmKeyBindingJWTValidationOptions::from( + KeyBindingJWTValidationOptions::default(), + )) + } + } +} + +impl_wasm_json!(WasmKeyBindingJWTValidationOptions, KeyBindingJWTValidationOptions); +impl_wasm_clone!(WasmKeyBindingJWTValidationOptions, KeyBindingJWTValidationOptions); + +impl From for WasmKeyBindingJWTValidationOptions { + fn from(options: KeyBindingJWTValidationOptions) -> Self { + Self(options) + } +} + +impl From for KeyBindingJWTValidationOptions { + fn from(options: WasmKeyBindingJWTValidationOptions) -> Self { + options.0 + } +} + +// Interface to allow creating `KeyBindingJWTValidationOptions` easily. +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "IKeyBindingJWTValidationOptions")] + pub type IKeyBindingJWTValidationOptions; +} + +#[wasm_bindgen(typescript_custom_section)] +const I_KEY_BINDING_JWT_VALIDATION_OPTIONS: &'static str = r#" +/** Holds options to create a new `KeyBindingJWTValidationOptions`. */ +interface IKeyBindingJWTValidationOptions { + /** + * Validates the nonce value of the KB-JWT claims. + */ + readonly nonce?: string; + + /** + * Validates the `aud` properties in the KB-JWT claims. + */ + readonly aud?: string; + + /** + * Options which affect the verification of the signature on the KB-JWT. + */ + readonly jwsOptions: JwsVerificationOptions; + + /** + * Declares that the KB-JWT is considered invalid if the `iat` value in the claims + * is earlier than this timestamp. + */ + readonly earliestIssuanceDate?: Timestamp; + + /** + * Declares that the KB-JWT is considered invalid if the `iat` value in the claims is + * later than this timestamp. + * + * Uses the current timestamp during validation if not set. + */ + readonly latestIssuanceDate?: Timestamp; + +}"#; diff --git a/bindings/wasm/src/credential/jwt_credential_validation/mod.rs b/bindings/wasm/src/credential/jwt_credential_validation/mod.rs index c4677f27f4..826d0388d9 100644 --- a/bindings/wasm/src/credential/jwt_credential_validation/mod.rs +++ b/bindings/wasm/src/credential/jwt_credential_validation/mod.rs @@ -3,12 +3,14 @@ mod decoded_jwt_credential; mod jwt_credential_validator; +mod kb_validation_options; mod options; mod sd_jwt_validator; mod unknown_credential; pub use self::decoded_jwt_credential::*; pub use self::jwt_credential_validator::*; +pub use self::kb_validation_options::*; pub use self::options::*; pub use self::sd_jwt_validator::*; pub use self::unknown_credential::*; diff --git a/bindings/wasm/src/credential/jwt_credential_validation/sd_jwt_validator.rs b/bindings/wasm/src/credential/jwt_credential_validation/sd_jwt_validator.rs index c4a5ab149f..812b25414b 100644 --- a/bindings/wasm/src/credential/jwt_credential_validation/sd_jwt_validator.rs +++ b/bindings/wasm/src/credential/jwt_credential_validation/sd_jwt_validator.rs @@ -1,32 +1,24 @@ // Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use identity_iota::core::Object; -use identity_iota::core::Url; -use identity_iota::credential::SdJwtCredentialValidator; -use identity_iota::credential::StatusCheck; -use identity_iota::did::CoreDID; -use identity_iota::sd_jwt_payload::SdObjectDecoder; - use super::options::WasmJwtCredentialValidationOptions; +use super::WasmKeyBindingJWTValidationOptions; use crate::common::ImportedDocumentLock; use crate::common::ImportedDocumentReadGuard; -use crate::common::WasmTimestamp; -use crate::credential::options::WasmStatusCheck; -use crate::credential::WasmCredential; use crate::credential::WasmDecodedJwtCredential; use crate::credential::WasmFailFast; -use crate::credential::WasmJwt; -use crate::credential::WasmSubjectHolderRelationship; use crate::did::ArrayIToCoreDocument; use crate::did::IToCoreDocument; -use crate::did::WasmCoreDID; use crate::did::WasmJwsVerificationOptions; use crate::error::Result; use crate::error::WasmResult; +use crate::sd_jwt::WasmKeyBindingJwtClaims; use crate::sd_jwt::WasmSdJwt; use crate::verification::IJwsVerifier; use crate::verification::WasmJwsVerifier; +use identity_iota::credential::SdJwtCredentialValidator; +use identity_iota::sd_jwt_payload::KeyBindingJwtClaims; +use identity_iota::sd_jwt_payload::SdObjectDecoder; use wasm_bindgen::prelude::*; @@ -131,4 +123,27 @@ impl WasmSdJwtCredentialValidator { .wasm_result() .map(WasmDecodedJwtCredential) } + + /// Validates a Key Binding JWT (KB-JWT) according to `https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-07.html#name-key-binding-jwt`. + /// The Validation process includes: + /// * Signature validation using public key materials defined in the `holder` document. + /// * `typ` value in KB-JWT header. + /// * `sd_hash` claim value in the KB-JWT claim. + /// * Optional `nonce`, `aud` and issuance date validation. + #[wasm_bindgen(js_name = validateKeyBindingJwt)] + #[allow(non_snake_case)] + pub fn validate_key_binding_jwt( + &self, + sdJwt: &WasmSdJwt, + holder: &IToCoreDocument, + options: &WasmKeyBindingJWTValidationOptions, + ) -> Result { + let holder_lock = ImportedDocumentLock::from(holder); + let holder_guard = holder_lock.try_read()?; + let claims: KeyBindingJwtClaims = self + .0 + .validate_key_binding_jwt(&sdJwt.0, &holder_guard, &options.0) + .wasm_result()?; + Ok(WasmKeyBindingJwtClaims(claims)) + } } diff --git a/bindings/wasm/src/error.rs b/bindings/wasm/src/error.rs index 72c0fd4765..44fc571946 100644 --- a/bindings/wasm/src/error.rs +++ b/bindings/wasm/src/error.rs @@ -105,7 +105,8 @@ impl_wasm_error_from!( identity_iota::credential::RevocationError, identity_iota::verification::Error, identity_iota::credential::DomainLinkageValidationError, - identity_iota::sd_jwt_payload::Error + identity_iota::sd_jwt_payload::Error, + identity_iota::credential::KeyBindingJwtError ); // Similar to `impl_wasm_error_from`, but uses the types name instead of requiring/calling Into &'static str diff --git a/bindings/wasm/src/iota/iota_document.rs b/bindings/wasm/src/iota/iota_document.rs index e01c96a90b..8f8cbe6823 100644 --- a/bindings/wasm/src/iota/iota_document.rs +++ b/bindings/wasm/src/iota/iota_document.rs @@ -704,7 +704,39 @@ impl WasmIotaDocument { /// /// Upon success a string representing a JWS encoded according to the Compact JWS Serialization format is returned. /// See [RFC7515 section 3.1](https://www.rfc-editor.org/rfc/rfc7515#section-3.1). + /// + /// @deprecated Use `createJws()` instead. + #[deprecated] #[wasm_bindgen(js_name = createJwt)] + pub fn create_jwt( + &self, + storage: &WasmStorage, + fragment: String, + payload: String, + options: &WasmJwsSignatureOptions, + ) -> Result { + let storage_clone: Rc = storage.0.clone(); + let options_clone: JwsSignatureOptions = options.0.clone(); + let document_lock_clone: Rc = self.0.clone(); + let promise: Promise = future_to_promise(async move { + document_lock_clone + .read() + .await + .create_jws(&storage_clone, &fragment, payload.as_bytes(), &options_clone) + .await + .wasm_result() + .map(WasmJws::new) + .map(JsValue::from) + }); + Ok(promise.unchecked_into()) + } + + /// Sign the `payload` according to `options` with the storage backed private key corresponding to the public key + /// material in the verification method identified by the given `fragment. + /// + /// Upon success a string representing a JWS encoded according to the Compact JWS Serialization format is returned. + /// See [RFC7515 section 3.1](https://www.rfc-editor.org/rfc/rfc7515#section-3.1). + #[wasm_bindgen(js_name = createJws)] pub fn create_jws( &self, storage: &WasmStorage, diff --git a/bindings/wasm/src/sd_jwt/disclosure.rs b/bindings/wasm/src/sd_jwt/disclosure.rs index 1a1e19bfed..1e350415e3 100644 --- a/bindings/wasm/src/sd_jwt/disclosure.rs +++ b/bindings/wasm/src/sd_jwt/disclosure.rs @@ -42,8 +42,8 @@ impl WasmDisclosure { } /// Returns a copy of the base64url-encoded string. - #[wasm_bindgen(js_name = toString)] - pub fn to_string(&self) -> String { + #[wasm_bindgen(js_name = toEncodedString)] + pub fn to_encoded_string(&self) -> String { self.0.disclosure.clone() } diff --git a/bindings/wasm/src/sd_jwt/encoder.rs b/bindings/wasm/src/sd_jwt/encoder.rs index 0dbd8e5a74..3aedde9c4b 100644 --- a/bindings/wasm/src/sd_jwt/encoder.rs +++ b/bindings/wasm/src/sd_jwt/encoder.rs @@ -83,6 +83,12 @@ impl WasmSdObjectEncoder { self.0.add_sd_alg_property(); } + /// Returns the modified object as a string. + #[wasm_bindgen(js_name = encodeToString)] + pub fn encoded_to_string(&self) -> Result { + self.0.try_to_string().wasm_result() + } + /// Returns the modified object as a string. #[wasm_bindgen(js_name = toString)] pub fn to_string(&self) -> Result { @@ -90,8 +96,8 @@ impl WasmSdObjectEncoder { } /// Returns the modified object. - #[wasm_bindgen(js_name = encodedObject)] - pub fn encoded_object(&self) -> Result { + #[wasm_bindgen(js_name = encodeToObject)] + pub fn encode_to_object(&self) -> Result { Ok( JsValue::from_serde(&self.0.object()) .wasm_result()? diff --git a/bindings/wasm/src/sd_jwt/key_binding_jwt_claims.rs b/bindings/wasm/src/sd_jwt/key_binding_jwt_claims.rs index 7e90cdb8e5..45e722526e 100644 --- a/bindings/wasm/src/sd_jwt/key_binding_jwt_claims.rs +++ b/bindings/wasm/src/sd_jwt/key_binding_jwt_claims.rs @@ -6,6 +6,7 @@ use crate::common::RecordStringAny; use crate::common::WasmTimestamp; use crate::error::Result; use crate::error::WasmResult; +use identity_iota::core::Timestamp; use identity_iota::core::ToJson; use identity_iota::sd_jwt_payload::KeyBindingJwtClaims; use identity_iota::sd_jwt_payload::Sha256Hasher; @@ -46,7 +47,9 @@ impl WasmKeyBindingJwtClaims { disclosures, nonce, aud, - issued_at.map(|value| value.0.to_unix()), + issued_at + .map(|value| value.0.to_unix()) + .unwrap_or(Timestamp::now_utc().to_unix()), ); if let Some(custom_properties) = custom_properties { let custom_properties: BTreeMap = custom_properties.into_serde().wasm_result()?; @@ -94,6 +97,13 @@ impl WasmKeyBindingJwtClaims { .unchecked_into::(), ) } + + /// Returns the value of the `typ` property of the JWT header according to + /// https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-07.html#name-key-binding-jwt + #[wasm_bindgen(js_name = keyBindingJwtHeaderTyp)] + pub fn header_type() -> String { + KeyBindingJwtClaims::KB_JWT_HEADER_TYP.to_string() + } } impl_wasm_json!(WasmKeyBindingJwtClaims, KeyBindingJwtClaims); diff --git a/examples/1_advanced/7_sd_jwt.rs b/examples/1_advanced/7_sd_jwt.rs index 0c07531861..50fb5d87d2 100644 --- a/examples/1_advanced/7_sd_jwt.rs +++ b/examples/1_advanced/7_sd_jwt.rs @@ -2,7 +2,7 @@ // 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). +//! using the standard [Selective Disclosure for JWTs (SD-JWT)](https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-07.html). //! //! cargo run --release --example 7_sd_jwt @@ -15,6 +15,7 @@ use identity_eddsa_verifier::EdDSAJwsVerifier; use identity_iota::core::json; use identity_iota::core::FromJson; use identity_iota::core::Object; +use identity_iota::core::Timestamp; use identity_iota::core::ToJson; use identity_iota::core::Url; use identity_iota::credential::Credential; @@ -78,7 +79,7 @@ async fn main() -> anyhow::Result<()> { // Step 2: Issuer creates and signs a selectively disclosable JWT verifiable credential. // =========================================================================== - // Create a credential subject indicating the degree earned by Alice. + // Create an address credential subject. let subject: Subject = Subject::from_json_value(json!({ "id": alice_document.id().as_str(), "name": "Alice", @@ -92,7 +93,7 @@ async fn main() -> anyhow::Result<()> { // Build credential using subject above and issuer. let credential: Credential = CredentialBuilder::default() - .id(Url::parse("https://example.edu/credentials/3732")?) + .id(Url::parse("https://example.com/credentials/3732")?) .issuer(Url::parse(issuer_document.id().as_str())?) .type_("AddressCredential") .subject(subject) @@ -105,7 +106,7 @@ async fn main() -> anyhow::Result<()> { // 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 . + // Read more in https://github.com/iotaledger/sd-jwt-payload . let mut encoder = SdObjectEncoder::new(&payload)?; // Make "locality", "postal_code" and "street_address" selectively disclosable while keeping // other properties in plain text. @@ -170,7 +171,7 @@ async fn main() -> anyhow::Result<()> { disclosures.clone(), nonce.to_string(), VERIFIER_DID.to_string(), - None, + Timestamp::now_utc().to_unix(), ) .to_json()?; diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 5829e0c995..7651e67de9 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -13,7 +13,7 @@ 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"] } +sd-jwt-payload = { version = "0.1.2", default-features = false, features = ["sha"] } serde_json = { version = "1.0", default-features = false } tokio = { version = "1.29", default-features = false, features = ["rt"] } diff --git a/identity_credential/Cargo.toml b/identity_credential/Cargo.toml index 0ba72f2a5c..b6137a57d6 100644 --- a/identity_credential/Cargo.toml +++ b/identity_credential/Cargo.toml @@ -24,7 +24,7 @@ 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"] } +sd-jwt-payload = { version = "0.1.2", default-features = false, features = ["sha"] } serde.workspace = true serde_json.workspace = true serde_repr = { version = "0.1", default-features = false, optional = true } diff --git a/identity_credential/src/validator/sd_jwt/validator.rs b/identity_credential/src/validator/sd_jwt/validator.rs index 3cbbb867d5..0eedf13bf5 100644 --- a/identity_credential/src/validator/sd_jwt/validator.rs +++ b/identity_credential/src/validator/sd_jwt/validator.rs @@ -161,11 +161,11 @@ impl SdJwtCredentialValidator { Ok(decoded_credential) } - /// Validates a Key Binding JWT (KB-JWT) according to `https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-06.html#name-key-binding-jwt`. + /// Validates a Key Binding JWT (KB-JWT) according to `https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-07.html#name-key-binding-jwt`. /// The Validation process includes: /// * Signature validation using public key materials defined in the `holder` document. /// * `typ` value in KB-JWT header. - /// * `_sd_hash` claim value in the KB-JWT claim. + /// * `sd_hash` claim value in the KB-JWT claim. /// * Optional `nonce`, `aud` and issuance date validation. pub fn validate_key_binding_jwt( &self, diff --git a/identity_iota/src/lib.rs b/identity_iota/src/lib.rs index 117bcd6c54..24a20359eb 100644 --- a/identity_iota/src/lib.rs +++ b/identity_iota/src/lib.rs @@ -110,5 +110,6 @@ pub mod storage { #[cfg(feature = "sd-jwt")] pub mod sd_jwt_payload { + //! Expose the selective disclosure crate. pub use identity_credential::sd_jwt_payload::*; } diff --git a/identity_storage/src/storage/tests/kb_jwt.rs b/identity_storage/src/storage/tests/kb_jwt.rs index dde89d10d7..5ea27ed5b9 100644 --- a/identity_storage/src/storage/tests/kb_jwt.rs +++ b/identity_storage/src/storage/tests/kb_jwt.rs @@ -90,7 +90,7 @@ async fn setup_test() -> (Setup, Credential, SdJwt) disclosures.clone(), NONCE.to_string(), VERIFIER_ID.to_string(), - None, + Timestamp::now_utc().to_unix(), ) .to_json() .unwrap(); @@ -184,12 +184,10 @@ async fn kb_in_the_future() { sd_jwt.disclosures.clone(), NONCE.to_string(), VERIFIER_ID.to_string(), - Some( - Timestamp::now_utc() - .checked_add(Duration::seconds(30)) - .unwrap() - .to_unix(), - ), + Timestamp::now_utc() + .checked_add(Duration::seconds(30)) + .unwrap() + .to_unix(), ) .to_json() .unwrap();