From ce8e316668cfb4765d974bd8d15bafe42ba8c92b Mon Sep 17 00:00:00 2001 From: umr1352 Date: Tue, 17 Dec 2024 14:21:13 +0100 Subject: [PATCH 01/11] reworked sd-jwt bindings --- bindings/wasm/Cargo.toml | 4 +- bindings/wasm/package.json | 2 +- bindings/wasm/src/error.rs | 21 ++- bindings/wasm/src/lib.rs | 2 +- bindings/wasm/src/sd_jwt_vc/claims.rs | 0 bindings/wasm/src/sd_jwt_vc/mod.rs | 7 + bindings/wasm/src/sd_jwt_vc/resolver.rs | 26 ++++ .../src/sd_jwt_vc/sd_jwt_v2/disclosure.rs | 60 ++++++++ .../wasm/src/sd_jwt_vc/sd_jwt_v2/hasher.rs | 43 ++++++ .../wasm/src/sd_jwt_vc/sd_jwt_v2/kb_jwt.rs | 127 ++++++++++++++++ bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/mod.rs | 11 ++ .../wasm/src/sd_jwt_vc/sd_jwt_v2/sd_jwt.rs | 136 ++++++++++++++++++ .../wasm/src/sd_jwt_vc/sd_jwt_v2/signer.rs | 45 ++++++ bindings/wasm/src/sd_jwt_vc/token.rs | 53 +++++++ .../src/sd_jwt_vc/metadata/vc_type.rs | 8 +- identity_credential/src/sd_jwt_vc/mod.rs | 3 +- identity_credential/src/sd_jwt_vc/resolver.rs | 9 +- identity_credential/src/sd_jwt_vc/token.rs | 3 - identity_iota/Cargo.toml | 3 + identity_iota/src/lib.rs | 7 + 20 files changed, 555 insertions(+), 15 deletions(-) create mode 100644 bindings/wasm/src/sd_jwt_vc/claims.rs create mode 100644 bindings/wasm/src/sd_jwt_vc/mod.rs create mode 100644 bindings/wasm/src/sd_jwt_vc/resolver.rs create mode 100644 bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/disclosure.rs create mode 100644 bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/hasher.rs create mode 100644 bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/kb_jwt.rs create mode 100644 bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/mod.rs create mode 100644 bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/sd_jwt.rs create mode 100644 bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/signer.rs create mode 100644 bindings/wasm/src/sd_jwt_vc/token.rs diff --git a/bindings/wasm/Cargo.toml b/bindings/wasm/Cargo.toml index 8406b386b2..6f2fafc2d2 100644 --- a/bindings/wasm/Cargo.toml +++ b/bindings/wasm/Cargo.toml @@ -16,6 +16,7 @@ description = "Web Assembly bindings for the identity-rs crate." crate-type = ["cdylib", "rlib"] [dependencies] +anyhow = { version = "1.0.94", features = ["std"] } async-trait = { version = "0.1", default-features = false } bls12_381_plus = "0.8.17" console_error_panic_hook = { version = "0.1" } @@ -26,6 +27,7 @@ js-sys = { version = "0.3.61" } json-proof-token = "0.3.4" proc_typescript = { version = "0.1.0", path = "./proc_typescript" } serde = { version = "1.0", features = ["derive"] } +serde-wasm-bindgen = "0.6.5" serde_json = { version = "1.0", default-features = false } serde_repr = { version = "0.1", default-features = false } # Want to use the nice API of tokio::sync::RwLock for now even though we can't use threads. @@ -37,7 +39,7 @@ zkryptium = "0.2.2" [dependencies.identity_iota] path = "../../identity_iota" default-features = false -features = ["client", "revocation-bitmap", "resolver", "domain-linkage", "sd-jwt", "status-list-2021", "jpt-bbs-plus"] +features = ["client", "revocation-bitmap", "resolver", "domain-linkage", "sd-jwt", "status-list-2021", "jpt-bbs-plus", "sd-jwt-vc"] [dev-dependencies] rand = "0.8.5" diff --git a/bindings/wasm/package.json b/bindings/wasm/package.json index 1673750e23..801aab239a 100644 --- a/bindings/wasm/package.json +++ b/bindings/wasm/package.json @@ -11,7 +11,7 @@ }, "scripts": { "build:src": "cargo build --lib --release --target wasm32-unknown-unknown", - "bundle:nodejs": "wasm-bindgen target/wasm32-unknown-unknown/release/identity_wasm.wasm --typescript --weak-refs --target nodejs --out-dir node && node ./build/node && tsc --project ./lib/tsconfig.json && node ./build/replace_paths ./lib/tsconfig.json node", + "bundle:nodejs": "~/.cargo/bin/wasm-bindgen target/wasm32-unknown-unknown/release/identity_wasm.wasm --typescript --weak-refs --target nodejs --out-dir node && node ./build/node && tsc --project ./lib/tsconfig.json && node ./build/replace_paths ./lib/tsconfig.json node", "bundle:web": "wasm-bindgen target/wasm32-unknown-unknown/release/identity_wasm.wasm --typescript --weak-refs --target web --out-dir web && node ./build/web && tsc --project ./lib/tsconfig.web.json && node ./build/replace_paths ./lib/tsconfig.web.json web", "build:nodejs": "npm run build:src && npm run bundle:nodejs && wasm-opt -O node/identity_wasm_bg.wasm -o node/identity_wasm_bg.wasm", "build:web": "npm run build:src && npm run bundle:web && wasm-opt -O web/identity_wasm_bg.wasm -o web/identity_wasm_bg.wasm", diff --git a/bindings/wasm/src/error.rs b/bindings/wasm/src/error.rs index 34d4c98d8b..3bb51a7002 100644 --- a/bindings/wasm/src/error.rs +++ b/bindings/wasm/src/error.rs @@ -108,7 +108,8 @@ impl_wasm_error_from!( identity_iota::sd_jwt_payload::Error, identity_iota::credential::KeyBindingJwtError, identity_iota::credential::status_list_2021::StatusListError, - identity_iota::credential::status_list_2021::StatusList2021CredentialError + identity_iota::credential::status_list_2021::StatusList2021CredentialError, + identity_iota::sd_jwt_rework::Error ); // Similar to `impl_wasm_error_from`, but uses the types name instead of requiring/calling Into &'static str @@ -184,6 +185,15 @@ impl From for WasmError<'_> { } } +impl From for WasmError<'_> { + fn from(value: serde_wasm_bindgen::Error) -> Self { + Self { + name: Cow::Borrowed("JSConversionError"), + message: Cow::Owned(value.to_string()), + } + } +} + impl From for WasmError<'_> { fn from(error: identity_iota::credential::CompoundCredentialValidationError) -> Self { Self { @@ -265,6 +275,15 @@ impl From for WasmError<'_> { } } +impl From for WasmError<'_> { + fn from(error: identity_iota::credential::sd_jwt_vc::Error) -> Self { + Self { + name: Cow::Borrowed("SdJwtVcError"), + message: Cow::Owned(ErrorMessage(&error).to_string()), + } + } +} + /// Convenience struct to convert Result to errors in the Rust library. pub struct JsValueResult(pub(crate) Result); diff --git a/bindings/wasm/src/lib.rs b/bindings/wasm/src/lib.rs index cf8344925a..9e39ddb6d8 100644 --- a/bindings/wasm/src/lib.rs +++ b/bindings/wasm/src/lib.rs @@ -1,7 +1,6 @@ // Copyright 2020-2022 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -#![forbid(unsafe_code)] #![allow(deprecated)] #![allow(clippy::upper_case_acronyms)] // wasm_bindgen calls drop on non-Drop types. When/If this is fixed, this can be removed (no issue to link here yet). @@ -30,6 +29,7 @@ pub mod revocation; pub mod sd_jwt; pub mod storage; pub mod verification; +pub mod sd_jwt_vc; /// Initializes the console error panic hook for better error messages #[wasm_bindgen(start)] diff --git a/bindings/wasm/src/sd_jwt_vc/claims.rs b/bindings/wasm/src/sd_jwt_vc/claims.rs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/bindings/wasm/src/sd_jwt_vc/mod.rs b/bindings/wasm/src/sd_jwt_vc/mod.rs new file mode 100644 index 0000000000..2159f0d5cd --- /dev/null +++ b/bindings/wasm/src/sd_jwt_vc/mod.rs @@ -0,0 +1,7 @@ +mod token; +mod resolver; +mod claims; +pub mod sd_jwt_v2; + +pub use token::*; +pub use claims::*; \ No newline at end of file diff --git a/bindings/wasm/src/sd_jwt_vc/resolver.rs b/bindings/wasm/src/sd_jwt_vc/resolver.rs new file mode 100644 index 0000000000..efc384fa1e --- /dev/null +++ b/bindings/wasm/src/sd_jwt_vc/resolver.rs @@ -0,0 +1,26 @@ +use async_trait::async_trait; +use identity_iota::core::Url; +use identity_iota::credential::sd_jwt_vc::resolver::Error as ErrorR; +use identity_iota::credential::sd_jwt_vc::Resolver; +use js_sys::Uint8Array; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +extern "C" { + pub type ResolverUrlToU8Array; + + #[wasm_bindgen(structural, method, catch)] + pub async fn resolve(this: &ResolverUrlToU8Array, input: &str) -> Result; +} + + +#[async_trait(?Send)] +impl Resolver> for ResolverUrlToU8Array { + async fn resolve(&self, input: &Url) -> std::result::Result, ErrorR> { + self + .resolve(input.as_str()) + .await + .map(|arr| arr.to_vec()) + .map_err(|e| ErrorR::Generic(anyhow::anyhow!("{}", e.to_string()))) + } +} diff --git a/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/disclosure.rs b/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/disclosure.rs new file mode 100644 index 0000000000..5174af28b6 --- /dev/null +++ b/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/disclosure.rs @@ -0,0 +1,60 @@ +use identity_iota::sd_jwt_rework::Disclosure; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::JsValue; + +use crate::error::Result; +use crate::error::WasmResult; + +/// A disclosable value. +/// Both object properties and array elements disclosures are supported. +/// +/// See: https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-07.html#name-disclosures +#[derive(Clone)] +#[wasm_bindgen(js_name = DisclosureV2, getter_with_clone)] +pub struct WasmDisclosure { + pub salt: String, + #[wasm_bindgen(js_name = claimName)] + pub claim_name: Option, + #[wasm_bindgen(js_name = claimValue)] + pub claim_value: JsValue, + unparsed: String, +} + +#[wasm_bindgen(js_class = DisclosureV2)] +impl WasmDisclosure { + #[wasm_bindgen] + pub fn parse(s: &str) -> Result { + Disclosure::parse(s).map(Self::from).wasm_result() + } + + #[wasm_bindgen(js_name = toString)] + pub fn to_string(&self) -> String { + self.unparsed.clone() + } +} + +impl From for Disclosure { + fn from(value: WasmDisclosure) -> Self { + Disclosure::parse(&value.unparsed).expect("valid WasmDisclosure is a valid disclosure") + } +} + +impl From for WasmDisclosure { + fn from(value: Disclosure) -> Self { + let unparsed = value.to_string(); + let Disclosure { + salt, + claim_name, + claim_value, + .. + } = value; + let claim_value = serde_wasm_bindgen::to_value(&claim_value).expect("serde JSON Value is a valid JS Value"); + + Self { + salt, + claim_name, + claim_value, + unparsed, + } + } +} diff --git a/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/hasher.rs b/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/hasher.rs new file mode 100644 index 0000000000..5093d4b32f --- /dev/null +++ b/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/hasher.rs @@ -0,0 +1,43 @@ +use std::sync::OnceLock; + +use identity_iota::sd_jwt_rework::Hasher; +use wasm_bindgen::prelude::wasm_bindgen; + +#[wasm_bindgen(typescript_custom_section)] +const I_HASHER: &str = r#" +interface Hasher { + digest: (input: Uint8Array) => Uint8Array; + algName: () => string; + encodedDigest: (data: string) => string; +} +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "Hasher")] + pub type WasmHasher; + + #[wasm_bindgen(structural, method)] + pub fn digest(this: &WasmHasher, input: &[u8]) -> Vec; + + #[wasm_bindgen(structural, method, js_name = "algName")] + pub fn alg_name(this: &WasmHasher) -> String; + + #[wasm_bindgen(structural, method, js_name = "encodedDigest")] + pub fn encoded_digest(this: &WasmHasher, data: &str) -> String; +} + +impl Hasher for WasmHasher { + fn alg_name(&self) -> &str { + static ALG: OnceLock = OnceLock::new(); + ALG.get_or_init(|| self.alg_name()) + } + + fn digest(&self, input: &[u8]) -> Vec { + self.digest(input) + } + + fn encoded_digest(&self, disclosure: &str) -> String { + self.encoded_digest(disclosure) + } +} diff --git a/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/kb_jwt.rs b/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/kb_jwt.rs new file mode 100644 index 0000000000..8255b7fecd --- /dev/null +++ b/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/kb_jwt.rs @@ -0,0 +1,127 @@ +use identity_iota::credential; +use identity_iota::credential::sd_jwt_vc; +use identity_iota::sd_jwt_rework::KeyBindingJwt; +use identity_iota::sd_jwt_rework::KeyBindingJwtBuilder; +use identity_iota::sd_jwt_rework::Sha256Hasher; +use js_sys::Object; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::JsCast; +use wasm_bindgen::JsValue; + +use crate::error::Result; +use crate::error::WasmResult; + +use super::WasmJwsSigner; +use super::WasmSdJwt; + +#[wasm_bindgen(typescript_custom_section)] +const T_REQUIRED_KB: &str = r#" +type RequiredKeyBinding = { jwk: Jwk } + | { jwe: string } + | { kid: string } + | { jwu: { jwu: string, kid: string }} + | any; +"#; + +#[wasm_bindgen(typescript_custom_section)] +const I_KB_JWT_CLAIMS: &str = r#" +interface KeyBindingJwtClaimsV2 { + iat: number; + aud: string; + nonce: string; + sd_hash: string; + [properties: string]: unknown; +} +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "RequiredKeyBinding")] + pub type WasmRequiredKeyBinding; + + #[wasm_bindgen(typescript_type = "KeyBindingJwtClaimsV2")] + pub type WasmKeyBindingJwtClaims; +} + +#[wasm_bindgen(js_name = KeyBindingJwt)] +pub struct WasmKeyBindingJwt(pub(crate) KeyBindingJwt); + +#[wasm_bindgen(js_class = KeyBindingJwt)] +impl WasmKeyBindingJwt { + #[wasm_bindgen] + pub fn parse(s: &str) -> Result { + s.parse::() + .map_err(sd_jwt_vc::Error::from) + .map(WasmKeyBindingJwt) + .wasm_result() + } + + #[wasm_bindgen] + pub fn claims(&self) -> WasmKeyBindingJwtClaims { + serde_wasm_bindgen::to_value(self.0.claims()).unwrap().unchecked_into() + } + + #[wasm_bindgen(js_name = "toString")] + pub fn to_string(&self) -> String { + self.0.to_string() + } +} + +#[wasm_bindgen(js_name = KeyBindingJwtBuilder)] +pub struct WasmKeyBindingJwtBuilder(pub(crate) KeyBindingJwtBuilder); + +#[wasm_bindgen(js_class = KeyBindingJwtBuilder)] +impl WasmKeyBindingJwtBuilder { + #[wasm_bindgen(constructor)] + pub fn new() -> WasmKeyBindingJwtBuilder { + Self(KeyBindingJwtBuilder::default()) + } + + #[wasm_bindgen(js_name = "fromObject")] + pub fn from_object(obj: Object) -> Result { + serde_wasm_bindgen::from_value(obj.into()) + .map(KeyBindingJwtBuilder::from_object) + .map(Self) + .wasm_result() + } + + #[wasm_bindgen] + pub fn header(self, header: Object) -> Result { + serde_wasm_bindgen::from_value(header.into()) + .map(|obj| self.0.header(obj)) + .map(Self) + .wasm_result() + } + + #[wasm_bindgen] + pub fn iat(self, iat: i64) -> Self { + Self(self.0.iat(iat)) + } + + #[wasm_bindgen] + pub fn aud(self, aud: String) -> Self { + Self(self.0.aud(aud)) + } + + #[wasm_bindgen] + pub fn nonce(self, nonce: String) -> Self { + Self(self.0.nonce(nonce)) + } + + #[wasm_bindgen(js_name = "insertProperty")] + pub fn insert_property(self, name: String, value: JsValue) -> Result { + let value = serde_wasm_bindgen::from_value(value).wasm_result()?; + Ok(Self(self.0.insert_property(&name, value))) + } + + #[wasm_bindgen] + pub async fn finish(self, sd_jwt: &WasmSdJwt, alg: &str, signer: &WasmJwsSigner) -> Result { + self + .0 + .finish(&sd_jwt.0, &Sha256Hasher, alg, signer) + .await + .map(WasmKeyBindingJwt) + .map_err(|e| credential::Error::from(sd_jwt_vc::Error::SdJwt(e))) + .wasm_result() + } +} diff --git a/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/mod.rs b/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/mod.rs new file mode 100644 index 0000000000..afed902a7b --- /dev/null +++ b/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/mod.rs @@ -0,0 +1,11 @@ +mod sd_jwt; +mod kb_jwt; +mod signer; +mod hasher; +mod disclosure; + +pub use sd_jwt::*; +pub use kb_jwt::*; +pub use signer::*; +pub use hasher::*; +pub use disclosure::*; diff --git a/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/sd_jwt.rs b/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/sd_jwt.rs new file mode 100644 index 0000000000..7567482ddf --- /dev/null +++ b/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/sd_jwt.rs @@ -0,0 +1,136 @@ +use identity_iota::credential::sd_jwt_vc::Error; +use identity_iota::sd_jwt_rework::SdJwt; +use identity_iota::sd_jwt_rework::SdJwtPresentationBuilder; +use identity_iota::sd_jwt_rework::Sha256Hasher; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::JsCast; +use wasm_bindgen::JsValue; + +use crate::error::Result; +use crate::error::WasmResult; + +use super::WasmDisclosure; +use super::WasmHasher; +use super::WasmKeyBindingJwt; +use super::WasmRequiredKeyBinding; + +#[wasm_bindgen(typescript_custom_section)] +const I_SD_JWT_CLAIMS: &str = r#" +interface SdJwtClaims { + _sd: string[]; + _sd_alg: string | undefined; + cnf: RequiredKeyBinding | undefined; + [properties: string]: unknown; +} +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "SdJwtClaims")] + pub type WasmSdJwtClaims; +} + +#[derive(Clone)] +#[wasm_bindgen(js_name = SdJwtV2)] +pub struct WasmSdJwt(pub(crate) SdJwt); + +#[wasm_bindgen(js_class = SdJwtV2)] +impl WasmSdJwt { + #[wasm_bindgen] + pub fn parse(s: &str) -> Result { + SdJwt::parse(s).map(Self).map_err(|e| Error::from(e)).wasm_result() + } + + #[wasm_bindgen] + pub fn header(&self) -> JsValue { + serde_wasm_bindgen::to_value(self.0.header()).unwrap() + } + + #[wasm_bindgen] + pub fn claims(&self) -> Result { + serde_wasm_bindgen::to_value(self.0.claims()) + .wasm_result() + .map(JsCast::unchecked_into) + } + + #[wasm_bindgen] + pub fn disclosures(&self) -> Vec { + self.0.disclosures().iter().map(ToString::to_string).collect() + } + + #[wasm_bindgen(js_name = "requiredKeyBind")] + pub fn required_key_bind(&self) -> Option { + self.0.required_key_bind().map(|required_kb| { + serde_wasm_bindgen::to_value(required_kb) + .expect("RequiredKeyBinding can be turned into a JS value") + .unchecked_into() + }) + } + + /// Returns the JSON object obtained by replacing all disclosures into their + /// corresponding JWT concealable claims. + #[wasm_bindgen(js_name = "intoDisclosedObject")] + pub fn into_disclosed_object(self) -> Result { + self + .0 + .into_disclosed_object(&Sha256Hasher) + .map_err(Error::from) + .map(|obj| serde_wasm_bindgen::to_value(&obj).expect("obj can be turned into a JS value")) + .wasm_result() + } + + /// Serializes the components into the final SD-JWT. + #[wasm_bindgen] + pub fn presentation(&self) -> String { + self.0.presentation() + } +} + +#[wasm_bindgen(js_name = SdJwtPresentationBuilder)] +pub struct WasmSdJwtPresentationBuilder(pub(crate) SdJwtPresentationBuilder); + +#[wasm_bindgen(js_class = SdJwtPresentationBuilder)] +impl WasmSdJwtPresentationBuilder { + #[wasm_bindgen(constructor)] + pub fn new(sd_jwt: WasmSdJwt, hasher: &WasmHasher) -> Result { + SdJwtPresentationBuilder::new(sd_jwt.0, hasher).map(Self).wasm_result() + } + + /// Removes the disclosure for the property at `path`, concealing it. + /// + /// ## Notes + /// - When concealing a claim more than one disclosure may be removed: the disclosure for the claim itself and the + /// disclosures for any concealable sub-claim. + #[wasm_bindgen] + pub fn conceal(self, path: &str) -> Result { + self.0.conceal(path).map(Self).wasm_result() + } + + /// Adds a {@link KeyBindingJwt} to this {@link SdJwt}'s presentation. + #[wasm_bindgen(js_name = attachKeyBindingJwt)] + pub fn attach_key_binding_jwt(self, kb_jwt: WasmKeyBindingJwt) -> Self { + Self(self.0.attach_key_binding_jwt(kb_jwt.0)) + } + + /// Returns the resulting {@link SdJwt} together with all removed disclosures. + /// ## Errors + /// - Fails with `Error::MissingKeyBindingJwt` if this {@link SdJwt} requires a key binding but none was provided. + #[wasm_bindgen] + pub fn finish(self) -> Result { + self + .0 + .finish() + .map(|(sd_jwt, disclosures)| SdJwtPresentationResult { + sd_jwt: WasmSdJwt(sd_jwt), + disclosures: disclosures.into_iter().map(WasmDisclosure::from).collect(), + }) + .wasm_result() + } +} + +#[wasm_bindgen(inspectable, getter_with_clone)] +pub struct SdJwtPresentationResult { + #[wasm_bindgen(js_name = sdJwt)] + pub sd_jwt: WasmSdJwt, + pub disclosures: Vec, +} diff --git a/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/signer.rs b/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/signer.rs new file mode 100644 index 0000000000..3d5d518c81 --- /dev/null +++ b/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/signer.rs @@ -0,0 +1,45 @@ +use async_trait::async_trait; +use identity_iota::sd_jwt_rework::JsonObject; +use identity_iota::sd_jwt_rework::JwsSigner; +use js_sys::Error as JsError; +use js_sys::Object; +use js_sys::Uint8Array; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::JsCast; + +use crate::error::Result; + +#[wasm_bindgen(typescript_custom_section)] +const I_JWS_SIGNER: &str = r#" +interface JwsSigner { + sign: (header: any, payload: any) => Promise; +} +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "JwsSigner")] + pub type WasmJwsSigner; + + #[wasm_bindgen(structural, method, catch)] + pub async fn sign(this: &WasmJwsSigner, header: Object, payload: Object) -> Result; +} + +#[async_trait(?Send)] +impl JwsSigner for WasmJwsSigner { + type Error = String; + async fn sign(&self, header: &JsonObject, payload: &JsonObject) -> std::result::Result, Self::Error> { + let header = serde_wasm_bindgen::to_value(header) + .map_err(|e| format!("{e:?}"))? + .unchecked_into(); + let payload = serde_wasm_bindgen::to_value(payload) + .map_err(|e| format!("{e:?}"))? + .unchecked_into(); + + self + .sign(header, payload) + .await + .map_err(|e| e.unchecked_into::().to_string().into()) + .map(|arr| arr.to_vec()) + } +} diff --git a/bindings/wasm/src/sd_jwt_vc/token.rs b/bindings/wasm/src/sd_jwt_vc/token.rs new file mode 100644 index 0000000000..904ee2f2f4 --- /dev/null +++ b/bindings/wasm/src/sd_jwt_vc/token.rs @@ -0,0 +1,53 @@ +use identity_iota::core::Url; +use identity_iota::credential::sd_jwt_vc::vct_to_url as vct_to_url_impl; +use identity_iota::credential::sd_jwt_vc::SdJwtVc; +use wasm_bindgen::prelude::*; + +use crate::error::Result; +use crate::error::WasmResult; +use crate::jose::WasmJwk; +use crate::verification::IJwsVerifier; +use crate::verification::WasmJwsVerifier; + +use super::resolver::ResolverUrlToU8Array; + +#[wasm_bindgen(js_name = SdJwtVc)] +pub struct WasmSdJwtVc(pub(crate) SdJwtVc); + +#[wasm_bindgen(js_class = "SdJwtVc")] +impl WasmSdJwtVc { + /// Parses a `string` into an {@link SdJwtVc}. + #[wasm_bindgen] + pub fn parse(s: &str) -> Result { + SdJwtVc::parse(s).map(WasmSdJwtVc).wasm_result() + } + + #[wasm_bindgen(js_name = "issuerJwk")] + pub async fn issuer_jwk(&self, resolver: &ResolverUrlToU8Array) -> Result { + self + .0 + .issuer_jwk(resolver) + .await + .map(WasmJwk) + .wasm_result() + } + + #[wasm_bindgen(js_name = "verifySignature")] + pub fn verify_signature(&self, jws_verifier: Option, jwk: &WasmJwk) -> Result<()> { + let verifier = WasmJwsVerifier::new(jws_verifier); + self.0.verify_signature(&verifier, &jwk.0).wasm_result() + } + + /// Verify the signature of this {@link SdJwtVc}'s {@link KeyBindingJwt}. + #[wasm_bindgen(js_name = "verifyKeyBinding")] + pub fn verify_key_binding(&self, jws_verifier: Option, jwk: &WasmJwk) -> Result<()> { + let verifier = WasmJwsVerifier::new(jws_verifier); + self.0.verify_key_binding(&verifier, &jwk.0).wasm_result() + } +} + +#[wasm_bindgen(js_name = "vctToUrl")] +pub fn vct_to_url(resource: &str) -> Option { + let url = resource.parse::().ok()?; + vct_to_url_impl(&url).map(|url| url.to_string()) +} diff --git a/identity_credential/src/sd_jwt_vc/metadata/vc_type.rs b/identity_credential/src/sd_jwt_vc/metadata/vc_type.rs index 97be651392..57f71d720e 100644 --- a/identity_credential/src/sd_jwt_vc/metadata/vc_type.rs +++ b/identity_credential/src/sd_jwt_vc/metadata/vc_type.rs @@ -1,8 +1,8 @@ // Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use futures::future::BoxFuture; use futures::future::FutureExt; +use futures::future::LocalBoxFuture; use identity_core::common::Url; use itertools::Itertools as _; use serde::Deserialize; @@ -84,7 +84,7 @@ impl TypeMetadata { } /// Similar to [`TypeMetadata::validate_credential`], but accepts a [`Resolver`] - /// [`StringOrUrl`] -> [`Value`] that is used to resolve any reference to either + /// [`Url`] -> [`Value`] that is used to resolve any reference to either /// another type or JSON schema. pub async fn validate_credential_with_resolver(&self, credential: &Value, resolver: &R) -> Result<()> where @@ -100,7 +100,7 @@ fn validate_credential_impl<'c, 'r, R>( credential: &'c Value, resolver: &'r R, mut passed_types: Vec, -) -> BoxFuture<'c, Result<()>> +) -> LocalBoxFuture<'c, Result<()>> where R: Resolver + Sync, 'r: 'c, @@ -153,7 +153,7 @@ where Ok(()) } } - .boxed() + .boxed_local() } fn validate_credential_with_schema(schema: &Value, credential: &Value) -> Result<()> { diff --git a/identity_credential/src/sd_jwt_vc/mod.rs b/identity_credential/src/sd_jwt_vc/mod.rs index f3173264bf..66f4b530a1 100644 --- a/identity_credential/src/sd_jwt_vc/mod.rs +++ b/identity_credential/src/sd_jwt_vc/mod.rs @@ -8,7 +8,8 @@ mod error; /// such as issuer's metadata and credential type metadata. pub mod metadata; mod presentation; -mod resolver; +/// Resolver trait. +pub mod resolver; mod status; #[cfg(test)] pub(crate) mod tests; diff --git a/identity_credential/src/sd_jwt_vc/resolver.rs b/identity_credential/src/sd_jwt_vc/resolver.rs index 5e0993a90a..69bd74fc7a 100644 --- a/identity_credential/src/sd_jwt_vc/resolver.rs +++ b/identity_credential/src/sd_jwt_vc/resolver.rs @@ -1,25 +1,28 @@ // Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -#![allow(async_fn_in_trait)] - use async_trait::async_trait; use thiserror::Error; pub(crate) type Result = std::result::Result; +/// [`Resolver`]'s errors. #[derive(Debug, Error)] pub enum Error { + /// The queried item doesn't exist. #[error("The requested item \"{0}\" was not found.")] NotFound(String), + /// Failed to parse input. #[error("Failed to parse the provided input into a resolvable type: {0}")] ParsingFailure(#[source] anyhow::Error), + /// Generic error. #[error(transparent)] Generic(#[from] anyhow::Error), } /// A type capable of asynchronously producing values of type `T` from inputs of type `I`. -#[async_trait] +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] pub trait Resolver { /// Fetch the resource of type [`Resolver::Target`] using `input`. async fn resolve(&self, input: &I) -> Result; diff --git a/identity_credential/src/sd_jwt_vc/token.rs b/identity_credential/src/sd_jwt_vc/token.rs index f2635c7f7a..ead1fff35d 100644 --- a/identity_credential/src/sd_jwt_vc/token.rs +++ b/identity_credential/src/sd_jwt_vc/token.rs @@ -10,7 +10,6 @@ use super::metadata::ClaimMetadata; use super::metadata::IssuerMetadata; use super::metadata::Jwks; use super::metadata::TypeMetadata; -#[allow(unused_imports)] use super::metadata::WELL_KNOWN_VCT; use super::metadata::WELL_KNOWN_VC_ISSUER; use super::resolver::Error as ResolverErr; @@ -255,8 +254,6 @@ impl SdJwtVc { // Claims' disclosability. self.validate_claims_disclosability(type_metadata.claim_metadata())?; - // - Ok(()) } diff --git a/identity_iota/Cargo.toml b/identity_iota/Cargo.toml index cdddbcebc9..13448046f9 100644 --- a/identity_iota/Cargo.toml +++ b/identity_iota/Cargo.toml @@ -62,6 +62,9 @@ memstore = ["identity_storage/memstore"] # Enables selective disclosure features. sd-jwt = ["identity_credential/sd-jwt"] +# Enables selectively disclosable credentials. +sd-jwt-vc = ["identity_credential/sd-jwt-vc"] + # Enables zero knowledge selective disclosurable VCs jpt-bbs-plus = ["identity_storage/jpt-bbs-plus", "identity_credential/jpt-bbs-plus"] diff --git a/identity_iota/src/lib.rs b/identity_iota/src/lib.rs index 0a16a7e819..468ffb6f01 100644 --- a/identity_iota/src/lib.rs +++ b/identity_iota/src/lib.rs @@ -41,6 +41,8 @@ pub mod credential { #[cfg(feature = "revocation-bitmap")] pub use identity_credential::revocation::*; pub use identity_credential::validator::*; + #[cfg(feature = "sd-jwt-vc")] + pub use identity_credential::sd_jwt_vc; } pub mod did { @@ -127,3 +129,8 @@ pub mod sd_jwt_payload { //! Expose the selective disclosure crate. pub use identity_credential::sd_jwt_payload::*; } + +// Exposes the reworked version of the selective disclosure crate +// which is needed for selectively disclosable credentials. +#[cfg(feature = "sd-jwt-vc")] +pub use identity_credential::sd_jwt_v2 as sd_jwt_rework; From b4ed9de779ac815ba5ba20226b39ac9ceb448146 Mon Sep 17 00:00:00 2001 From: umr1352 Date: Wed, 18 Dec 2024 18:56:16 +0100 Subject: [PATCH 02/11] SdJwtVc WASM bindings --- bindings/wasm/src/error.rs | 9 ++ bindings/wasm/src/lib.rs | 2 +- bindings/wasm/src/macros.rs | 6 +- bindings/wasm/src/sd_jwt_vc/builder.rs | 132 ++++++++++++++++++ bindings/wasm/src/sd_jwt_vc/claims.rs | 25 ++++ bindings/wasm/src/sd_jwt_vc/metadata/claim.rs | 49 +++++++ .../wasm/src/sd_jwt_vc/metadata/issuer.rs | 57 ++++++++ bindings/wasm/src/sd_jwt_vc/metadata/mod.rs | 10 ++ .../wasm/src/sd_jwt_vc/metadata/vc_type.rs | 82 +++++++++++ bindings/wasm/src/sd_jwt_vc/mod.rs | 16 ++- bindings/wasm/src/sd_jwt_vc/presentation.rs | 53 +++++++ bindings/wasm/src/sd_jwt_vc/resolver.rs | 45 +++++- .../wasm/src/sd_jwt_vc/sd_jwt_v2/builder.rs | 83 +++++++++++ .../src/sd_jwt_vc/sd_jwt_v2/disclosure.rs | 4 + .../wasm/src/sd_jwt_vc/sd_jwt_v2/hasher.rs | 25 ++++ .../wasm/src/sd_jwt_vc/sd_jwt_v2/kb_jwt.rs | 5 + bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/mod.rs | 17 ++- .../wasm/src/sd_jwt_vc/sd_jwt_v2/sd_jwt.rs | 5 +- .../wasm/src/sd_jwt_vc/sd_jwt_v2/signer.rs | 3 + bindings/wasm/src/sd_jwt_vc/status.rs | 20 +++ bindings/wasm/src/sd_jwt_vc/token.rs | 90 +++++++++++- identity_core/src/common/url.rs | 8 +- identity_credential/src/sd_jwt_vc/builder.rs | 2 +- .../src/sd_jwt_vc/metadata/vc_type.rs | 4 +- 24 files changed, 723 insertions(+), 29 deletions(-) create mode 100644 bindings/wasm/src/sd_jwt_vc/builder.rs create mode 100644 bindings/wasm/src/sd_jwt_vc/metadata/claim.rs create mode 100644 bindings/wasm/src/sd_jwt_vc/metadata/issuer.rs create mode 100644 bindings/wasm/src/sd_jwt_vc/metadata/mod.rs create mode 100644 bindings/wasm/src/sd_jwt_vc/metadata/vc_type.rs create mode 100644 bindings/wasm/src/sd_jwt_vc/presentation.rs create mode 100644 bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/builder.rs create mode 100644 bindings/wasm/src/sd_jwt_vc/status.rs diff --git a/bindings/wasm/src/error.rs b/bindings/wasm/src/error.rs index 3bb51a7002..8c0effc4c7 100644 --- a/bindings/wasm/src/error.rs +++ b/bindings/wasm/src/error.rs @@ -176,6 +176,15 @@ impl From for WasmError<'_> { } } +impl From for WasmError<'_> { + fn from(value: anyhow::Error) -> Self { + Self { + name: Cow::Borrowed("Generic Error"), + message: Cow::Owned(value.to_string()), + } + } +} + impl From for WasmError<'_> { fn from(error: identity_iota::iota::block::Error) -> Self { Self { diff --git a/bindings/wasm/src/lib.rs b/bindings/wasm/src/lib.rs index 9e39ddb6d8..b7a0f08ea5 100644 --- a/bindings/wasm/src/lib.rs +++ b/bindings/wasm/src/lib.rs @@ -27,9 +27,9 @@ pub mod jpt; pub mod resolver; pub mod revocation; pub mod sd_jwt; +pub mod sd_jwt_vc; pub mod storage; pub mod verification; -pub mod sd_jwt_vc; /// Initializes the console error panic hook for better error messages #[wasm_bindgen(start)] diff --git a/bindings/wasm/src/macros.rs b/bindings/wasm/src/macros.rs index ecc99a8082..26cb197993 100644 --- a/bindings/wasm/src/macros.rs +++ b/bindings/wasm/src/macros.rs @@ -29,14 +29,14 @@ macro_rules! impl_wasm_json { impl $wasm_class { /// Serializes this to a JSON object. #[wasm_bindgen(js_name = toJSON)] - pub fn to_json(&self) -> $crate::error::Result { + pub fn to_json(&self) -> $crate::error::Result { use $crate::error::WasmResult; - JsValue::from_serde(&self.0).wasm_result() + wasm_bindgen::JsValue::from_serde(&self.0).wasm_result() } /// Deserializes an instance from a JSON object. #[wasm_bindgen(js_name = fromJSON)] - pub fn from_json(json: &JsValue) -> $crate::error::Result<$wasm_class> { + pub fn from_json(json: &wasm_bindgen::JsValue) -> $crate::error::Result<$wasm_class> { use $crate::error::WasmResult; json.into_serde().map(Self).wasm_result() } diff --git a/bindings/wasm/src/sd_jwt_vc/builder.rs b/bindings/wasm/src/sd_jwt_vc/builder.rs new file mode 100644 index 0000000000..8b181b8640 --- /dev/null +++ b/bindings/wasm/src/sd_jwt_vc/builder.rs @@ -0,0 +1,132 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_iota::core::StringOrUrl; +use identity_iota::core::Url; +use identity_iota::credential::sd_jwt_vc::SdJwtVcBuilder; +use wasm_bindgen::prelude::wasm_bindgen; + +use crate::common::WasmTimestamp; +use crate::credential::WasmCredential; +use crate::error::Result; +use crate::error::WasmResult; +use crate::sd_jwt_vc::WasmSdJwtVc; + +use super::sd_jwt_v2::WasmHasher; +use super::sd_jwt_v2::WasmJwsSigner; +use super::sd_jwt_v2::WasmRequiredKeyBinding; +use super::WasmStatus; + +#[wasm_bindgen(js_name = SdJwtVcBuilder)] +pub struct WasmSdJwtVcBuilder(pub(crate) SdJwtVcBuilder); + +#[wasm_bindgen(js_class = SdJwtVcBuilder)] +impl WasmSdJwtVcBuilder { + /// Creates a new {@link SdJwtVcBuilder} using `object` JSON representation and a given + /// hasher `hasher`. + #[wasm_bindgen(constructor)] + pub fn new(object: js_sys::Object, hasher: WasmHasher) -> Result { + let object = serde_wasm_bindgen::from_value::(object.into()).wasm_result()?; + SdJwtVcBuilder::new_with_hasher(object, hasher).map(Self).wasm_result() + } + + /// Creates a new [`SdJwtVcBuilder`] starting from a {@link Credential} that is converted to a JWT claim set. + #[wasm_bindgen(js_name = fromCredential)] + pub fn new_from_credential(credential: WasmCredential, hasher: WasmHasher) -> Result { + SdJwtVcBuilder::new_from_credential(credential.0, hasher) + .map(Self) + .wasm_result() + } + + /// Substitutes a value with the digest of its disclosure. + /// + /// ## Notes + /// - `path` indicates the pointer to the value that will be concealed using the syntax of [JSON pointer](https://datatracker.ietf.org/doc/html/rfc6901). + #[wasm_bindgen(js_name = makeConcealable)] + pub fn make_concealable(self, path: &str) -> Result { + self.0.make_concealable(path).map(Self).wasm_result() + } + + /// Sets the JWT header. + /// ## Notes + /// - if {@link SdJwtVcBuilder.header} is not called, the default header is used: ```json { "typ": "sd-jwt", "alg": + /// "" } ``` + /// - `alg` is always replaced with the value passed to {@link SdJwtVcBuilder.finish}. + #[wasm_bindgen] + pub fn header(self, header: js_sys::Object) -> Self { + let header = serde_wasm_bindgen::from_value(header.into()).expect("JS object is a valid JSON object"); + Self(self.0.header(header)) + } + + /// Require a proof of possession of a given key from the holder. + /// + /// This operation adds a JWT confirmation (`cnf`) claim as specified in + /// [RFC8300](https://www.rfc-editor.org/rfc/rfc7800.html#section-3). + #[wasm_bindgen(js_name = addDecoys)] + pub fn add_decoys(self, path: &str, number_of_decoys: usize) -> Result { + self.0.add_decoys(path, number_of_decoys).map(Self).wasm_result() + } + + /// Require a proof of possession of a given key from the holder. + /// + /// This operation adds a JWT confirmation (`cnf`) claim as specified in + /// [RFC8300](https://www.rfc-editor.org/rfc/rfc7800.html#section-3). + #[wasm_bindgen(js_name = requireKeyBinding)] + pub fn require_key_binding(self, key_bind: WasmRequiredKeyBinding) -> Result { + let key_bind = serde_wasm_bindgen::from_value(key_bind.into()).wasm_result()?; + Ok(Self(self.0.require_key_binding(key_bind))) + } + + /// Inserts an `iss` claim. See {@link SdJwtVcClaim.iss}. + #[wasm_bindgen] + pub fn iss(self, issuer: &str) -> Result { + let url = Url::parse(issuer).wasm_result()?; + Ok(Self(self.0.iss(url))) + } + + /// Inserts a `nbf` claim. See {@link SdJwtVcClaims.nbf}. + #[wasm_bindgen] + pub fn nbf(self, nbf: WasmTimestamp) -> Self { + Self(self.0.nbf(nbf.0)) + } + + /// Inserts a `exp` claim. See {@link SdJwtVcClaims.exp}. + #[wasm_bindgen] + pub fn exp(self, exp: WasmTimestamp) -> Self { + Self(self.0.exp(exp.0)) + } + + /// Inserts a `iat` claim. See {@link SdJwtVcClaims.iat}. + #[wasm_bindgen] + pub fn iat(self, iat: WasmTimestamp) -> Self { + Self(self.0.iat(iat.0)) + } + + /// Inserts a `vct` claim. See {@link SdJwtVcClaims.vct}. + #[wasm_bindgen] + pub fn vct(self, vct: &str) -> Self { + let vct = StringOrUrl::parse(vct).unwrap(); + Self(self.0.vct(vct)) + } + + /// Inserts a `sub` claim. See {@link SdJwtVcClaims.sub}. + #[allow(clippy::should_implement_trait)] + #[wasm_bindgen] + pub fn sub(self, sub: &str) -> Self { + let sub = StringOrUrl::parse(sub).unwrap(); + Self(self.0.sub(sub)) + } + + /// Inserts a `status` claim. See {@link SdJwtVcClaims.status}. + #[wasm_bindgen] + pub fn status(self, status: WasmStatus) -> Result { + let status = serde_wasm_bindgen::from_value(status.into()).wasm_result()?; + Ok(Self(self.0.status(status))) + } + + /// Creates an {@link SdJwtVc} with the provided data. + #[wasm_bindgen] + pub async fn finish(self, signer: &WasmJwsSigner, alg: &str) -> Result { + self.0.finish(signer, alg).await.map(WasmSdJwtVc).wasm_result() + } +} diff --git a/bindings/wasm/src/sd_jwt_vc/claims.rs b/bindings/wasm/src/sd_jwt_vc/claims.rs index e69de29bb2..52093bec5f 100644 --- a/bindings/wasm/src/sd_jwt_vc/claims.rs +++ b/bindings/wasm/src/sd_jwt_vc/claims.rs @@ -0,0 +1,25 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use wasm_bindgen::prelude::wasm_bindgen; + +#[wasm_bindgen(typescript_custom_section)] +const I_SD_JWT_VC_CLAIMS: &str = r#" +interface ISdJwtVcClaims { + iss: string; + nbf: string | undefined; + exp: string | undefined; + vct: string; + status: SdJwtVcStatus; + iat: string | undefined; + sub: string | undefined; +} + +type SdJwtVcClaims = ISdJwtVcClaims & SdJwtClaims; +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "SdJwtVcClaims")] + pub type WasmSdJwtVcClaims; +} diff --git a/bindings/wasm/src/sd_jwt_vc/metadata/claim.rs b/bindings/wasm/src/sd_jwt_vc/metadata/claim.rs new file mode 100644 index 0000000000..30c59e8174 --- /dev/null +++ b/bindings/wasm/src/sd_jwt_vc/metadata/claim.rs @@ -0,0 +1,49 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use wasm_bindgen::prelude::wasm_bindgen; + +#[wasm_bindgen(typescript_custom_section)] +const T_CLAIM_PATH: &str = r#" +type ClaimPathSegment = string | number | null; +type ClaimPath = ClaimPathSegment[]; +"#; + +#[wasm_bindgen(typescript_custom_section)] +const T_DISCLOSABILITY: &str = r#" +type ClaimDisclosability = "always" | "allowed" | "never"; +"#; + +#[wasm_bindgen] +extern "C" { + #[derive(Clone)] + #[wasm_bindgen(typescript_type = ClaimPathSegment)] + pub type WasmClaimPathSegment; + + #[derive(Clone)] + #[wasm_bindgen(typescript_type = ClaimPath)] + pub type WasmClaimPath; + + #[derive(Clone)] + #[wasm_bindgen(typescript_type = ClaimDisclosability)] + pub type WasmClaimDisclosability; +} + +#[wasm_bindgen(js_name = ClaimMetadata, inspectable, getter_with_clone)] +pub struct WasmClaimMetadata { + pub path: WasmClaimPath, + pub display: Vec, + pub sd: Option, + pub svg_id: Option, +} + +#[derive(Clone)] +#[wasm_bindgen(js_name = ClaimDisplay, inspectable, getter_with_clone)] +pub struct WasmClaimDisplay { + /// A language tag as defined in [RFC5646](https://www.rfc-editor.org/rfc/rfc5646.txt). + pub lang: String, + /// A human-readable label for the claim. + pub label: String, + /// A human-readable description for the claim. + pub description: Option, +} diff --git a/bindings/wasm/src/sd_jwt_vc/metadata/issuer.rs b/bindings/wasm/src/sd_jwt_vc/metadata/issuer.rs new file mode 100644 index 0000000000..5a62f28b61 --- /dev/null +++ b/bindings/wasm/src/sd_jwt_vc/metadata/issuer.rs @@ -0,0 +1,57 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_iota::core::Url; +use identity_iota::credential::sd_jwt_vc::metadata::IssuerMetadata; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::JsCast; + +use crate::error::Result; +use crate::error::WasmResult; +use crate::sd_jwt_vc::WasmSdJwtVc; + +#[wasm_bindgen(typescript_custom_section)] +pub const I_JWKS: &str = r#" +type Jwks = { jwks_uri: string } | { jwks: { keys: Jwk[] }}; +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = Jwks)] + pub type WasmJwks; +} + +#[wasm_bindgen(js_name = IssuerMetadata)] +pub struct WasmIssuerMetadata(pub(crate) IssuerMetadata); + +impl_wasm_json!(WasmIssuerMetadata, IssuerMetadata); + +#[wasm_bindgen(js_class = IssuerMetadata)] +impl WasmIssuerMetadata { + #[wasm_bindgen(constructor)] + pub fn new(issuer: String, jwks: WasmJwks) -> Result { + let issuer = Url::parse(&issuer).wasm_result()?; + let jwks = serde_wasm_bindgen::from_value(jwks.into()).wasm_result()?; + + Ok(Self(IssuerMetadata { issuer, jwks })) + } + + #[wasm_bindgen] + pub fn issuer(&self) -> String { + self.0.issuer.to_string() + } + + #[wasm_bindgen] + pub fn jwks(&self) -> Result { + serde_wasm_bindgen::to_value(&self.0.jwks) + .wasm_result() + .map(JsCast::unchecked_into) + } + + /// Checks the validity of this {@link IssuerMetadata}. + /// {@link IssuerMetadata.issuer} must match `sd_jwt_vc`'s `iss` claim. + #[wasm_bindgen] + pub fn validate(&self, sd_jwt_vc: &WasmSdJwtVc) -> Result<()> { + self.0.validate(&sd_jwt_vc.0).wasm_result() + } +} diff --git a/bindings/wasm/src/sd_jwt_vc/metadata/mod.rs b/bindings/wasm/src/sd_jwt_vc/metadata/mod.rs new file mode 100644 index 0000000000..eff8dbcf41 --- /dev/null +++ b/bindings/wasm/src/sd_jwt_vc/metadata/mod.rs @@ -0,0 +1,10 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +mod claim; +mod issuer; +mod vc_type; + +pub use claim::*; +pub use issuer::*; +pub use vc_type::*; diff --git a/bindings/wasm/src/sd_jwt_vc/metadata/vc_type.rs b/bindings/wasm/src/sd_jwt_vc/metadata/vc_type.rs new file mode 100644 index 0000000000..6d804374d3 --- /dev/null +++ b/bindings/wasm/src/sd_jwt_vc/metadata/vc_type.rs @@ -0,0 +1,82 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_iota::credential::sd_jwt_vc::metadata::TypeMetadata; +use serde::Serialize; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::JsCast; +use wasm_bindgen::JsValue; + +use crate::error::Result; +use crate::error::WasmResult; +use crate::sd_jwt_vc::resolver::ResolverUrlToValue; + +#[wasm_bindgen(typescript_custom_section)] +const I_TYPE_METADATA: &str = r#" +type TypeSchema = { schema_uri: string, schema_uri#integrity: string | undefined } + | { schema: any, schema#integrity: string | undefined }; + +type TypeMetadataHelper = { + name: string | undefined; + description: string | undefined; + extends: string | undefined; + extends#integrity: string | undefined; + display: any[]; + claims: ClaimMetadata[]; +} & TypeSchema; +"#; + +#[wasm_bindgen] +extern "C" { + pub type TypeMetadataHelper; +} + +#[derive(Serialize)] +#[serde(transparent)] +#[wasm_bindgen(js_name = TypeMetadata)] +pub struct WasmTypeMetadata(pub(crate) TypeMetadata); + +impl_wasm_json!(WasmTypeMetadata, TypeMetadata); + +#[wasm_bindgen(js_class = TypeMetadata)] +impl WasmTypeMetadata { + #[wasm_bindgen(constructor)] + pub fn new(helper: TypeMetadataHelper) -> Result { + serde_wasm_bindgen::from_value(helper.into()).map(Self).wasm_result() + } + + #[wasm_bindgen(js_name = intoInner)] + pub fn into_inner(&self) -> Result { + serde_wasm_bindgen::to_value(&self.0) + .wasm_result() + .and_then(JsCast::dyn_into) + } + + /// Uses this {@link TypeMetadata} to validate JSON object `credential`. This method fails + /// if the schema is referenced instead of embedded. + /// Use {@link TypeMetadata.validate_credential_with_resolver} for such cases. + /// ## Notes + /// This method ignores type extensions. + #[wasm_bindgen(js_name = validateCredential)] + pub fn validate_credential(&self, credential: JsValue) -> Result<()> { + let credential = serde_wasm_bindgen::from_value(credential).wasm_result()?; + self.0.validate_credential(&credential).wasm_result() + } + + /// Similar to {@link TypeMetadata.validate_credential}, but accepts a {@link Resolver} + /// {@link Url} -> {@link any} that is used to resolve any reference to either + /// another type or JSON schema. + #[wasm_bindgen(js_name = validateCredentialWithResolver)] + pub async fn validate_credential_with_resolver( + &self, + credential: JsValue, + resolver: &ResolverUrlToValue, + ) -> Result<()> { + let credential = serde_wasm_bindgen::from_value(credential).wasm_result()?; + self + .0 + .validate_credential_with_resolver(&credential, resolver) + .await + .wasm_result() + } +} diff --git a/bindings/wasm/src/sd_jwt_vc/mod.rs b/bindings/wasm/src/sd_jwt_vc/mod.rs index 2159f0d5cd..7396c14386 100644 --- a/bindings/wasm/src/sd_jwt_vc/mod.rs +++ b/bindings/wasm/src/sd_jwt_vc/mod.rs @@ -1,7 +1,17 @@ -mod token; -mod resolver; +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +mod builder; mod claims; +pub mod metadata; +mod presentation; +mod resolver; pub mod sd_jwt_v2; +mod status; +mod token; +pub use builder::*; +pub use claims::*; +pub use presentation::*; +pub use status::*; pub use token::*; -pub use claims::*; \ No newline at end of file diff --git a/bindings/wasm/src/sd_jwt_vc/presentation.rs b/bindings/wasm/src/sd_jwt_vc/presentation.rs new file mode 100644 index 0000000000..7b4c4da12e --- /dev/null +++ b/bindings/wasm/src/sd_jwt_vc/presentation.rs @@ -0,0 +1,53 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_iota::credential::sd_jwt_vc::SdJwtVcPresentationBuilder; +use wasm_bindgen::prelude::wasm_bindgen; + +use super::sd_jwt_v2::WasmDisclosure; +use super::sd_jwt_v2::WasmHasher; +use super::sd_jwt_v2::WasmKeyBindingJwt; +use super::WasmSdJwtVc; +use crate::error::Result; +use crate::error::WasmResult; + +#[wasm_bindgen(js_name = SdJwtVcPresentationBuilder)] +pub struct WasmSdJwtVcPresentationBuilder(pub(crate) SdJwtVcPresentationBuilder); + +#[wasm_bindgen(js_class = SdJwtVcPresentationBuilder)] +impl WasmSdJwtVcPresentationBuilder { + /// Prepares a new presentation from a given {@link SdJwtVc}. + #[wasm_bindgen(constructor)] + pub fn new(token: WasmSdJwtVc, hasher: &WasmHasher) -> Result { + SdJwtVcPresentationBuilder::new(token.0, hasher).map(Self).wasm_result() + } + + #[wasm_bindgen] + pub fn conceal(self, path: &str) -> Result { + self.0.conceal(path).map(Self).wasm_result() + } + + #[wasm_bindgen(js_name = attachKeyBindingJwt)] + pub fn attach_key_binding_jwt(self, kb_jwt: WasmKeyBindingJwt) -> Self { + Self(self.0.attach_key_binding_jwt(kb_jwt.0)) + } + + #[wasm_bindgen] + pub fn finish(self) -> Result { + self + .0 + .finish() + .map(|(token, disclosures)| PresentationResult { + sd_jwt_vc: WasmSdJwtVc(token), + disclosures: disclosures.into_iter().map(WasmDisclosure::from).collect(), + }) + .wasm_result() + } +} + +#[wasm_bindgen(js_name = SdJwtVcPresentationResult, getter_with_clone)] +pub struct PresentationResult { + #[wasm_bindgen(js_name = sdJwtVc)] + pub sd_jwt_vc: WasmSdJwtVc, + pub disclosures: Vec, +} diff --git a/bindings/wasm/src/sd_jwt_vc/resolver.rs b/bindings/wasm/src/sd_jwt_vc/resolver.rs index efc384fa1e..585b672618 100644 --- a/bindings/wasm/src/sd_jwt_vc/resolver.rs +++ b/bindings/wasm/src/sd_jwt_vc/resolver.rs @@ -1,3 +1,7 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use anyhow::anyhow; use async_trait::async_trait; use identity_iota::core::Url; use identity_iota::credential::sd_jwt_vc::resolver::Error as ErrorR; @@ -5,22 +9,51 @@ use identity_iota::credential::sd_jwt_vc::Resolver; use js_sys::Uint8Array; use wasm_bindgen::prelude::*; +#[wasm_bindgen(typescript_custom_section)] +const I_RESOLVER: &str = r#" +interface Resolver { + resolve: async (input: I) => T; +} +"#; + #[wasm_bindgen] extern "C" { - pub type ResolverUrlToU8Array; + // Resolver> + #[wasm_bindgen(typescript_type = "Resolver")] + pub type ResolverStringToUint8Array; #[wasm_bindgen(structural, method, catch)] - pub async fn resolve(this: &ResolverUrlToU8Array, input: &str) -> Result; -} + pub async fn resolve(this: &ResolverStringToUint8Array, input: &str) -> Result; + // Resolver + #[wasm_bindgen(typescript_type = "Resolver")] + pub type ResolverUrlToValue; + + #[wasm_bindgen(structural, method, catch)] + pub async fn resolve(this: &ResolverUrlToValue, input: &str) -> Result; +} #[async_trait(?Send)] -impl Resolver> for ResolverUrlToU8Array { - async fn resolve(&self, input: &Url) -> std::result::Result, ErrorR> { +impl Resolver> for ResolverStringToUint8Array +where + I: AsRef + Sync, +{ + async fn resolve(&self, input: &I) -> Result, ErrorR> { self - .resolve(input.as_str()) + .resolve(input.as_ref()) .await .map(|arr| arr.to_vec()) .map_err(|e| ErrorR::Generic(anyhow::anyhow!("{}", e.to_string()))) } } + +#[async_trait(?Send)] +impl Resolver for ResolverUrlToValue { + async fn resolve(&self, input: &Url) -> Result { + self + .resolve(input.as_str()) + .await + .map(|js_value| serde_wasm_bindgen::from_value(js_value).expect("JS value is a JSON value")) + .map_err(|e| ErrorR::Generic(anyhow!("{}", e.to_string()))) + } +} diff --git a/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/builder.rs b/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/builder.rs new file mode 100644 index 0000000000..b6f6ca46c3 --- /dev/null +++ b/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/builder.rs @@ -0,0 +1,83 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_iota::sd_jwt_rework::SdJwtBuilder; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::JsValue; + +use crate::error::Result; +use crate::error::WasmResult; +use crate::sd_jwt_vc::sd_jwt_v2::WasmSdJwt; + +use super::WasmHasher; +use super::WasmJwsSigner; +use super::WasmRequiredKeyBinding; + +#[wasm_bindgen(js_name = SdJwtBuilder)] +pub struct WasmSdJwtBuilder(pub(crate) SdJwtBuilder); + +#[wasm_bindgen(js_class = SdJwtBuilder)] +impl WasmSdJwtBuilder { + /// Creates a new {@link SdJwtVcBuilder} using `object` JSON representation and a given + /// hasher `hasher`. + #[wasm_bindgen(constructor)] + pub fn new(object: js_sys::Object, hasher: WasmHasher, salt_size: Option) -> Result { + let object = serde_wasm_bindgen::from_value::(object.into()).wasm_result()?; + let salt_size = salt_size.unwrap_or(30); + SdJwtBuilder::new_with_hasher_and_salt_size(object, hasher, salt_size) + .map(Self) + .wasm_result() + } + + /// Substitutes a value with the digest of its disclosure. + /// + /// ## Notes + /// - `path` indicates the pointer to the value that will be concealed using the syntax of [JSON pointer](https://datatracker.ietf.org/doc/html/rfc6901). + #[wasm_bindgen(js_name = makeConcealable)] + pub fn make_concealable(self, path: &str) -> Result { + self.0.make_concealable(path).map(Self).wasm_result() + } + + /// Sets the JWT header. + /// ## Notes + /// - if {@link SdJwtVcBuilder.header} is not called, the default header is used: ```json { "typ": "sd-jwt", "alg": + /// "" } ``` + /// - `alg` is always replaced with the value passed to {@link SdJwtVcBuilder.finish}. + #[wasm_bindgen] + pub fn header(self, header: js_sys::Object) -> Self { + let header = serde_wasm_bindgen::from_value(header.into()).expect("JS object is a valid JSON object"); + Self(self.0.header(header)) + } + + /// Adds a new claim to the underlying object. + #[wasm_bindgen(js_name = insertClaim)] + pub fn insert_claim(self, key: String, value: JsValue) -> Result { + let value = serde_wasm_bindgen::from_value::(value).wasm_result()?; + self.0.insert_claim(key, value).map(Self).wasm_result() + } + + /// Require a proof of possession of a given key from the holder. + /// + /// This operation adds a JWT confirmation (`cnf`) claim as specified in + /// [RFC8300](https://www.rfc-editor.org/rfc/rfc7800.html#section-3). + #[wasm_bindgen(js_name = addDecoys)] + pub fn add_decoys(self, path: &str, number_of_decoys: usize) -> Result { + self.0.add_decoys(path, number_of_decoys).map(Self).wasm_result() + } + + /// Require a proof of possession of a given key from the holder. + /// + /// This operation adds a JWT confirmation (`cnf`) claim as specified in + /// [RFC8300](https://www.rfc-editor.org/rfc/rfc7800.html#section-3). + #[wasm_bindgen(js_name = requireKeyBinding)] + pub fn require_key_binding(self, key_bind: WasmRequiredKeyBinding) -> Result { + let key_bind = serde_wasm_bindgen::from_value(key_bind.into()).wasm_result()?; + Ok(Self(self.0.require_key_binding(key_bind))) + } + + /// Creates an {@link SdJwtVc} with the provided data. + #[wasm_bindgen] + pub async fn finish(self, signer: &WasmJwsSigner, alg: &str) -> Result { + self.0.finish(signer, alg).await.map(WasmSdJwt).wasm_result() + } +} diff --git a/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/disclosure.rs b/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/disclosure.rs index 5174af28b6..aa7b6d7a7b 100644 --- a/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/disclosure.rs +++ b/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/disclosure.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use identity_iota::sd_jwt_rework::Disclosure; use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::JsValue; @@ -27,6 +30,7 @@ impl WasmDisclosure { Disclosure::parse(s).map(Self::from).wasm_result() } + #[allow(clippy::inherent_to_string)] #[wasm_bindgen(js_name = toString)] pub fn to_string(&self) -> String { self.unparsed.clone() diff --git a/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/hasher.rs b/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/hasher.rs index 5093d4b32f..982e9a51d7 100644 --- a/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/hasher.rs +++ b/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/hasher.rs @@ -1,6 +1,10 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use std::sync::OnceLock; use identity_iota::sd_jwt_rework::Hasher; +use identity_iota::sd_jwt_rework::Sha256Hasher; use wasm_bindgen::prelude::wasm_bindgen; #[wasm_bindgen(typescript_custom_section)] @@ -41,3 +45,24 @@ impl Hasher for WasmHasher { self.encoded_digest(disclosure) } } + +#[wasm_bindgen(js_name = Sha256Hasher)] +pub struct WasmSha256Hasher(pub(crate) Sha256Hasher); + +#[wasm_bindgen(js_class = Sha256Hasher)] +impl WasmSha256Hasher { + #[wasm_bindgen(js_name = algName)] + pub fn alg_name(&self) -> String { + self.0.alg_name().to_owned() + } + + #[wasm_bindgen] + pub fn digest(&self, input: &[u8]) -> Vec { + self.0.digest(input) + } + + #[wasm_bindgen(js_name = encodedDigest)] + pub fn encoded_digest(&self, data: &str) -> String { + self.0.encoded_digest(data) + } +} diff --git a/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/kb_jwt.rs b/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/kb_jwt.rs index 8255b7fecd..ec4b83f710 100644 --- a/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/kb_jwt.rs +++ b/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/kb_jwt.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use identity_iota::credential; use identity_iota::credential::sd_jwt_vc; use identity_iota::sd_jwt_rework::KeyBindingJwt; @@ -61,6 +64,7 @@ impl WasmKeyBindingJwt { serde_wasm_bindgen::to_value(self.0.claims()).unwrap().unchecked_into() } + #[allow(clippy::inherent_to_string)] #[wasm_bindgen(js_name = "toString")] pub fn to_string(&self) -> String { self.0.to_string() @@ -72,6 +76,7 @@ pub struct WasmKeyBindingJwtBuilder(pub(crate) KeyBindingJwtBuilder); #[wasm_bindgen(js_class = KeyBindingJwtBuilder)] impl WasmKeyBindingJwtBuilder { + #[allow(clippy::new_without_default)] #[wasm_bindgen(constructor)] pub fn new() -> WasmKeyBindingJwtBuilder { Self(KeyBindingJwtBuilder::default()) diff --git a/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/mod.rs b/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/mod.rs index afed902a7b..5a526b5d62 100644 --- a/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/mod.rs +++ b/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/mod.rs @@ -1,11 +1,16 @@ -mod sd_jwt; +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +mod builder; +mod disclosure; +mod hasher; mod kb_jwt; +mod sd_jwt; mod signer; -mod hasher; -mod disclosure; -pub use sd_jwt::*; +pub use builder::*; +pub use disclosure::*; +pub use hasher::*; pub use kb_jwt::*; +pub use sd_jwt::*; pub use signer::*; -pub use hasher::*; -pub use disclosure::*; diff --git a/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/sd_jwt.rs b/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/sd_jwt.rs index 7567482ddf..5c633bd675 100644 --- a/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/sd_jwt.rs +++ b/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/sd_jwt.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use identity_iota::credential::sd_jwt_vc::Error; use identity_iota::sd_jwt_rework::SdJwt; use identity_iota::sd_jwt_rework::SdJwtPresentationBuilder; @@ -38,7 +41,7 @@ pub struct WasmSdJwt(pub(crate) SdJwt); impl WasmSdJwt { #[wasm_bindgen] pub fn parse(s: &str) -> Result { - SdJwt::parse(s).map(Self).map_err(|e| Error::from(e)).wasm_result() + SdJwt::parse(s).map(Self).map_err(Error::from).wasm_result() } #[wasm_bindgen] diff --git a/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/signer.rs b/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/signer.rs index 3d5d518c81..60e7ae7fa6 100644 --- a/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/signer.rs +++ b/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/signer.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use async_trait::async_trait; use identity_iota::sd_jwt_rework::JsonObject; use identity_iota::sd_jwt_rework::JwsSigner; diff --git a/bindings/wasm/src/sd_jwt_vc/status.rs b/bindings/wasm/src/sd_jwt_vc/status.rs new file mode 100644 index 0000000000..0b22eaa634 --- /dev/null +++ b/bindings/wasm/src/sd_jwt_vc/status.rs @@ -0,0 +1,20 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use wasm_bindgen::prelude::wasm_bindgen; + +#[wasm_bindgen(typescript_custom_section)] +const I_SD_JWT_VC_STATUS: &str = r#" +interface SdJwtVcStatusListRef { + uri: string; + idx: number; +} + +type SdJwtVcStatus = { status_list: SdJwtVcStatusListRef } | any; +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(js_name = "SdJwtVcStatus")] + pub type WasmStatus; +} diff --git a/bindings/wasm/src/sd_jwt_vc/token.rs b/bindings/wasm/src/sd_jwt_vc/token.rs index 904ee2f2f4..03016a6ab4 100644 --- a/bindings/wasm/src/sd_jwt_vc/token.rs +++ b/bindings/wasm/src/sd_jwt_vc/token.rs @@ -1,16 +1,35 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use anyhow::anyhow; +use anyhow::Context; use identity_iota::core::Url; use identity_iota::credential::sd_jwt_vc::vct_to_url as vct_to_url_impl; use identity_iota::credential::sd_jwt_vc::SdJwtVc; use wasm_bindgen::prelude::*; +use crate::credential::WasmKeyBindingJWTValidationOptions; use crate::error::Result; use crate::error::WasmResult; use crate::jose::WasmJwk; +use crate::sd_jwt_vc::metadata::WasmTypeMetadata; use crate::verification::IJwsVerifier; use crate::verification::WasmJwsVerifier; -use super::resolver::ResolverUrlToU8Array; +use super::metadata::WasmIssuerMetadata; +use super::resolver::ResolverStringToUint8Array; +use super::sd_jwt_v2::WasmHasher; +use super::sd_jwt_v2::WasmSdJwt; +use super::WasmSdJwtVcClaims; +use super::WasmSdJwtVcPresentationBuilder; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "[TypeMetadata, Uint8Array]")] + pub type TypeMetadataResult; +} +#[derive(Clone)] #[wasm_bindgen(js_name = SdJwtVc)] pub struct WasmSdJwtVc(pub(crate) SdJwtVc); @@ -22,14 +41,40 @@ impl WasmSdJwtVc { SdJwtVc::parse(s).map(WasmSdJwtVc).wasm_result() } + #[wasm_bindgen] + pub fn claims(&self) -> Result { + serde_wasm_bindgen::to_value(self.0.claims()) + .map(JsCast::unchecked_into) + .wasm_result() + } + #[wasm_bindgen(js_name = "issuerJwk")] - pub async fn issuer_jwk(&self, resolver: &ResolverUrlToU8Array) -> Result { - self + pub async fn issuer_jwk(&self, resolver: &ResolverStringToUint8Array) -> Result { + self.0.issuer_jwk(resolver).await.map(WasmJwk).wasm_result() + } + + #[wasm_bindgen(js_name = "issuerMetadata")] + pub async fn issuer_metadata(&self, resolver: &ResolverStringToUint8Array) -> Result> { + self + .0 + .issuer_metadata(resolver) + .await + .map(|maybe_metadata| maybe_metadata.map(WasmIssuerMetadata)) + .wasm_result() + } + + #[wasm_bindgen(js_name = "typeMetadata")] + pub async fn type_metadata(&self, resolver: &ResolverStringToUint8Array) -> Result { + self .0 - .issuer_jwk(resolver) + .type_metadata(resolver) .await - .map(WasmJwk) + .context("resolution error") + .and_then(|(metadata, integrity)| { + serde_wasm_bindgen::to_value(&(WasmTypeMetadata(metadata), integrity)).map_err(|e| anyhow!("{}", e.to_string())) + }) .wasm_result() + .and_then(JsCast::dyn_into) } #[wasm_bindgen(js_name = "verifySignature")] @@ -44,6 +89,41 @@ impl WasmSdJwtVc { let verifier = WasmJwsVerifier::new(jws_verifier); self.0.verify_key_binding(&verifier, &jwk.0).wasm_result() } + + #[wasm_bindgen(js_name = "validateKeyBinding")] + pub fn validate_key_binding( + &self, + jws_verifier: Option, + jwk: &WasmJwk, + hasher: &WasmHasher, + options: &WasmKeyBindingJWTValidationOptions, + ) -> Result<()> { + let jws_verifier = WasmJwsVerifier::new(jws_verifier); + self + .0 + .validate_key_binding(&jws_verifier, &jwk.0, hasher, &options.0) + .wasm_result() + } + + #[wasm_bindgen(js_name = "intoSdJwt")] + pub fn into_sd_jwt(self) -> WasmSdJwt { + WasmSdJwt(self.0.into()) + } + + #[wasm_bindgen(js_name = "intoDisclosedObject")] + pub fn into_disclosed_object(self, hasher: &WasmHasher) -> Result { + self + .0 + .into_disclosed_object(hasher) + .map(|obj| serde_wasm_bindgen::to_value(&obj).expect("JSON object is a valid JS object")) + .map(JsCast::unchecked_into) + .wasm_result() + } + + #[wasm_bindgen(js_name = "intoPresentation")] + pub fn into_presentation(self, hasher: &WasmHasher) -> Result { + WasmSdJwtVcPresentationBuilder::new(self, hasher) + } } #[wasm_bindgen(js_name = "vctToUrl")] diff --git a/identity_core/src/common/url.rs b/identity_core/src/common/url.rs index 6736a4cac9..f7fc244654 100644 --- a/identity_core/src/common/url.rs +++ b/identity_core/src/common/url.rs @@ -17,7 +17,7 @@ use crate::error::Error; use crate::error::Result; /// A parsed URL. -#[derive(Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)] +#[derive(Clone, Hash, Eq, PartialOrd, Ord, Deserialize, Serialize)] #[repr(transparent)] #[serde(transparent)] pub struct Url(::url::Url); @@ -96,3 +96,9 @@ impl KeyComparable for Url { self } } + +impl AsRef for Url { + fn as_ref(&self) -> &str { + self.as_str() + } +} \ No newline at end of file diff --git a/identity_credential/src/sd_jwt_vc/builder.rs b/identity_credential/src/sd_jwt_vc/builder.rs index 46b28bc9b2..234f815bbd 100644 --- a/identity_credential/src/sd_jwt_vc/builder.rs +++ b/identity_credential/src/sd_jwt_vc/builder.rs @@ -156,7 +156,7 @@ impl SdJwtVcBuilder { /// Sets the JWT header. /// ## Notes /// - if [`SdJwtVcBuilder::header`] is not called, the default header is used: ```json { "typ": "sd-jwt", "alg": - /// "" } ``` + /// "" } ``` /// - `alg` is always replaced with the value passed to [`SdJwtVcBuilder::finish`]. pub fn header(mut self, header: JsonObject) -> Self { self.header = header; diff --git a/identity_credential/src/sd_jwt_vc/metadata/vc_type.rs b/identity_credential/src/sd_jwt_vc/metadata/vc_type.rs index 57f71d720e..4d7361904f 100644 --- a/identity_credential/src/sd_jwt_vc/metadata/vc_type.rs +++ b/identity_credential/src/sd_jwt_vc/metadata/vc_type.rs @@ -88,7 +88,7 @@ impl TypeMetadata { /// another type or JSON schema. pub async fn validate_credential_with_resolver(&self, credential: &Value, resolver: &R) -> Result<()> where - R: Resolver + Sync, + R: Resolver, { validate_credential_impl(self.clone(), credential, resolver, vec![]).await } @@ -102,7 +102,7 @@ fn validate_credential_impl<'c, 'r, R>( mut passed_types: Vec, ) -> LocalBoxFuture<'c, Result<()>> where - R: Resolver + Sync, + R: Resolver, 'r: 'c, { async move { From 5cc5b284cd98eaa0bfb836e1b9c46e40916a6ebc Mon Sep 17 00:00:00 2001 From: umr1352 Date: Fri, 20 Dec 2024 16:18:19 +0100 Subject: [PATCH 03/11] small example, many small fixes --- .../examples/src/1_advanced/10_sd_jwt_vc.ts | 130 ++++++++++ bindings/wasm/examples/src/main.ts | 3 + bindings/wasm/package-lock.json | 225 +++++++++++++++++- bindings/wasm/package.json | 4 +- bindings/wasm/src/jose/jwk.rs | 3 +- bindings/wasm/src/sd_jwt_vc/metadata/claim.rs | 26 ++ .../wasm/src/sd_jwt_vc/metadata/issuer.rs | 12 +- .../wasm/src/sd_jwt_vc/metadata/vc_type.rs | 14 +- bindings/wasm/src/sd_jwt_vc/resolver.rs | 23 +- .../src/sd_jwt_vc/sd_jwt_v2/disclosure.rs | 2 +- .../wasm/src/sd_jwt_vc/sd_jwt_v2/hasher.rs | 6 + .../wasm/src/sd_jwt_vc/sd_jwt_v2/kb_jwt.rs | 5 + .../wasm/src/sd_jwt_vc/sd_jwt_v2/sd_jwt.rs | 11 + .../wasm/src/sd_jwt_vc/sd_jwt_v2/signer.rs | 11 +- bindings/wasm/src/sd_jwt_vc/token.rs | 66 +++-- identity_credential/src/sd_jwt_vc/token.rs | 7 +- identity_jose/src/jwk/key_set.rs | 1 - 17 files changed, 498 insertions(+), 51 deletions(-) create mode 100644 bindings/wasm/examples/src/1_advanced/10_sd_jwt_vc.ts diff --git a/bindings/wasm/examples/src/1_advanced/10_sd_jwt_vc.ts b/bindings/wasm/examples/src/1_advanced/10_sd_jwt_vc.ts new file mode 100644 index 0000000000..d997bb31b0 --- /dev/null +++ b/bindings/wasm/examples/src/1_advanced/10_sd_jwt_vc.ts @@ -0,0 +1,130 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { IssuerMetadata, SdJwtVcBuilder, Sha256Hasher, Timestamp, TypeMetadataHelper, IResolver, JwkType, IJwk, Jwk, IJwkParams } from "@iota/identity-wasm/node"; +import { base64url, exportJWK, generateKeyPair, JWK, JWTHeaderParameters, JWTPayload, SignJWT } from "jose"; + +const vc_metadata: TypeMetadataHelper = JSON.parse(`{ + "vct": "https://example.com/education_credential", + "name": "Betelgeuse Education Credential - Preliminary Version", + "description": "This is our development version of the education credential. Don't panic.", + "claims": [ + { + "path": ["name"], + "display": [ + { + "lang": "de-DE", + "label": "Vor- und Nachname", + "description": "Der Name des Studenten" + }, + { + "lang": "en-US", + "label": "Name", + "description": "The name of the student" + } + ], + "sd": "allowed" + }, + { + "path": ["address"], + "display": [ + { + "lang": "de-DE", + "label": "Adresse", + "description": "Adresse zum Zeitpunkt des Abschlusses" + }, + { + "lang": "en-US", + "label": "Address", + "description": "Address at the time of graduation" + } + ], + "sd": "always" + }, + { + "path": ["address", "street_address"], + "display": [ + { + "lang": "de-DE", + "label": "Straße" + }, + { + "lang": "en-US", + "label": "Street Address" + } + ], + "sd": "always", + "svg_id": "address_street_address" + }, + { + "path": ["degrees", null], + "display": [ + { + "lang": "de-DE", + "label": "Abschluss", + "description": "Der Abschluss des Studenten" + }, + { + "lang": "en-US", + "label": "Degree", + "description": "Degree earned by the student" + } + ], + "sd": "allowed" + } + ] +}`) + +const issuer_jwks = async (): Promise<[JWK, JWK]> => { + const [sk, pk] = await generateKeyPair("ES256").then(res => [res.privateKey, res.publicKey]); + const sk_jwk = await exportJWK(sk); + const pk_jwk = await exportJWK(pk); + + return [sk_jwk, pk_jwk]; +} + +export async function sdJwtVc() { + const hasher = new Sha256Hasher(); + const issuer = "https://example.com/"; + const [sk_jwk, pk_jwk] = await issuer_jwks(); + const issuer_public_jwk = { ...pk_jwk, kty: JwkType.Ec, kid: "key1" } as IJwk; + const issuer_metadata = new IssuerMetadata(issuer, { jwks: { keys: [issuer_public_jwk] } }) + const dummy_resolver = { + resolve: async (input: string) => { + if (input == "https://example.com/.well-known/jwt-vc-issuer/") return new TextEncoder().encode(JSON.stringify(issuer_metadata.toJSON())); + if (input == "https://example.com/.well-known/vct/education_credential") return new TextEncoder().encode(JSON.stringify(vc_metadata)); + } + } as IResolver; + + const sign = async (header: object, payload: object) => { + console.log(`about to sign payload ${typeof payload} with values ${JSON.stringify(payload)}`) + return new SignJWT(payload as JWTPayload) + .setProtectedHeader(header as JWTHeaderParameters) + .sign(sk_jwk) + .then(jws => new TextEncoder().encode(jws)) + } + + let sd_jwt_vc = await new SdJwtVcBuilder({ + name: "John Doe", + address: { + street_address: "A random street", + number: "3a", + }, + degree: [] + }, hasher) + .header({ kid: "key1" }) + .vct("https://example.com/education_credential") + .iat(Timestamp.nowUTC()) + .iss(issuer) + // .requireKeyBinding({ kid: "key2" }) + .makeConcealable("/address/street_address") + .makeConcealable("/address") + .finish({ sign }, "ES256"); + + console.log(sd_jwt_vc.toString()); + + + await sd_jwt_vc.validate(dummy_resolver, hasher); + + console.log("SdJwtVc is valid!"); +} \ No newline at end of file diff --git a/bindings/wasm/examples/src/main.ts b/bindings/wasm/examples/src/main.ts index 0a074d3fd2..9d21d07334 100644 --- a/bindings/wasm/examples/src/main.ts +++ b/bindings/wasm/examples/src/main.ts @@ -19,6 +19,7 @@ import { sdJwt } from "./1_advanced/6_sd_jwt"; import { statusList2021 } from "./1_advanced/7_status_list_2021"; import { zkp } from "./1_advanced/8_zkp"; import { zkp_revocation } from "./1_advanced/9_zkp_revocation"; +import { sdJwtVc } from "./1_advanced/10_sd_jwt_vc"; async function main() { // Extract example name. @@ -64,6 +65,8 @@ async function main() { return await zkp(); case "9_zkp_revocation": return await zkp_revocation(); + case "10_sd_jwt_vc": + return await sdJwtVc(); default: throw "Unknown example name: '" + argument + "'"; } diff --git a/bindings/wasm/package-lock.json b/bindings/wasm/package-lock.json index c6afb8ec91..61bc73ffe4 100644 --- a/bindings/wasm/package-lock.json +++ b/bindings/wasm/package-lock.json @@ -12,10 +12,13 @@ "@noble/ed25519": "^1.7.3", "@types/node-fetch": "^2.6.2", "base64-arraybuffer": "^1.0.2", + "jose": "^5.9.6", + "jsonwebtoken": "^9.0.2", "node-fetch": "^2.6.7" }, "devDependencies": { "@transmute/did-key-ed25519": "0.3.0-unstable.9", + "@types/jsonwebtoken": "^9.0.7", "@types/mocha": "^9.1.0", "big-integer": "^1.6.51", "copy-webpack-plugin": "^7.0.0", @@ -551,6 +554,16 @@ "dev": true, "optional": true }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.7.tgz", + "integrity": "sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/linkify-it": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.2.tgz", @@ -1290,6 +1303,12 @@ "node": "*" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -2239,6 +2258,15 @@ "safer-buffer": "^2.1.0" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.304", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.304.tgz", @@ -3256,6 +3284,15 @@ "node": ">= 10.13.0" } }, + "node_modules/jose": { + "version": "5.9.6", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.9.6.tgz", + "integrity": "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -3463,6 +3500,28 @@ "node": ">=10" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, "node_modules/jsprim": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", @@ -3478,6 +3537,27 @@ "verror": "1.10.0" } }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -3655,6 +3735,42 @@ "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", "dev": true }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, "node_modules/lodash.omit": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.5.0.tgz", @@ -3664,8 +3780,7 @@ "node_modules/lodash.once": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", - "dev": true + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" }, "node_modules/lodash.padend": { "version": "4.6.1", @@ -4586,8 +4701,7 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/nanoid": { "version": "3.3.1", @@ -5192,7 +5306,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -7030,6 +7143,15 @@ "dev": true, "optional": true }, + "@types/jsonwebtoken": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.7.tgz", + "integrity": "sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/linkify-it": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.2.tgz", @@ -7619,6 +7741,11 @@ "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", "dev": true }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -8338,6 +8465,14 @@ "safer-buffer": "^2.1.0" } }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, "electron-to-chromium": { "version": "1.4.304", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.304.tgz", @@ -9062,6 +9197,11 @@ "supports-color": "^8.0.0" } }, + "jose": { + "version": "5.9.6", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.9.6.tgz", + "integrity": "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==" + }, "js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -9232,6 +9372,23 @@ "node-fetch": "^2.6.1" } }, + "jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "requires": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + } + }, "jsprim": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", @@ -9244,6 +9401,25 @@ "verror": "1.10.0" } }, + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -9363,6 +9539,36 @@ "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", "dev": true }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, "lodash.omit": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.5.0.tgz", @@ -9372,8 +9578,7 @@ "lodash.once": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", - "dev": true + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" }, "lodash.padend": { "version": "4.6.1", @@ -9967,8 +10172,7 @@ "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "nanoid": { "version": "3.3.1", @@ -10406,8 +10610,7 @@ "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" }, "safer-buffer": { "version": "2.1.2", diff --git a/bindings/wasm/package.json b/bindings/wasm/package.json index 801aab239a..fb430a0acf 100644 --- a/bindings/wasm/package.json +++ b/bindings/wasm/package.json @@ -13,7 +13,7 @@ "build:src": "cargo build --lib --release --target wasm32-unknown-unknown", "bundle:nodejs": "~/.cargo/bin/wasm-bindgen target/wasm32-unknown-unknown/release/identity_wasm.wasm --typescript --weak-refs --target nodejs --out-dir node && node ./build/node && tsc --project ./lib/tsconfig.json && node ./build/replace_paths ./lib/tsconfig.json node", "bundle:web": "wasm-bindgen target/wasm32-unknown-unknown/release/identity_wasm.wasm --typescript --weak-refs --target web --out-dir web && node ./build/web && tsc --project ./lib/tsconfig.web.json && node ./build/replace_paths ./lib/tsconfig.web.json web", - "build:nodejs": "npm run build:src && npm run bundle:nodejs && wasm-opt -O node/identity_wasm_bg.wasm -o node/identity_wasm_bg.wasm", + "build:nodejs": "npm run build:src && npm run bundle:nodejs && wasm-opt -Oz node/identity_wasm_bg.wasm -o node/identity_wasm_bg.wasm", "build:web": "npm run build:src && npm run bundle:web && wasm-opt -O web/identity_wasm_bg.wasm -o web/identity_wasm_bg.wasm", "build:docs": "typedoc && npm run fix_docs", "build:examples:web": "tsc --project ./examples/tsconfig.web.json && node ./build/replace_paths ./examples/tsconfig.web.json ./examples/dist resolve", @@ -58,6 +58,7 @@ ], "devDependencies": { "@transmute/did-key-ed25519": "0.3.0-unstable.9", + "@types/jsonwebtoken": "^9.0.7", "@types/mocha": "^9.1.0", "big-integer": "^1.6.51", "copy-webpack-plugin": "^7.0.0", @@ -80,6 +81,7 @@ "@noble/ed25519": "^1.7.3", "@types/node-fetch": "^2.6.2", "base64-arraybuffer": "^1.0.2", + "jose": "^5.9.6", "node-fetch": "^2.6.7" }, "peerDependencies": { diff --git a/bindings/wasm/src/jose/jwk.rs b/bindings/wasm/src/jose/jwk.rs index 877ee30481..cf20245302 100644 --- a/bindings/wasm/src/jose/jwk.rs +++ b/bindings/wasm/src/jose/jwk.rs @@ -21,7 +21,8 @@ use crate::jose::WasmJwsAlgorithm; use core::ops::Deref; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -#[wasm_bindgen(js_name = Jwk, inspectable)] +#[serde(transparent)] +#[wasm_bindgen(js_name = Jwk)] pub struct WasmJwk(pub(crate) Jwk); #[wasm_bindgen(js_class = Jwk)] diff --git a/bindings/wasm/src/sd_jwt_vc/metadata/claim.rs b/bindings/wasm/src/sd_jwt_vc/metadata/claim.rs index 30c59e8174..b99c2b0993 100644 --- a/bindings/wasm/src/sd_jwt_vc/metadata/claim.rs +++ b/bindings/wasm/src/sd_jwt_vc/metadata/claim.rs @@ -1,6 +1,8 @@ // Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +use identity_iota::credential::sd_jwt_vc::metadata::ClaimDisplay; +use identity_iota::credential::sd_jwt_vc::metadata::ClaimMetadata; use wasm_bindgen::prelude::wasm_bindgen; #[wasm_bindgen(typescript_custom_section)] @@ -37,6 +39,20 @@ pub struct WasmClaimMetadata { pub svg_id: Option, } +impl From for ClaimMetadata { + fn from(value: WasmClaimMetadata) -> Self { + let path = serde_wasm_bindgen::from_value(value.path.into()).unwrap(); + let display = value.display.into_iter().map(ClaimDisplay::from).collect(); + let sd = value.sd.map(|sd| serde_wasm_bindgen::from_value(sd.into()).unwrap()); + Self { + path, + display, + sd, + svg_id: value.svg_id, + } + } +} + #[derive(Clone)] #[wasm_bindgen(js_name = ClaimDisplay, inspectable, getter_with_clone)] pub struct WasmClaimDisplay { @@ -47,3 +63,13 @@ pub struct WasmClaimDisplay { /// A human-readable description for the claim. pub description: Option, } + +impl From for ClaimDisplay { + fn from(value: WasmClaimDisplay) -> Self { + Self { + lang: value.lang, + label: value.label, + description: value.description, + } + } +} diff --git a/bindings/wasm/src/sd_jwt_vc/metadata/issuer.rs b/bindings/wasm/src/sd_jwt_vc/metadata/issuer.rs index 5a62f28b61..03c3e37e65 100644 --- a/bindings/wasm/src/sd_jwt_vc/metadata/issuer.rs +++ b/bindings/wasm/src/sd_jwt_vc/metadata/issuer.rs @@ -3,8 +3,10 @@ use identity_iota::core::Url; use identity_iota::credential::sd_jwt_vc::metadata::IssuerMetadata; +use serde::Serialize; use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::JsCast; +use wasm_bindgen::JsValue; use crate::error::Result; use crate::error::WasmResult; @@ -12,7 +14,7 @@ use crate::sd_jwt_vc::WasmSdJwtVc; #[wasm_bindgen(typescript_custom_section)] pub const I_JWKS: &str = r#" -type Jwks = { jwks_uri: string } | { jwks: { keys: Jwk[] }}; +type Jwks = { jwks_uri: string } | { jwks: { keys: IJwk[] }}; "#; #[wasm_bindgen] @@ -24,8 +26,6 @@ extern "C" { #[wasm_bindgen(js_name = IssuerMetadata)] pub struct WasmIssuerMetadata(pub(crate) IssuerMetadata); -impl_wasm_json!(WasmIssuerMetadata, IssuerMetadata); - #[wasm_bindgen(js_class = IssuerMetadata)] impl WasmIssuerMetadata { #[wasm_bindgen(constructor)] @@ -54,4 +54,10 @@ impl WasmIssuerMetadata { pub fn validate(&self, sd_jwt_vc: &WasmSdJwtVc) -> Result<()> { self.0.validate(&sd_jwt_vc.0).wasm_result() } + + #[wasm_bindgen(js_name = toJSON)] + pub fn to_json(&self) -> Result { + let js_serializer = serde_wasm_bindgen::Serializer::default().serialize_maps_as_objects(true); + self.0.serialize(&js_serializer).wasm_result() + } } diff --git a/bindings/wasm/src/sd_jwt_vc/metadata/vc_type.rs b/bindings/wasm/src/sd_jwt_vc/metadata/vc_type.rs index 6d804374d3..7af4fe71ba 100644 --- a/bindings/wasm/src/sd_jwt_vc/metadata/vc_type.rs +++ b/bindings/wasm/src/sd_jwt_vc/metadata/vc_type.rs @@ -13,17 +13,17 @@ use crate::sd_jwt_vc::resolver::ResolverUrlToValue; #[wasm_bindgen(typescript_custom_section)] const I_TYPE_METADATA: &str = r#" -type TypeSchema = { schema_uri: string, schema_uri#integrity: string | undefined } - | { schema: any, schema#integrity: string | undefined }; +type TypeSchema = { schema_uri: string, "schema_uri#integrity": string | undefined } + | { schema: any, "schema#integrity": string | undefined }; type TypeMetadataHelper = { name: string | undefined; description: string | undefined; extends: string | undefined; - extends#integrity: string | undefined; - display: any[]; - claims: ClaimMetadata[]; -} & TypeSchema; + "extends#integrity": string | undefined; + display: any[] | undefined; + claims: ClaimMetadata[] | undefined; +} & (TypeSchema | undefined); "#; #[wasm_bindgen] @@ -31,7 +31,7 @@ extern "C" { pub type TypeMetadataHelper; } -#[derive(Serialize)] +#[derive(Serialize, Clone)] #[serde(transparent)] #[wasm_bindgen(js_name = TypeMetadata)] pub struct WasmTypeMetadata(pub(crate) TypeMetadata); diff --git a/bindings/wasm/src/sd_jwt_vc/resolver.rs b/bindings/wasm/src/sd_jwt_vc/resolver.rs index 585b672618..b90a67b9d1 100644 --- a/bindings/wasm/src/sd_jwt_vc/resolver.rs +++ b/bindings/wasm/src/sd_jwt_vc/resolver.rs @@ -11,22 +11,22 @@ use wasm_bindgen::prelude::*; #[wasm_bindgen(typescript_custom_section)] const I_RESOLVER: &str = r#" -interface Resolver { - resolve: async (input: I) => T; +interface IResolver { + resolve: (input: I) => Promise; } "#; #[wasm_bindgen] extern "C" { // Resolver> - #[wasm_bindgen(typescript_type = "Resolver")] + #[wasm_bindgen(typescript_type = "IResolver")] pub type ResolverStringToUint8Array; #[wasm_bindgen(structural, method, catch)] pub async fn resolve(this: &ResolverStringToUint8Array, input: &str) -> Result; // Resolver - #[wasm_bindgen(typescript_type = "Resolver")] + #[wasm_bindgen(typescript_type = "IResolver")] pub type ResolverUrlToValue; #[wasm_bindgen(structural, method, catch)] @@ -47,6 +47,21 @@ where } } +#[async_trait(?Send)] +impl Resolver for ResolverStringToUint8Array +where + I: AsRef + Sync, +{ + async fn resolve(&self, input: &I) -> Result { + self + .resolve(input.as_ref()) + .await + .map(|arr| arr.to_vec()) + .map_err(|e| ErrorR::Generic(anyhow::anyhow!("{}", e.to_string()))) + .and_then(|bytes| serde_json::from_slice(&bytes).map_err(|e| ErrorR::ParsingFailure(e.into()))) + } +} + #[async_trait(?Send)] impl Resolver for ResolverUrlToValue { async fn resolve(&self, input: &Url) -> Result { diff --git a/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/disclosure.rs b/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/disclosure.rs index aa7b6d7a7b..b207fc0999 100644 --- a/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/disclosure.rs +++ b/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/disclosure.rs @@ -13,7 +13,7 @@ use crate::error::WasmResult; /// /// See: https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-07.html#name-disclosures #[derive(Clone)] -#[wasm_bindgen(js_name = DisclosureV2, getter_with_clone)] +#[wasm_bindgen(js_name = DisclosureV2, inspectable, getter_with_clone)] pub struct WasmDisclosure { pub salt: String, #[wasm_bindgen(js_name = claimName)] diff --git a/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/hasher.rs b/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/hasher.rs index 982e9a51d7..18016713f3 100644 --- a/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/hasher.rs +++ b/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/hasher.rs @@ -51,6 +51,12 @@ pub struct WasmSha256Hasher(pub(crate) Sha256Hasher); #[wasm_bindgen(js_class = Sha256Hasher)] impl WasmSha256Hasher { + #[allow(clippy::new_without_default)] + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + Self(Sha256Hasher) + } + #[wasm_bindgen(js_name = algName)] pub fn alg_name(&self) -> String { self.0.alg_name().to_owned() diff --git a/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/kb_jwt.rs b/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/kb_jwt.rs index ec4b83f710..52f1d7d5d0 100644 --- a/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/kb_jwt.rs +++ b/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/kb_jwt.rs @@ -69,6 +69,11 @@ impl WasmKeyBindingJwt { pub fn to_string(&self) -> String { self.0.to_string() } + + #[wasm_bindgen(js_name = "toJSON")] + pub fn to_json(&self) -> JsValue { + JsValue::from_str(&self.to_string()) + } } #[wasm_bindgen(js_name = KeyBindingJwtBuilder)] diff --git a/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/sd_jwt.rs b/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/sd_jwt.rs index 5c633bd675..c435d4397c 100644 --- a/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/sd_jwt.rs +++ b/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/sd_jwt.rs @@ -87,6 +87,17 @@ impl WasmSdJwt { pub fn presentation(&self) -> String { self.0.presentation() } + + #[wasm_bindgen(js_name = "toJSON")] + pub fn to_json(&self) -> JsValue { + JsValue::from_str(&self.presentation()) + } + + #[allow(clippy::inherent_to_string)] + #[wasm_bindgen(js_name = "toString")] + pub fn to_string(&self) -> JsValue { + JsValue::from_str(&self.presentation()) + } } #[wasm_bindgen(js_name = SdJwtPresentationBuilder)] diff --git a/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/signer.rs b/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/signer.rs index 60e7ae7fa6..b22bd7f2fa 100644 --- a/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/signer.rs +++ b/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/signer.rs @@ -7,6 +7,7 @@ use identity_iota::sd_jwt_rework::JwsSigner; use js_sys::Error as JsError; use js_sys::Object; use js_sys::Uint8Array; +use serde::Serialize as _; use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::JsCast; @@ -15,7 +16,7 @@ use crate::error::Result; #[wasm_bindgen(typescript_custom_section)] const I_JWS_SIGNER: &str = r#" interface JwsSigner { - sign: (header: any, payload: any) => Promise; + sign: (header: object, payload: object) => Promise; } "#; @@ -32,10 +33,14 @@ extern "C" { impl JwsSigner for WasmJwsSigner { type Error = String; async fn sign(&self, header: &JsonObject, payload: &JsonObject) -> std::result::Result, Self::Error> { - let header = serde_wasm_bindgen::to_value(header) + assert!(!payload.is_empty()); + let js_serializer = serde_wasm_bindgen::Serializer::new().serialize_maps_as_objects(true); + let header = header + .serialize(&js_serializer) .map_err(|e| format!("{e:?}"))? .unchecked_into(); - let payload = serde_wasm_bindgen::to_value(payload) + let payload = payload + .serialize(&js_serializer) .map_err(|e| format!("{e:?}"))? .unchecked_into(); diff --git a/bindings/wasm/src/sd_jwt_vc/token.rs b/bindings/wasm/src/sd_jwt_vc/token.rs index 03016a6ab4..abd227be60 100644 --- a/bindings/wasm/src/sd_jwt_vc/token.rs +++ b/bindings/wasm/src/sd_jwt_vc/token.rs @@ -1,9 +1,8 @@ // Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use anyhow::anyhow; -use anyhow::Context; use identity_iota::core::Url; +use identity_iota::credential::sd_jwt_vc::metadata::ClaimMetadata; use identity_iota::credential::sd_jwt_vc::vct_to_url as vct_to_url_impl; use identity_iota::credential::sd_jwt_vc::SdJwtVc; use wasm_bindgen::prelude::*; @@ -16,6 +15,7 @@ use crate::sd_jwt_vc::metadata::WasmTypeMetadata; use crate::verification::IJwsVerifier; use crate::verification::WasmJwsVerifier; +use super::metadata::WasmClaimMetadata; use super::metadata::WasmIssuerMetadata; use super::resolver::ResolverStringToUint8Array; use super::sd_jwt_v2::WasmHasher; @@ -23,16 +23,12 @@ use super::sd_jwt_v2::WasmSdJwt; use super::WasmSdJwtVcClaims; use super::WasmSdJwtVcPresentationBuilder; -#[wasm_bindgen] -extern "C" { - #[wasm_bindgen(typescript_type = "[TypeMetadata, Uint8Array]")] - pub type TypeMetadataResult; -} - #[derive(Clone)] #[wasm_bindgen(js_name = SdJwtVc)] pub struct WasmSdJwtVc(pub(crate) SdJwtVc); +impl_wasm_clone!(WasmSdJwtVc, SdJwtVc); + #[wasm_bindgen(js_class = "SdJwtVc")] impl WasmSdJwtVc { /// Parses a `string` into an {@link SdJwtVc}. @@ -64,25 +60,49 @@ impl WasmSdJwtVc { } #[wasm_bindgen(js_name = "typeMetadata")] - pub async fn type_metadata(&self, resolver: &ResolverStringToUint8Array) -> Result { + pub async fn type_metadata(&self, resolver: &ResolverStringToUint8Array) -> Result { self .0 .type_metadata(resolver) .await - .context("resolution error") - .and_then(|(metadata, integrity)| { - serde_wasm_bindgen::to_value(&(WasmTypeMetadata(metadata), integrity)).map_err(|e| anyhow!("{}", e.to_string())) - }) + .map(|(metadata, _)| WasmTypeMetadata(metadata)) .wasm_result() - .and_then(JsCast::dyn_into) } + /// Verifies this {@link SdJwtVc} JWT's signature. #[wasm_bindgen(js_name = "verifySignature")] - pub fn verify_signature(&self, jws_verifier: Option, jwk: &WasmJwk) -> Result<()> { + pub fn verify_signature(&self, jwk: &WasmJwk, jws_verifier: Option) -> Result<()> { let verifier = WasmJwsVerifier::new(jws_verifier); self.0.verify_signature(&verifier, &jwk.0).wasm_result() } + /// Checks the disclosability of this {@link SdJwtVc}'s claims against a list of {@link ClaimMetadata}. + /// ## Notes + /// This check should be performed by the token's holder in order to assert the issuer's compliance with + /// the credential's type. + #[wasm_bindgen(js_name = "validateClaimDisclosability")] + pub fn validate_claims_disclosability(&self, claims_metadata: Vec) -> Result<()> { + let claims_metadata = claims_metadata.into_iter().map(ClaimMetadata::from).collect::>(); + self.0.validate_claims_disclosability(&claims_metadata).wasm_result() + } + + /// Check whether this {@link SdJwtVc} is valid. + /// + /// This method checks: + /// - JWS signature + /// - credential's type + /// - claims' disclosability + #[wasm_bindgen] + pub async fn validate( + &self, + resolver: &ResolverStringToUint8Array, + hasher: &WasmHasher, + jws_verifier: Option, + ) -> Result<()> { + let jws_verifier = WasmJwsVerifier::new(jws_verifier); + self.0.validate(resolver, &jws_verifier, hasher).await.wasm_result() + } + /// Verify the signature of this {@link SdJwtVc}'s {@link KeyBindingJwt}. #[wasm_bindgen(js_name = "verifyKeyBinding")] pub fn verify_key_binding(&self, jws_verifier: Option, jwk: &WasmJwk) -> Result<()> { @@ -111,9 +131,10 @@ impl WasmSdJwtVc { } #[wasm_bindgen(js_name = "intoDisclosedObject")] - pub fn into_disclosed_object(self, hasher: &WasmHasher) -> Result { + pub fn into_disclosed_object(&self, hasher: &WasmHasher) -> Result { self .0 + .clone() .into_disclosed_object(hasher) .map(|obj| serde_wasm_bindgen::to_value(&obj).expect("JSON object is a valid JS object")) .map(JsCast::unchecked_into) @@ -124,10 +145,21 @@ impl WasmSdJwtVc { pub fn into_presentation(self, hasher: &WasmHasher) -> Result { WasmSdJwtVcPresentationBuilder::new(self, hasher) } + + #[wasm_bindgen(js_name = "toJSON")] + pub fn to_json(&self) -> JsValue { + JsValue::from_str(&self.0.to_string()) + } + + #[allow(clippy::inherent_to_string)] + #[wasm_bindgen(js_name = "toString")] + pub fn to_string(&self) -> JsValue { + JsValue::from_str(&self.0.to_string()) + } } #[wasm_bindgen(js_name = "vctToUrl")] pub fn vct_to_url(resource: &str) -> Option { let url = resource.parse::().ok()?; vct_to_url_impl(&url).map(|url| url.to_string()) -} +} \ No newline at end of file diff --git a/identity_credential/src/sd_jwt_vc/token.rs b/identity_credential/src/sd_jwt_vc/token.rs index ead1fff35d..0cc05514a1 100644 --- a/identity_credential/src/sd_jwt_vc/token.rs +++ b/identity_credential/src/sd_jwt_vc/token.rs @@ -155,7 +155,11 @@ impl SdJwtVc { jwk } else { // Issuer has no metadata that can lead to its JWK. Let's see if it can be resolved directly. - let jwk_uri = kid.parse::().map_err(|e| Error::Verification(e.into()))?; + let jwk_uri = kid.parse::().map_err(|_| { + Error::Verification(anyhow!( + "JWK's kid \"{kid}\" could not be found in JKW set and cannot be resolved" + )) + })?; resolver .resolve(&jwk_uri) .await @@ -235,7 +239,6 @@ impl SdJwtVc { R: Resolver>, R: Resolver>, R: Resolver, - R: Sync, V: JwsVerifier, { // Signature verification. diff --git a/identity_jose/src/jwk/key_set.rs b/identity_jose/src/jwk/key_set.rs index e1c9754a8a..22a629eaab 100644 --- a/identity_jose/src/jwk/key_set.rs +++ b/identity_jose/src/jwk/key_set.rs @@ -14,7 +14,6 @@ use crate::jwk::Jwk; /// /// [More Info](https://tools.ietf.org/html/rfc7517#section-5) #[derive(Clone, Debug, Default, PartialEq, Eq, serde::Deserialize, serde::Serialize)] -#[repr(transparent)] pub struct JwkSet { /// An array of JWK values. /// From 481965930047c8fd12815ad6c4ec7b4231e33bdd Mon Sep 17 00:00:00 2001 From: umr1352 Date: Mon, 23 Dec 2024 12:59:14 +0100 Subject: [PATCH 04/11] example & small fixes --- .../examples/src/1_advanced/10_sd_jwt_vc.ts | 49 ++++++++++++------- .../wasm/src/sd_jwt_vc/sd_jwt_v2/kb_jwt.rs | 4 +- bindings/wasm/src/sd_jwt_vc/token.rs | 11 ++++- 3 files changed, 44 insertions(+), 20 deletions(-) diff --git a/bindings/wasm/examples/src/1_advanced/10_sd_jwt_vc.ts b/bindings/wasm/examples/src/1_advanced/10_sd_jwt_vc.ts index d997bb31b0..069042afa3 100644 --- a/bindings/wasm/examples/src/1_advanced/10_sd_jwt_vc.ts +++ b/bindings/wasm/examples/src/1_advanced/10_sd_jwt_vc.ts @@ -1,8 +1,8 @@ // Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import { IssuerMetadata, SdJwtVcBuilder, Sha256Hasher, Timestamp, TypeMetadataHelper, IResolver, JwkType, IJwk, Jwk, IJwkParams } from "@iota/identity-wasm/node"; -import { base64url, exportJWK, generateKeyPair, JWK, JWTHeaderParameters, JWTPayload, SignJWT } from "jose"; +import { IssuerMetadata, SdJwtVcBuilder, Sha256Hasher, Timestamp, TypeMetadataHelper, IResolver, JwkType, IJwk, KeyBindingJwtBuilder, Jwk, IJwkParams, KeyBindingJWTValidationOptions, JwsVerificationOptions } from "@iota/identity-wasm/node"; +import { exportJWK, generateKeyPair, JWK, JWTHeaderParameters, JWTPayload, SignJWT } from "jose"; const vc_metadata: TypeMetadataHelper = JSON.parse(`{ "vct": "https://example.com/education_credential", @@ -75,7 +75,7 @@ const vc_metadata: TypeMetadataHelper = JSON.parse(`{ ] }`) -const issuer_jwks = async (): Promise<[JWK, JWK]> => { +const keypair_jwk = async (): Promise<[JWK, JWK]> => { const [sk, pk] = await generateKeyPair("ES256").then(res => [res.privateKey, res.publicKey]); const sk_jwk = await exportJWK(sk); const pk_jwk = await exportJWK(pk); @@ -83,11 +83,19 @@ const issuer_jwks = async (): Promise<[JWK, JWK]> => { return [sk_jwk, pk_jwk]; } +const signer = async (header: object, payload: object, sk_jwk: JWK) => { + return new SignJWT(payload as JWTPayload) + .setProtectedHeader(header as JWTHeaderParameters) + .sign(sk_jwk) + .then(jws => new TextEncoder().encode(jws)) +} + export async function sdJwtVc() { const hasher = new Sha256Hasher(); const issuer = "https://example.com/"; - const [sk_jwk, pk_jwk] = await issuer_jwks(); + const [sk_jwk, pk_jwk] = await keypair_jwk(); const issuer_public_jwk = { ...pk_jwk, kty: JwkType.Ec, kid: "key1" } as IJwk; + const issuer_signer = (header: object, payload: object) => signer(header, payload, sk_jwk); const issuer_metadata = new IssuerMetadata(issuer, { jwks: { keys: [issuer_public_jwk] } }) const dummy_resolver = { resolve: async (input: string) => { @@ -95,15 +103,11 @@ export async function sdJwtVc() { if (input == "https://example.com/.well-known/vct/education_credential") return new TextEncoder().encode(JSON.stringify(vc_metadata)); } } as IResolver; + const [holder_sk, holder_pk] = await keypair_jwk(); + const holder_public_jwk = { ...holder_pk, kty: JwkType.Ec, kid: "key2" } as IJwk; + const holder_signer = (header: object, payload: object) => signer(header, payload, holder_sk); - const sign = async (header: object, payload: object) => { - console.log(`about to sign payload ${typeof payload} with values ${JSON.stringify(payload)}`) - return new SignJWT(payload as JWTPayload) - .setProtectedHeader(header as JWTHeaderParameters) - .sign(sk_jwk) - .then(jws => new TextEncoder().encode(jws)) - } - + /// Issuer creates an SD-JWT VC. let sd_jwt_vc = await new SdJwtVcBuilder({ name: "John Doe", address: { @@ -116,15 +120,26 @@ export async function sdJwtVc() { .vct("https://example.com/education_credential") .iat(Timestamp.nowUTC()) .iss(issuer) - // .requireKeyBinding({ kid: "key2" }) + .requireKeyBinding({ kid: holder_public_jwk.kid }) .makeConcealable("/address/street_address") .makeConcealable("/address") - .finish({ sign }, "ES256"); + .finish({ sign: issuer_signer }, "ES256"); - console.log(sd_jwt_vc.toString()); + console.log(`issued SD-JWT VC: ${sd_jwt_vc.toString()}`); + // Holder receives its SD-JWT VC and attaches its keybinding JWT. + const kb_jwt = await new KeyBindingJwtBuilder() + .iat(Timestamp.nowUTC()) + .header({ kid: holder_public_jwk.kid }) + .nonce("abcdefghi") + .aud("https://example.com/verify") + .finish(sd_jwt_vc.asSdJwt(), "ES256", { sign: holder_signer }); + const { disclosures, sdJwtVc } = sd_jwt_vc.intoPresentation(hasher).attachKeyBindingJwt(kb_jwt).finish(); + console.log(`presented SD-JWT VC: ${sdJwtVc}`); - await sd_jwt_vc.validate(dummy_resolver, hasher); + // Verifier checks the presented sdJwtVc. + await sdJwtVc.validate(dummy_resolver, hasher); + sdJwtVc.validateKeyBinding(new Jwk(holder_public_jwk as IJwkParams), hasher, new KeyBindingJWTValidationOptions({ nonce: "abcdefghi", jwsOptions: new JwsVerificationOptions() })); - console.log("SdJwtVc is valid!"); + console.log("The presented SdJwtVc is valid!"); } \ No newline at end of file diff --git a/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/kb_jwt.rs b/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/kb_jwt.rs index 52f1d7d5d0..c0f28f9228 100644 --- a/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/kb_jwt.rs +++ b/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/kb_jwt.rs @@ -11,6 +11,7 @@ use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::JsCast; use wasm_bindgen::JsValue; +use crate::common::WasmTimestamp; use crate::error::Result; use crate::error::WasmResult; @@ -104,7 +105,8 @@ impl WasmKeyBindingJwtBuilder { } #[wasm_bindgen] - pub fn iat(self, iat: i64) -> Self { + pub fn iat(self, iat: WasmTimestamp) -> Self { + let iat = iat.0.to_unix(); Self(self.0.iat(iat)) } diff --git a/bindings/wasm/src/sd_jwt_vc/token.rs b/bindings/wasm/src/sd_jwt_vc/token.rs index abd227be60..9890aa59d2 100644 --- a/bindings/wasm/src/sd_jwt_vc/token.rs +++ b/bindings/wasm/src/sd_jwt_vc/token.rs @@ -1,6 +1,8 @@ // Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +use std::ops::Deref; + use identity_iota::core::Url; use identity_iota::credential::sd_jwt_vc::metadata::ClaimMetadata; use identity_iota::credential::sd_jwt_vc::vct_to_url as vct_to_url_impl; @@ -44,6 +46,11 @@ impl WasmSdJwtVc { .wasm_result() } + #[wasm_bindgen(js_name = "asSdJwt")] + pub fn as_sd_jwt(&self) -> WasmSdJwt { + WasmSdJwt(self.0.deref().clone()) + } + #[wasm_bindgen(js_name = "issuerJwk")] pub async fn issuer_jwk(&self, resolver: &ResolverStringToUint8Array) -> Result { self.0.issuer_jwk(resolver).await.map(WasmJwk).wasm_result() @@ -105,7 +112,7 @@ impl WasmSdJwtVc { /// Verify the signature of this {@link SdJwtVc}'s {@link KeyBindingJwt}. #[wasm_bindgen(js_name = "verifyKeyBinding")] - pub fn verify_key_binding(&self, jws_verifier: Option, jwk: &WasmJwk) -> Result<()> { + pub fn verify_key_binding(&self, jwk: &WasmJwk, jws_verifier: Option) -> Result<()> { let verifier = WasmJwsVerifier::new(jws_verifier); self.0.verify_key_binding(&verifier, &jwk.0).wasm_result() } @@ -113,10 +120,10 @@ impl WasmSdJwtVc { #[wasm_bindgen(js_name = "validateKeyBinding")] pub fn validate_key_binding( &self, - jws_verifier: Option, jwk: &WasmJwk, hasher: &WasmHasher, options: &WasmKeyBindingJWTValidationOptions, + jws_verifier: Option, ) -> Result<()> { let jws_verifier = WasmJwsVerifier::new(jws_verifier); self From 63e9d43391099d43b8783672508f13f4a9a77431 Mon Sep 17 00:00:00 2001 From: umr1352 Date: Mon, 23 Dec 2024 13:09:47 +0100 Subject: [PATCH 05/11] restore package.json --- bindings/wasm/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bindings/wasm/package.json b/bindings/wasm/package.json index fb430a0acf..4ace39969c 100644 --- a/bindings/wasm/package.json +++ b/bindings/wasm/package.json @@ -11,9 +11,9 @@ }, "scripts": { "build:src": "cargo build --lib --release --target wasm32-unknown-unknown", - "bundle:nodejs": "~/.cargo/bin/wasm-bindgen target/wasm32-unknown-unknown/release/identity_wasm.wasm --typescript --weak-refs --target nodejs --out-dir node && node ./build/node && tsc --project ./lib/tsconfig.json && node ./build/replace_paths ./lib/tsconfig.json node", + "bundle:nodejs": "wasm-bindgen target/wasm32-unknown-unknown/release/identity_wasm.wasm --typescript --weak-refs --target nodejs --out-dir node && node ./build/node && tsc --project ./lib/tsconfig.json && node ./build/replace_paths ./lib/tsconfig.json node", "bundle:web": "wasm-bindgen target/wasm32-unknown-unknown/release/identity_wasm.wasm --typescript --weak-refs --target web --out-dir web && node ./build/web && tsc --project ./lib/tsconfig.web.json && node ./build/replace_paths ./lib/tsconfig.web.json web", - "build:nodejs": "npm run build:src && npm run bundle:nodejs && wasm-opt -Oz node/identity_wasm_bg.wasm -o node/identity_wasm_bg.wasm", + "build:nodejs": "npm run build:src && npm run bundle:nodejs && wasm-opt -O node/identity_wasm_bg.wasm -o node/identity_wasm_bg.wasm", "build:web": "npm run build:src && npm run bundle:web && wasm-opt -O web/identity_wasm_bg.wasm -o web/identity_wasm_bg.wasm", "build:docs": "typedoc && npm run fix_docs", "build:examples:web": "tsc --project ./examples/tsconfig.web.json && node ./build/replace_paths ./examples/tsconfig.web.json ./examples/dist resolve", From f2a8c4d1ec55120626e041ed23d3adc72a3df2f5 Mon Sep 17 00:00:00 2001 From: Enrico Marconi <31142849+UMR1352@users.noreply.github.com> Date: Tue, 7 Jan 2025 09:42:54 +0100 Subject: [PATCH 06/11] Update bindings/wasm/src/sd_jwt_vc/builder.rs Co-authored-by: wulfraem --- bindings/wasm/src/sd_jwt_vc/builder.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/bindings/wasm/src/sd_jwt_vc/builder.rs b/bindings/wasm/src/sd_jwt_vc/builder.rs index 8b181b8640..b8606bccba 100644 --- a/bindings/wasm/src/sd_jwt_vc/builder.rs +++ b/bindings/wasm/src/sd_jwt_vc/builder.rs @@ -58,10 +58,12 @@ impl WasmSdJwtVcBuilder { Self(self.0.header(header)) } - /// Require a proof of possession of a given key from the holder. + /// Adds a decoy digest to the specified path. /// - /// This operation adds a JWT confirmation (`cnf`) claim as specified in - /// [RFC8300](https://www.rfc-editor.org/rfc/rfc7800.html#section-3). + /// `path` indicates the pointer to the value that will be concealed using the syntax of + /// [JSON pointer](https://datatracker.ietf.org/doc/html/rfc6901). + /// + /// Use `path` = "" to add decoys to the top level. #[wasm_bindgen(js_name = addDecoys)] pub fn add_decoys(self, path: &str, number_of_decoys: usize) -> Result { self.0.add_decoys(path, number_of_decoys).map(Self).wasm_result() From a9c30b4c645950e879f27fed4c79db296a47834b Mon Sep 17 00:00:00 2001 From: Enrico Marconi <31142849+UMR1352@users.noreply.github.com> Date: Tue, 7 Jan 2025 09:43:05 +0100 Subject: [PATCH 07/11] Update bindings/wasm/src/sd_jwt_vc/claims.rs Co-authored-by: wulfraem --- bindings/wasm/src/sd_jwt_vc/claims.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bindings/wasm/src/sd_jwt_vc/claims.rs b/bindings/wasm/src/sd_jwt_vc/claims.rs index 52093bec5f..77c2dc3a89 100644 --- a/bindings/wasm/src/sd_jwt_vc/claims.rs +++ b/bindings/wasm/src/sd_jwt_vc/claims.rs @@ -7,12 +7,12 @@ use wasm_bindgen::prelude::wasm_bindgen; const I_SD_JWT_VC_CLAIMS: &str = r#" interface ISdJwtVcClaims { iss: string; - nbf: string | undefined; - exp: string | undefined; vct: string; status: SdJwtVcStatus; - iat: string | undefined; - sub: string | undefined; + nbf?: string; + exp?: string; + iat?: string; + sub?: string; } type SdJwtVcClaims = ISdJwtVcClaims & SdJwtClaims; From bb12d0fab89404bdb1774e813e7e7c6254b03843 Mon Sep 17 00:00:00 2001 From: Enrico Marconi <31142849+UMR1352@users.noreply.github.com> Date: Tue, 7 Jan 2025 09:43:13 +0100 Subject: [PATCH 08/11] Update bindings/wasm/src/sd_jwt_vc/metadata/vc_type.rs Co-authored-by: wulfraem --- bindings/wasm/src/sd_jwt_vc/metadata/vc_type.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/bindings/wasm/src/sd_jwt_vc/metadata/vc_type.rs b/bindings/wasm/src/sd_jwt_vc/metadata/vc_type.rs index 7af4fe71ba..caa8d8b1a4 100644 --- a/bindings/wasm/src/sd_jwt_vc/metadata/vc_type.rs +++ b/bindings/wasm/src/sd_jwt_vc/metadata/vc_type.rs @@ -13,16 +13,17 @@ use crate::sd_jwt_vc::resolver::ResolverUrlToValue; #[wasm_bindgen(typescript_custom_section)] const I_TYPE_METADATA: &str = r#" -type TypeSchema = { schema_uri: string, "schema_uri#integrity": string | undefined } - | { schema: any, "schema#integrity": string | undefined }; +type TypeSchema = { schema_uri: string, "schema_uri#integrity"?: string } + | { schema: any, "schema#integrity"?: string }; type TypeMetadataHelper = { - name: string | undefined; - description: string | undefined; - extends: string | undefined; - "extends#integrity": string | undefined; - display: any[] | undefined; - claims: ClaimMetadata[] | undefined; + name?: string; + description?: string; + extends?: string; + "extends#integrity"?: string; + display?: any[]; + claims?: ClaimMetadata[]; +"#; } & (TypeSchema | undefined); "#; From 7dfac44345dc130d42984f12fd542fd35728d669 Mon Sep 17 00:00:00 2001 From: Enrico Marconi <31142849+UMR1352@users.noreply.github.com> Date: Tue, 7 Jan 2025 09:45:23 +0100 Subject: [PATCH 09/11] Update bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/builder.rs Co-authored-by: wulfraem --- bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/builder.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/builder.rs b/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/builder.rs index b6f6ca46c3..e030bbbb94 100644 --- a/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/builder.rs +++ b/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/builder.rs @@ -56,10 +56,12 @@ impl WasmSdJwtBuilder { self.0.insert_claim(key, value).map(Self).wasm_result() } - /// Require a proof of possession of a given key from the holder. + /// Adds a decoy digest to the specified path. /// - /// This operation adds a JWT confirmation (`cnf`) claim as specified in - /// [RFC8300](https://www.rfc-editor.org/rfc/rfc7800.html#section-3). + /// `path` indicates the pointer to the value that will be concealed using the syntax of + /// [JSON pointer](https://datatracker.ietf.org/doc/html/rfc6901). + /// + /// Use `path` = "" to add decoys to the top level. #[wasm_bindgen(js_name = addDecoys)] pub fn add_decoys(self, path: &str, number_of_decoys: usize) -> Result { self.0.add_decoys(path, number_of_decoys).map(Self).wasm_result() From 7b29b3179a7db7861ffcca7418689fee4fa58d28 Mon Sep 17 00:00:00 2001 From: Enrico Marconi <31142849+UMR1352@users.noreply.github.com> Date: Tue, 7 Jan 2025 09:45:32 +0100 Subject: [PATCH 10/11] Update bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/sd_jwt.rs Co-authored-by: wulfraem --- bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/sd_jwt.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/sd_jwt.rs b/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/sd_jwt.rs index c435d4397c..f71f1b927a 100644 --- a/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/sd_jwt.rs +++ b/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/sd_jwt.rs @@ -21,8 +21,8 @@ use super::WasmRequiredKeyBinding; const I_SD_JWT_CLAIMS: &str = r#" interface SdJwtClaims { _sd: string[]; - _sd_alg: string | undefined; - cnf: RequiredKeyBinding | undefined; + _sd_alg?: string; + cnf?: RequiredKeyBinding; [properties: string]: unknown; } "#; From 51d873674de05af82b326e4cc4f0a0e7b6e88a1b Mon Sep 17 00:00:00 2001 From: umr1352 Date: Tue, 7 Jan 2025 10:13:47 +0100 Subject: [PATCH 11/11] review comments --- bindings/wasm/src/sd_jwt_vc/metadata/vc_type.rs | 11 ++++++----- bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/kb_jwt.rs | 2 +- bindings/wasm/src/sd_jwt_vc/status.rs | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/bindings/wasm/src/sd_jwt_vc/metadata/vc_type.rs b/bindings/wasm/src/sd_jwt_vc/metadata/vc_type.rs index caa8d8b1a4..409cfdff07 100644 --- a/bindings/wasm/src/sd_jwt_vc/metadata/vc_type.rs +++ b/bindings/wasm/src/sd_jwt_vc/metadata/vc_type.rs @@ -13,18 +13,19 @@ use crate::sd_jwt_vc::resolver::ResolverUrlToValue; #[wasm_bindgen(typescript_custom_section)] const I_TYPE_METADATA: &str = r#" -type TypeSchema = { schema_uri: string, "schema_uri#integrity"?: string } - | { schema: any, "schema#integrity"?: string }; +type SchemaByUri = { schema_uri: string, "schema_uri#integrity"?: string }; +type SchemaByObject = { schema: unknown, "schema#integrity"?: string }; +type NoSchema = {}; +type TypeSchema = SchemaByUri | SchemaByObject | NoSchema; type TypeMetadataHelper = { name?: string; description?: string; extends?: string; "extends#integrity"?: string; - display?: any[]; + display?: unknown[]; claims?: ClaimMetadata[]; -"#; -} & (TypeSchema | undefined); +} & TypeSchema; "#; #[wasm_bindgen] diff --git a/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/kb_jwt.rs b/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/kb_jwt.rs index c0f28f9228..d483737237 100644 --- a/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/kb_jwt.rs +++ b/bindings/wasm/src/sd_jwt_vc/sd_jwt_v2/kb_jwt.rs @@ -24,7 +24,7 @@ type RequiredKeyBinding = { jwk: Jwk } | { jwe: string } | { kid: string } | { jwu: { jwu: string, kid: string }} - | any; + | unknown; "#; #[wasm_bindgen(typescript_custom_section)] diff --git a/bindings/wasm/src/sd_jwt_vc/status.rs b/bindings/wasm/src/sd_jwt_vc/status.rs index 0b22eaa634..6d4e4ba9a8 100644 --- a/bindings/wasm/src/sd_jwt_vc/status.rs +++ b/bindings/wasm/src/sd_jwt_vc/status.rs @@ -10,7 +10,7 @@ interface SdJwtVcStatusListRef { idx: number; } -type SdJwtVcStatus = { status_list: SdJwtVcStatusListRef } | any; +type SdJwtVcStatus = { status_list: SdJwtVcStatusListRef } | unknown; "#; #[wasm_bindgen]