diff --git a/.github/banner_identity.png b/.github/banner_identity.png
new file mode 100644
index 0000000000..64b13841d1
Binary files /dev/null and b/.github/banner_identity.png differ
diff --git a/.github/banner_identity.svg b/.github/banner_identity.svg
deleted file mode 100644
index 61ffbf9bcc..0000000000
--- a/.github/banner_identity.svg
+++ /dev/null
@@ -1,83 +0,0 @@
-
diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml
index c28cc91f1b..64c6b93546 100644
--- a/.github/workflows/build-and-test.yml
+++ b/.github/workflows/build-and-test.yml
@@ -100,6 +100,11 @@ jobs:
}}
steps:
+
+
+ - name: git configure long path
+ if: matrix.os == 'windows-latest'
+ run: git config --global core.longpaths true
- uses: actions/checkout@v3
- name: Ensure, OpenSSL is available in Windows
diff --git a/.github/workflows/clippy.yml b/.github/workflows/clippy.yml
index 018b46147e..e4724abc8d 100644
--- a/.github/workflows/clippy.yml
+++ b/.github/workflows/clippy.yml
@@ -35,7 +35,7 @@ jobs:
- name: Install wasm-bindgen-cli
uses: jetli/wasm-bindgen-action@24ba6f9fff570246106ac3f80f35185600c3f6c9
with:
- version: '0.2.100'
+ version: '0.2.102'
- name: core clippy check
uses: actions-rs-plus/clippy-check@b09a9c37c9df7db8b1a5d52e8fe8e0b6e3d574c4
diff --git a/.github/workflows/shared-build-wasm.yml b/.github/workflows/shared-build-wasm.yml
index 45673fb886..01660fcb75 100644
--- a/.github/workflows/shared-build-wasm.yml
+++ b/.github/workflows/shared-build-wasm.yml
@@ -57,7 +57,7 @@ jobs:
- name: Install wasm-bindgen-cli
uses: jetli/wasm-bindgen-action@24ba6f9fff570246106ac3f80f35185600c3f6c9
with:
- version: '0.2.100'
+ version: '0.2.102'
- name: Setup sccache
uses: './.github/actions/rust/sccache/setup-sccache'
diff --git a/Cargo.toml b/Cargo.toml
index 95e7483905..5baad04cb9 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -22,6 +22,7 @@ members = [
"identity_jose",
"identity_ecdsa_verifier",
"identity_eddsa_verifier",
+ "identity_pqc_verifier",
"examples",
]
@@ -29,14 +30,15 @@ exclude = ["bindings/wasm/identity_wasm", "bindings/grpc"]
[workspace.dependencies]
bls12_381_plus = { version = "0.8.17" }
-iota_interaction = { git = "https://github.com/iotaledger/product-core.git", tag = "v0.8.0", package = "iota_interaction" }
-iota_interaction_ts = { git = "https://github.com/iotaledger/product-core.git", tag = "v0.8.0", package = "iota_interaction_ts" }
-product_common = { git = "https://github.com/iotaledger/product-core.git", tag = "v0.8.0", package = "product_common" }
+iota_interaction = { git = "https://github.com/iotaledger/product-core.git", tag = "v0.8.3", package = "iota_interaction" }
+iota_interaction_ts = { git = "https://github.com/iotaledger/product-core.git", tag = "v0.8.3", package = "iota_interaction_ts" }
+product_common = { git = "https://github.com/iotaledger/product-core.git", tag = "v0.8.3", package = "product_common" }
serde = { version = "1.0", default-features = false, features = ["alloc", "derive"] }
serde_json = { version = "1.0", default-features = false }
+oqs = { version = "0.10", default-features = false, features = ["sigs", "std", "vendored"] }
strum = { version = "0.25", default-features = false, features = ["std", "derive"] }
thiserror = { version = "1.0", default-features = false }
-json-proof-token = { version = "0.3.5" }
+json-proof-token = { version = "0.4" }
zkryptium = { version = "0.2.2", default-features = false, features = ["bbsplus"] }
[workspace.lints.clippy]
diff --git a/README.md b/README.md
index 30cf08f6c5..51b8425e76 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-
+
diff --git a/bindings/grpc/Cargo.toml b/bindings/grpc/Cargo.toml
index 9145fdd455..ab0312aa05 100644
--- a/bindings/grpc/Cargo.toml
+++ b/bindings/grpc/Cargo.toml
@@ -24,7 +24,7 @@ identity_iota = { path = "../../identity_iota", features = ["resolver", "sd-jwt"
identity_jose = { path = "../../identity_jose" }
identity_storage = { path = "../../identity_storage", features = ["memstore"] }
identity_stronghold = { path = "../../identity_stronghold", features = ["send-sync-storage"] }
-iota-sdk = { git = "https://github.com/iotaledger/iota.git", package = "iota-sdk", tag = "v1.4.1" }
+iota-sdk = { git = "https://github.com/iotaledger/iota.git", package = "iota-sdk", tag = "v1.7.0" }
iota-sdk-legacy = { package = "iota-sdk", version = "1.1.2", features = ["stronghold"] }
prost = "0.13"
rand = "0.8.5"
diff --git a/bindings/wasm/identity_wasm/Cargo.toml b/bindings/wasm/identity_wasm/Cargo.toml
index a386774ec3..02b68ae779 100644
--- a/bindings/wasm/identity_wasm/Cargo.toml
+++ b/bindings/wasm/identity_wasm/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "identity_wasm"
-version = "1.6.0-beta.7"
+version = "1.6.0-beta.10"
authors = ["IOTA Stiftung"]
edition = "2021"
homepage = "https://www.iota.org"
@@ -23,14 +23,15 @@ console_error_panic_hook = { version = "0.1" }
fastcrypto = { git = "https://github.com/MystenLabs/fastcrypto", rev = "69d496c71fb37e3d22fe85e5bbfd4256d61422b9", package = "fastcrypto" }
identity_ecdsa_verifier = { path = "../../../identity_ecdsa_verifier", default-features = false, features = ["es256", "es256k"] }
identity_eddsa_verifier = { path = "../../../identity_eddsa_verifier", default-features = false, features = ["ed25519"] }
+iota-caip = { git = "https://github.com/iotaledger/iota-caip.git", features = ["serde"] }
# Remove iota-sdk dependency while working on issue #1445
iota-sdk = { version = "1.1.5", default-features = false, features = ["serde", "std"] }
-iota_interaction = { git = "https://github.com/iotaledger/product-core.git", tag = "v0.8.0", package = "iota_interaction", default-features = false }
-iota_interaction_ts = { git = "https://github.com/iotaledger/product-core.git", tag = "v0.8.0", package = "iota_interaction_ts" }
+iota_interaction = { git = "https://github.com/iotaledger/product-core.git", tag = "v0.8.3", package = "iota_interaction", default-features = false }
+iota_interaction_ts = { git = "https://github.com/iotaledger/product-core.git", tag = "v0.8.3", package = "iota_interaction_ts" }
js-sys = { version = "0.3.61" }
-json-proof-token = "0.3.4"
+json-proof-token = "0.4"
proc_typescript = { version = "0.1.0", path = "./proc_typescript" }
-product_common = { git = "https://github.com/iotaledger/product-core.git", tag = "v0.8.0", package = "product_common", features = ["core-client", "transaction", "bindings", "gas-station", "default-http-client"] }
+product_common = { git = "https://github.com/iotaledger/product-core.git", tag = "v0.8.3", package = "product_common", features = ["core-client", "transaction", "bindings", "gas-station", "default-http-client"] }
secret-storage = { git = "https://github.com/iotaledger/secret-storage.git", default-features = false, tag = "v0.3.0" }
serde = { version = "1.0", features = ["derive"] }
serde-wasm-bindgen = "0.6.5"
@@ -39,7 +40,7 @@ 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.
tokio = { version = "1.46.1", default-features = false, features = ["sync"] }
tsify = "0.4.5"
-wasm-bindgen = { version = "0.2.100", features = ["serde-serialize"] }
+wasm-bindgen = { version = "=0.2.102", features = ["serde-serialize"] }
wasm-bindgen-futures = { version = "0.4", default-features = false }
[dependencies.identity_iota]
@@ -54,8 +55,11 @@ features = [
"sd-jwt-vc",
"status-list-2021",
"jpt-bbs-plus",
+ "pqc",
+ "hybrid",
"gas-station",
"default-http-client",
+ "irl",
]
[target.'cfg(all(target_arch = "wasm32", not(target_os = "wasi")))'.dependencies]
diff --git a/bindings/wasm/identity_wasm/examples/README.md b/bindings/wasm/identity_wasm/examples/README.md
index a55d044d2b..81af67422c 100644
--- a/bindings/wasm/identity_wasm/examples/README.md
+++ b/bindings/wasm/identity_wasm/examples/README.md
@@ -1,4 +1,4 @@
-
+
## IOTA Identity Examples
diff --git a/bindings/wasm/identity_wasm/examples/src/1_advanced/13_linked_verifiable_presentation.ts b/bindings/wasm/identity_wasm/examples/src/1_advanced/13_linked_verifiable_presentation.ts
new file mode 100644
index 0000000000..3cf7da5d79
--- /dev/null
+++ b/bindings/wasm/identity_wasm/examples/src/1_advanced/13_linked_verifiable_presentation.ts
@@ -0,0 +1,151 @@
+// Copyright 2020-2025 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+import {
+ CoreDID,
+ Credential,
+ EdDSAJwsVerifier,
+ IdentityClientReadOnly,
+ IotaDocument,
+ IrlResolver,
+ JwsSignatureOptions,
+ Jwt,
+ JwtPresentationOptions,
+ JwtPresentationValidationOptions,
+ JwtPresentationValidator,
+ LinkedVerifiablePresentationService,
+ Presentation,
+ Resolver,
+ Storage,
+} from "@iota/identity-wasm/node";
+import { IotaClient } from "@iota/iota-sdk/client";
+import { OnChainNotarization } from "@iota/notarization/node";
+import {
+ createDocumentForNetwork,
+ getFundedClient,
+ getMemstorage,
+ getNotarizationClient,
+ IOTA_IDENTITY_PKG_ID,
+ NETWORK_URL,
+ TEST_GAS_BUDGET,
+} from "../util";
+
+/**
+ * This example shows how to create a Verifiable Presentation and validate it.
+ * A Verifiable Presentation is the format in which a (collection of) Verifiable Credential(s) gets shared.
+ * It is signed by the subject, to prove control over the Verifiable Credential with a nonce or timestamp.
+ */
+export async function linkedVp() {
+ // ===========================================================================
+ // Create identities and clients.
+ // ===========================================================================
+
+ // Create new client to connect to IOTA network
+ const iotaClient = new IotaClient({ url: NETWORK_URL });
+ const network = await iotaClient.getChainIdentifier();
+
+ // Create issuer account, create identity, and publish DID document for it
+ const storage = getMemstorage();
+ const identityClient = await getFundedClient(storage);
+ const [unpublishedDidDocument, fragment] = await createDocumentForNetwork(storage, network);
+ const notarizationClient = await getNotarizationClient(identityClient.signer());
+ const publishedDidDocument = await identityClient
+ .publishDidDocument(unpublishedDidDocument, identityClient.senderAddress())
+ .buildAndExecute(identityClient)
+ .then(res => res.output);
+
+ // ===========================================================================
+ // Create a Verifiable Presentation and host it on-chain.
+ // ===========================================================================
+
+ const jwtVp = await makeVpJwt(publishedDidDocument, storage, fragment);
+ const notarizedVp: OnChainNotarization = await notarizationClient
+ .createLocked()
+ .withStringState(jwtVp.toString(), "My Linked VP")
+ .finish()
+ .buildAndExecute(notarizationClient)
+ .then(res => res.output);
+
+ // ===========================================================================
+ // Create Linked Verifiable Presentation service.
+ // ===========================================================================
+
+ const serviceUrl = publishedDidDocument.id().join("#linked-vp");
+ const linkedVpService = new LinkedVerifiablePresentationService({
+ id: serviceUrl,
+ linkedVp: notarizedVp.iotaResourceLocatorBuilder(notarizationClient.network()).data(),
+ });
+ publishedDidDocument.insertService(linkedVpService.toService());
+
+ await identityClient.publishDidDocumentUpdate(publishedDidDocument, TEST_GAS_BUDGET);
+
+ // ===========================================================================
+ // Verification.
+ // ===========================================================================
+
+ const resolver = new Resolver({
+ client: await IdentityClientReadOnly.createWithPkgId(iotaClient, IOTA_IDENTITY_PKG_ID),
+ });
+ // Resolve the presentation holder.
+ const presentationHolderDID: CoreDID = JwtPresentationValidator.extractHolder(jwtVp);
+ const resolvedHolder = await resolver.resolve(
+ presentationHolderDID.toString(),
+ );
+
+ // Get the Linked Verifiable Presentation Services from the DID Document.
+ const linkedVpServices = resolvedHolder.service().filter(service =>
+ service.type().includes("LinkedVerifiablePresentation")
+ );
+ console.assert(linkedVpServices.length == 1, "expected exactly one Linked Verifiable Presentation service");
+
+ // Get the VPs included the service.
+ const vpUrl = LinkedVerifiablePresentationService.fromService(linkedVpServices[0]).verifiablePresentationUrls()[0];
+ console.log(`Fetching VP at \`${vpUrl}\``);
+
+ const irlResolver = new IrlResolver({ customNetworks: [{ chainId: network, endpoint: NETWORK_URL }] });
+ const resolvedJwtVp = await irlResolver.resolve(vpUrl).then(value => new Jwt(value as string));
+
+ // Validate presentation. Note that this doesn't validate the included credentials.
+ const _decodedPresentation = new JwtPresentationValidator(new EdDSAJwsVerifier()).validate(
+ resolvedJwtVp,
+ resolvedHolder,
+ new JwtPresentationValidationOptions(),
+ );
+
+ console.log("Successfully validated the fetched presentation");
+}
+
+async function makeVpJwt(didDocument: IotaDocument, storage: Storage, fragment: string): Promise {
+ const credential = new Credential({
+ id: "https://example.edu/credentials/3732",
+ type: "UniversityDegreeCredential",
+ issuer: didDocument.id(),
+ credentialSubject: {
+ id: didDocument.id(),
+ name: "Alice",
+ degreeName: "Bachelor of Science and Arts",
+ degreeType: "BachelorDegree",
+ GPA: "4.0",
+ },
+ });
+ const jwtCredential = await didDocument.createCredentialJwt(
+ storage,
+ fragment,
+ credential,
+ new JwsSignatureOptions(),
+ );
+
+ const presentation = new Presentation({
+ holder: didDocument.id(),
+ verifiableCredential: [jwtCredential],
+ });
+ const jwtVp = await didDocument.createPresentationJwt(
+ storage,
+ fragment,
+ presentation,
+ new JwsSignatureOptions(),
+ new JwtPresentationOptions(),
+ );
+
+ return jwtVp;
+}
diff --git a/bindings/wasm/identity_wasm/examples/src/1_advanced/8_zkp.ts b/bindings/wasm/identity_wasm/examples/src/1_advanced/8_zkp.ts
index a1bb5bbc67..51904fa7ad 100644
--- a/bindings/wasm/identity_wasm/examples/src/1_advanced/8_zkp.ts
+++ b/bindings/wasm/identity_wasm/examples/src/1_advanced/8_zkp.ts
@@ -19,6 +19,53 @@ import {
import { IotaClient } from "@iota/iota-sdk/client";
import { getFundedClient, getMemstorage, IOTA_IDENTITY_PKG_ID, NETWORK_URL } from "../util";
+/** Creates a DID Document and publishes it in a new Alias Output.
+
+Its functionality is equivalent to the "create DID" example
+and exists for convenient calling from the other examples. */
+export async function createDid(client: Client, secretManager: SecretManagerType, storage: Storage): Promise<{
+ address: Address;
+ document: IotaDocument;
+ fragment: string;
+}> {
+ const didClient = new IotaIdentityClient(client);
+ const networkHrp: string = await didClient.getNetworkHrp();
+
+ const secretManagerInstance = new SecretManager(secretManager);
+ const walletAddressBech32 = (await secretManagerInstance.generateEd25519Addresses({
+ accountIndex: 0,
+ range: {
+ start: 0,
+ end: 1,
+ },
+ bech32Hrp: networkHrp,
+ }))[0];
+
+ console.log("Wallet address Bech32:", walletAddressBech32);
+
+ await ensureAddressHasFunds(client, walletAddressBech32);
+
+ const address: Address = Utils.parseBech32Address(walletAddressBech32);
+
+ // Create a new DID document with a placeholder DID.
+ // The DID will be derived from the Alias Id of the Alias Output after publishing.
+ const document = new IotaDocument(networkHrp);
+
+ const fragment = await document.generateMethodJwp(
+ storage,
+ ProofAlgorithm.BBS,
+ undefined,
+ MethodScope.VerificationMethod(),
+ );
+ // Construct an Alias Output containing the DID document, with the wallet address
+ // set as both the state controller and governor.
+ const aliasOutput: AliasOutput = await didClient.newDidOutput(address, document);
+
+ // Publish the Alias Output and get the published DID document.
+ const published = await didClient.publishDidOutput(secretManager, aliasOutput);
+
+ return { address, document: published, fragment };
+}
export async function zkp() {
// ===========================================================================
// Step 1: Create identity for the issuer.
diff --git a/bindings/wasm/identity_wasm/examples/src/1_advanced/hybrid.ts b/bindings/wasm/identity_wasm/examples/src/1_advanced/hybrid.ts
new file mode 100644
index 0000000000..f0d262e08d
--- /dev/null
+++ b/bindings/wasm/identity_wasm/examples/src/1_advanced/hybrid.ts
@@ -0,0 +1,231 @@
+// Copyright 2024 Fondazione Links
+// SPDX-License-Identifier: Apache-2.0
+
+import {
+ CompositeAlgId,
+ CoreDID,
+ Credential,
+ Duration,
+ EdDSAJwsVerifier,
+ FailFast,
+ IotaDocument,
+ JwkPqMemStore,
+ JwsSignatureOptions,
+ JwsVerificationOptions,
+ Jwt,
+ JwtCredentialValidationOptions,
+ JwtCredentialValidator,
+ JwtCredentialValidatorHybrid,
+ JwtPresentationOptions,
+ JwtPresentationValidationOptions,
+ JwtPresentationValidator,
+ JwtPresentationValidatorHybrid,
+ KeyIdMemStore,
+ MethodScope,
+ PQJwsVerifier,
+ Presentation,
+ Resolver,
+ Storage,
+ SubjectHolderRelationship,
+ Timestamp,
+} from "@iota/identity-wasm/node";
+import { IotaClient } from "@iota/iota-sdk/client";
+import { getFundedClient, NETWORK_URL } from "../util";
+
+/**
+ * This example shows how to create an hybrid Verifiable Presentation and validate it
+ */
+export async function hybrid() {
+ // ===========================================================================
+ // Step 1: Create identities for the issuer and the holder.
+ // ===========================================================================
+
+ const issuerStorage = new Storage(new JwkPqMemStore(), new KeyIdMemStore());
+ const issuerClient = await getFundedClient(issuerStorage);
+ const issuerDocument = new IotaDocument(issuerClient.network());
+ // Create a new method with PQ/T algorithm.
+ const issuerFragment = await issuerDocument.generateMethodHybrid(
+ issuerStorage,
+ CompositeAlgId.IdMldsa44Ed25519,
+ "#0",
+ MethodScope.VerificationMethod(),
+ );
+ const issuerIdentity = await issuerClient
+ .createIdentity(issuerDocument)
+ .finish()
+ .buildAndExecute(issuerClient)
+ .then(res => res.output);
+
+ const aliceStorage = new Storage(new JwkPqMemStore(), new KeyIdMemStore());
+ const aliceClient = await getFundedClient(aliceStorage);
+ const aliceDocument = new IotaDocument(aliceClient.network());
+ // Create a new method with PQ/T algorithm.
+ const aliceFragment = await aliceDocument.generateMethodHybrid(
+ aliceStorage,
+ CompositeAlgId.IdMldsa44Ed25519,
+ "#0",
+ MethodScope.VerificationMethod(),
+ );
+ const aliceIdentity = await aliceClient
+ .createIdentity(aliceDocument)
+ .finish()
+ .buildAndExecute(aliceClient)
+ .then(res => res.output);
+
+ // ===========================================================================
+ // Step 2: Issuer creates and signs a Verifiable Credential.
+ // ===========================================================================
+
+ const subject = {
+ id: aliceDocument.id(),
+ name: "Alice",
+ degreeName: "Bachelor of Science and Arts",
+ degreeType: "BachelorDegree",
+ GPA: "4.0",
+ };
+
+ // Create an unsigned `UniversityDegree` credential for Alice
+ const unsignedVc = new Credential({
+ id: "https://example.edu/credentials/3732",
+ type: "UniversityDegreeCredential",
+ issuer: issuerDocument.id(),
+ credentialSubject: subject,
+ });
+
+ // Create a Credential JWT with the issuer's hybrid verification method.
+ const credentialJwt = await issuerDocument.createCredentialJwtHybrid(
+ issuerStorage,
+ issuerFragment,
+ unsignedVc,
+ new JwsSignatureOptions(),
+ );
+
+ const res = new JwtCredentialValidatorHybrid(new EdDSAJwsVerifier(), new PQJwsVerifier()).validate(
+ credentialJwt,
+ issuerDocument,
+ new JwtCredentialValidationOptions(),
+ FailFast.FirstError,
+ );
+ console.log("credentialjwt validation", res.intoCredential());
+
+ // ===========================================================================
+ // Step 3: Issuer sends the Verifiable Credential to the holder.
+ // ===========================================================================
+
+ // The credential is then serialized to JSON and transmitted to the holder in a secure manner.
+ // Note that the credential is NOT published to the IOTA Tangle. It is sent and stored off-chain.
+ console.log(`Sending credential (as JWT) to the holder`, unsignedVc.toJSON());
+
+ // ===========================================================================
+ // Step 4: Verifier sends the holder a challenge and requests a signed Verifiable Presentation.
+ // ===========================================================================
+
+ // A unique random challenge generated by the requester per presentation can mitigate replay attacks.
+ const nonce = "475a7984-1bb5-4c4c-a56f-822bccd46440";
+
+ // The verifier and holder also agree that the signature should have an expiry date
+ // 10 minutes from now.
+ const expires = Timestamp.nowUTC().checkedAdd(Duration.minutes(10));
+
+ // ===========================================================================
+ // Step 5: Holder creates a verifiable presentation from the issued credential for the verifier to validate.
+ // ===========================================================================
+
+ // Create a Verifiable Presentation from the Credential
+ const unsignedVp = new Presentation({
+ holder: aliceDocument.id(),
+ verifiableCredential: [credentialJwt],
+ });
+
+ // Create a Hybrid JWT verifiable presentation using the holder's verification method
+ // and include the requested challenge and expiry timestamp.
+ const presentationJwt = await aliceDocument.createPresentationJwtHybrid(
+ aliceStorage,
+ aliceFragment,
+ unsignedVp,
+ new JwsSignatureOptions({ nonce }),
+ new JwtPresentationOptions({ expirationDate: expires }),
+ );
+
+ // ===========================================================================
+ // Step 6: Holder sends a verifiable presentation to the verifier.
+ // ===========================================================================
+ console.log(
+ `Sending presentation (as JWT) to the verifier`,
+ unsignedVp.toJSON(),
+ );
+
+ // ===========================================================================
+ // Step 7: Verifier receives the Verifiable Presentation and verifies it.
+ // ===========================================================================
+
+ // The verifier wants the following requirements to be satisfied:
+ // - JWT verification of the presentation (including checking the requested challenge to mitigate replay attacks)
+ // - JWT verification of the credentials.
+ // - The presentation holder must always be the subject, regardless of the presence of the nonTransferable property
+ // - The issuance date must not be in the future.
+
+ const jwtPresentationValidationOptions = new JwtPresentationValidationOptions(
+ {
+ presentationVerifierOptions: new JwsVerificationOptions({ nonce }),
+ },
+ );
+
+ const resolver = new Resolver({
+ client: aliceClient,
+ });
+ // Resolve the presentation holder.
+ const presentationHolderDID: CoreDID = JwtPresentationValidator.extractHolder(presentationJwt);
+ const resolvedHolder = await resolver.resolve(
+ presentationHolderDID.toString(),
+ );
+
+ // Validate presentation. Note that this doesn't validate the included credentials.
+ let decodedPresentation = new JwtPresentationValidatorHybrid(new EdDSAJwsVerifier(), new PQJwsVerifier()).validate(
+ presentationJwt,
+ resolvedHolder,
+ jwtPresentationValidationOptions,
+ );
+
+ // Validate the hybrid credentials in the presentation.
+ let credentialValidator = new JwtCredentialValidatorHybrid(new EdDSAJwsVerifier(), new PQJwsVerifier());
+ let validationOptions = new JwtCredentialValidationOptions({
+ subjectHolderRelationship: [
+ presentationHolderDID.toString(),
+ SubjectHolderRelationship.AlwaysSubject,
+ ],
+ });
+
+ let jwtCredentials: Jwt[] = decodedPresentation
+ .presentation()
+ .verifiableCredential()
+ .map((credential) => {
+ const jwt = credential.tryIntoJwt();
+ if (!jwt) {
+ throw new Error("expected a JWT credential");
+ } else {
+ return jwt;
+ }
+ });
+
+ // Concurrently resolve the issuers' documents.
+ let issuers: string[] = [];
+ for (let jwtCredential of jwtCredentials) {
+ let issuer = JwtCredentialValidator.extractIssuerFromJwt(jwtCredential);
+ issuers.push(issuer.toString());
+ }
+ let resolvedIssuers = await resolver.resolveMultiple(issuers);
+
+ // Validate the credentials in the presentation.
+ for (let i = 0; i < jwtCredentials.length; i++) {
+ credentialValidator.validate(
+ jwtCredentials[i],
+ resolvedIssuers[i],
+ validationOptions,
+ FailFast.FirstError,
+ );
+ }
+
+ // Since no errors were thrown we know that the validation was successful.
+ console.log(`VP successfully validated`);
+}
diff --git a/bindings/wasm/identity_wasm/examples/src/1_advanced/pq.ts b/bindings/wasm/identity_wasm/examples/src/1_advanced/pq.ts
new file mode 100644
index 0000000000..ffd0bf4c69
--- /dev/null
+++ b/bindings/wasm/identity_wasm/examples/src/1_advanced/pq.ts
@@ -0,0 +1,230 @@
+// Copyright 2024 Fondazione Links
+// SPDX-License-Identifier: Apache-2.0
+
+import {
+ CompositeAlgId,
+ CoreDID,
+ Credential,
+ Duration,
+ FailFast,
+ IotaDocument,
+ JwkPqMemStore,
+ JwsAlgorithm,
+ JwsSignatureOptions,
+ JwsVerificationOptions,
+ Jwt,
+ JwtCredentialValidationOptions,
+ JwtCredentialValidator,
+ JwtPresentationOptions,
+ JwtPresentationValidationOptions,
+ JwtPresentationValidator,
+ KeyIdMemStore,
+ MethodScope,
+ PQJwsVerifier,
+ Presentation,
+ Resolver,
+ Storage,
+ SubjectHolderRelationship,
+ Timestamp,
+} from "@iota/identity-wasm/node";
+import { getFundedClient } from "../util";
+
+/**
+ * This example shows how to create a PQ Verifiable Presentation and validate it
+ */
+export async function pq() {
+ // ===========================================================================
+ // Step 1: Create identities for the issuer and the holder.
+ // ===========================================================================
+
+ const issuerStorage = new Storage(new JwkPqMemStore(), new KeyIdMemStore());
+ const issuerClient = await getFundedClient(issuerStorage);
+ const issuerDocument = new IotaDocument(issuerClient.network());
+ // Create a new method with PQ/T algorithm.
+ const issuerFragment = await issuerDocument.generateMethodPQC(
+ issuerStorage,
+ JwkPqMemStore.mldsaKeyType(),
+ JwsAlgorithm.MLDSA44,
+ "#0",
+ MethodScope.VerificationMethod(),
+ );
+ const issuerIdentity = await issuerClient
+ .createIdentity(issuerDocument)
+ .finish()
+ .buildAndExecute(issuerClient)
+ .then(res => res.output);
+
+ const aliceStorage = new Storage(new JwkPqMemStore(), new KeyIdMemStore());
+ const aliceClient = await getFundedClient(aliceStorage);
+ const aliceDocument = new IotaDocument(aliceClient.network());
+ // Create a new method with PQ/T algorithm.
+ const aliceFragment = await aliceDocument.generateMethodPQC(
+ aliceStorage,
+ JwkPqMemStore.mldsaKeyType(),
+ JwsAlgorithm.MLDSA44,
+ "#0",
+ MethodScope.VerificationMethod(),
+ );
+ const aliceIdentity = await aliceClient
+ .createIdentity(aliceDocument)
+ .finish()
+ .buildAndExecute(aliceClient)
+ .then(res => res.output);
+
+ // ===========================================================================
+ // Step 2: Issuer creates and signs a Verifiable Credential.
+ // ===========================================================================
+
+ const subject = {
+ id: aliceDocument.id(),
+ name: "Alice",
+ degreeName: "Bachelor of Science and Arts",
+ degreeType: "BachelorDegree",
+ GPA: "4.0",
+ };
+
+ // Create an unsigned `UniversityDegree` credential for Alice
+ const unsignedVc = new Credential({
+ id: "https://example.edu/credentials/3732",
+ type: "UniversityDegreeCredential",
+ issuer: issuerDocument.id(),
+ credentialSubject: subject,
+ });
+
+ // Create a Credential JWT with the issuer's PQ verification method.
+ const credentialJwt = await issuerDocument.createCredentialJwtPqc(
+ issuerStorage,
+ issuerFragment,
+ unsignedVc,
+ new JwsSignatureOptions(),
+ );
+
+ const res = new JwtCredentialValidator(new PQJwsVerifier()).validate(
+ credentialJwt,
+ issuerDocument,
+ new JwtCredentialValidationOptions(),
+ FailFast.FirstError,
+ );
+ console.log("credentialjwt validation", res.intoCredential());
+
+ // ===========================================================================
+ // Step 3: Issuer sends the Verifiable Credential to the holder.
+ // ===========================================================================
+
+ // The credential is then serialized to JSON and transmitted to the holder in a secure manner.
+ // Note that the credential is NOT published to the IOTA Tangle. It is sent and stored off-chain.
+ console.log(`Sending credential (as JWT) to the holder`, unsignedVc.toJSON());
+
+ // ===========================================================================
+ // Step 4: Verifier sends the holder a challenge and requests a signed Verifiable Presentation.
+ // ===========================================================================
+
+ // A unique random challenge generated by the requester per presentation can mitigate replay attacks.
+ const nonce = "475a7984-1bb5-4c4c-a56f-822bccd46440";
+
+ // The verifier and holder also agree that the signature should have an expiry date
+ // 10 minutes from now.
+ const expires = Timestamp.nowUTC().checkedAdd(Duration.minutes(10));
+
+ // ===========================================================================
+ // Step 5: Holder creates a verifiable presentation from the issued credential for the verifier to validate.
+ // ===========================================================================
+
+ // Create a Verifiable Presentation from the Credential
+ const unsignedVp = new Presentation({
+ holder: aliceDocument.id(),
+ verifiableCredential: [credentialJwt],
+ });
+
+ // Create a PQ JWT verifiable presentation using the holder's verification method
+ // and include the requested challenge and expiry timestamp.
+ const presentationJwt = await aliceDocument.createPresentationJwtPqc(
+ aliceStorage,
+ aliceFragment,
+ unsignedVp,
+ new JwsSignatureOptions({ nonce }),
+ new JwtPresentationOptions({ expirationDate: expires }),
+ );
+
+ // ===========================================================================
+ // Step 6: Holder sends a verifiable presentation to the verifier.
+ // ===========================================================================
+ console.log(
+ `Sending presentation (as JWT) to the verifier`,
+ unsignedVp.toJSON(),
+ );
+
+ // ===========================================================================
+ // Step 7: Verifier receives the Verifiable Presentation and verifies it.
+ // ===========================================================================
+
+ // The verifier wants the following requirements to be satisfied:
+ // - JWT verification of the presentation (including checking the requested challenge to mitigate replay attacks)
+ // - JWT verification of the credentials.
+ // - The presentation holder must always be the subject, regardless of the presence of the nonTransferable property
+ // - The issuance date must not be in the future.
+
+ const jwtPresentationValidationOptions = new JwtPresentationValidationOptions(
+ {
+ presentationVerifierOptions: new JwsVerificationOptions({ nonce }),
+ },
+ );
+
+ const resolver = new Resolver({
+ client: aliceClient,
+ });
+ // Resolve the presentation holder.
+ const presentationHolderDID: CoreDID = JwtPresentationValidator.extractHolder(presentationJwt);
+ const resolvedHolder = await resolver.resolve(
+ presentationHolderDID.toString(),
+ );
+
+ // Validate presentation. Note that this doesn't validate the included credentials.
+ let decodedPresentation = new JwtPresentationValidator(new PQJwsVerifier()).validate(
+ presentationJwt,
+ resolvedHolder,
+ jwtPresentationValidationOptions,
+ );
+
+ // Validate the credentials in the presentation.
+ let credentialValidator = new JwtCredentialValidator(new PQJwsVerifier());
+ let validationOptions = new JwtCredentialValidationOptions({
+ subjectHolderRelationship: [
+ presentationHolderDID.toString(),
+ SubjectHolderRelationship.AlwaysSubject,
+ ],
+ });
+
+ let jwtCredentials: Jwt[] = decodedPresentation
+ .presentation()
+ .verifiableCredential()
+ .map((credential) => {
+ const jwt = credential.tryIntoJwt();
+ if (!jwt) {
+ throw new Error("expected a JWT credential");
+ } else {
+ return jwt;
+ }
+ });
+
+ // Concurrently resolve the issuers' documents.
+ let issuers: string[] = [];
+ for (let jwtCredential of jwtCredentials) {
+ let issuer = JwtCredentialValidator.extractIssuerFromJwt(jwtCredential);
+ issuers.push(issuer.toString());
+ }
+ let resolvedIssuers = await resolver.resolveMultiple(issuers);
+
+ // Validate the credentials in the presentation.
+ for (let i = 0; i < jwtCredentials.length; i++) {
+ credentialValidator.validate(
+ jwtCredentials[i],
+ resolvedIssuers[i],
+ validationOptions,
+ FailFast.FirstError,
+ );
+ }
+
+ // Since no errors were thrown we know that the validation was successful.
+ console.log(`VP successfully validated`);
+}
diff --git a/bindings/wasm/identity_wasm/examples/src/main.ts b/bindings/wasm/identity_wasm/examples/src/main.ts
index 82ef083d9f..4f236d5eb6 100644
--- a/bindings/wasm/identity_wasm/examples/src/main.ts
+++ b/bindings/wasm/identity_wasm/examples/src/main.ts
@@ -1,4 +1,4 @@
-// Copyright 2020-2025 IOTA Stiftung
+// Copyright 2020-2025 IOTA Stiftung, Fondazione LINKS
// SPDX-License-Identifier: Apache-2.0
import { createIdentity } from "./0_basic/0_create_did";
@@ -13,12 +13,15 @@ import { didOwnsDid } from "./1_advanced/0_did_owns_did";
import { sdJwtVc } from "./1_advanced/10_sd_jwt_vc";
import { advancedTransaction } from "./1_advanced/11_advanced_transactions";
import { iotaKeytoolIntegration } from "./1_advanced/12_iota_keytool_integration";
+import { linkedVp } from "./1_advanced/13_linked_verifiable_presentation";
import { customResolution } from "./1_advanced/4_custom_resolution";
import { domainLinkage } from "./1_advanced/5_domain_linkage";
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 { hybrid } from "./1_advanced/hybrid";
+import { pq } from "./1_advanced/pq";
export async function main(example?: string) {
// Extract example name.
@@ -60,10 +63,16 @@ export async function main(example?: string) {
return await zkp_revocation();
case "10_sd_jwt_vc":
return await sdJwtVc();
+ case "pq":
+ return await pq();
+ case "hybrid":
+ return await hybrid();
case "11_advanced_transactions":
return await advancedTransaction();
case "12_iota_keytool_integration":
return await iotaKeytoolIntegration();
+ case "13_linked_verifiable_presentation":
+ return await linkedVp();
default:
throw "Unknown example name: '" + argument + "'";
}
diff --git a/bindings/wasm/identity_wasm/examples/src/util.ts b/bindings/wasm/identity_wasm/examples/src/util.ts
index 3cf9dd656a..5bc348cffd 100644
--- a/bindings/wasm/identity_wasm/examples/src/util.ts
+++ b/bindings/wasm/identity_wasm/examples/src/util.ts
@@ -12,13 +12,17 @@ import {
Storage,
StorageSigner,
Transaction,
+ TransactionSigner,
} from "@iota/identity-wasm/node";
import { CoreClientReadOnly } from "@iota/iota-interaction-ts/node/core_client";
import { IotaClient, TransactionEffects } from "@iota/iota-sdk/client";
import { getFaucetHost, requestIotaFromFaucetV0 } from "@iota/iota-sdk/faucet";
+import { IotaEvent } from "@iota/iota-sdk/src/client/types/generated";
import { Transaction as SdkTransaction } from "@iota/iota-sdk/transactions";
+import { NotarizationClient, NotarizationClientReadOnly } from "@iota/notarization/node";
export const IOTA_IDENTITY_PKG_ID = globalThis?.process?.env?.IOTA_IDENTITY_PKG_ID || "";
+export const IOTA_NOTARIZATION_PKG_ID = globalThis?.process?.env?.IOTA_NOTARIZATION_PKG_ID || "";
export const NETWORK_NAME_FAUCET = globalThis?.process?.env?.NETWORK_NAME_FAUCET || "localnet";
export const NETWORK_URL = globalThis?.process?.env?.NETWORK_URL || "http://127.0.0.1:9000";
@@ -90,6 +94,20 @@ export async function getFundedClient(storage: Storage): Promise
return identityClient;
}
+export async function getNotarizationClient(signer: TransactionSigner): Promise {
+ if (!IOTA_NOTARIZATION_PKG_ID) {
+ throw new Error(`IOTA_NOTARIZATION_PKG_ID env variable must be provided to run the notarization examples`);
+ }
+
+ const iotaClient = new IotaClient({ url: NETWORK_URL });
+ const notarizationClientReadOnly = await NotarizationClientReadOnly.createWithPkgId(
+ iotaClient,
+ IOTA_NOTARIZATION_PKG_ID,
+ );
+
+ return await NotarizationClient.create(notarizationClientReadOnly, signer);
+}
+
export class SendZeroCoinTx implements Transaction {
recipient: string;
@@ -97,7 +115,7 @@ export class SendZeroCoinTx implements Transaction {
this.recipient = recipient;
}
- async buildProgrammableTransaction(client: CoreClientReadOnly): Promise {
+ async buildProgrammableTransaction(_client: CoreClientReadOnly): Promise {
const ptb = new SdkTransaction();
const recipientAddress = ptb.pure.address(this.recipient);
@@ -110,7 +128,15 @@ export class SendZeroCoinTx implements Transaction {
return tx_bytes.slice(1);
}
- async apply(effects: TransactionEffects, client: CoreClientReadOnly): Promise {
+ async apply(effects: TransactionEffects, _client: CoreClientReadOnly): Promise {
return effects.created![0].reference.objectId;
}
+
+ async applyWithEvents(
+ effects: TransactionEffects,
+ _events: IotaEvent[],
+ client: CoreClientReadOnly,
+ ): Promise {
+ return await this.apply(effects, client);
+ }
}
diff --git a/bindings/wasm/identity_wasm/lib/index.ts b/bindings/wasm/identity_wasm/lib/index.ts
index b9d8f68cbd..b5b7e7b272 100644
--- a/bindings/wasm/identity_wasm/lib/index.ts
+++ b/bindings/wasm/identity_wasm/lib/index.ts
@@ -16,3 +16,6 @@ export * from "@iota/iota-interaction-ts/transaction_internal";
// keep this export last to override the original `Resolver` from `identity_wasm` in the exports
export { Resolver } from "./resolver";
+
+export * from "./jwk_storage_pq.js";
+export * from "./pq_verifier.js";
diff --git a/bindings/wasm/identity_wasm/lib/jose/composite_jwk.ts b/bindings/wasm/identity_wasm/lib/jose/composite_jwk.ts
new file mode 100644
index 0000000000..89619eb52f
--- /dev/null
+++ b/bindings/wasm/identity_wasm/lib/jose/composite_jwk.ts
@@ -0,0 +1,14 @@
+// Copyright 2024 Fondazione Links
+// SPDX-License-Identifier: Apache-2.0
+
+export const enum CompositeAlgId {
+ IdMldsa44Ed25519 = "id-MLDSA44-Ed25519",
+
+ IdMldsa65Ed25519 = "id-MLDSA65-Ed25519",
+}
+
+export const enum CompositeAlgIdDomain {
+ IdMldsa44Ed25519 = "060B6086480186FA6B5008013E",
+
+ IdMldsa65Ed25519 = "060B6086480186FA6B50080147",
+}
diff --git a/bindings/wasm/identity_wasm/lib/jose/index.ts b/bindings/wasm/identity_wasm/lib/jose/index.ts
index f033c4f110..65d827b07a 100644
--- a/bindings/wasm/identity_wasm/lib/jose/index.ts
+++ b/bindings/wasm/identity_wasm/lib/jose/index.ts
@@ -1,3 +1,4 @@
+export * from "./composite_jwk";
export * from "./ec_curve";
export * from "./ed_curve";
export * from "./jwk_operation";
diff --git a/bindings/wasm/identity_wasm/lib/jose/jwk_type.ts b/bindings/wasm/identity_wasm/lib/jose/jwk_type.ts
index 1707c3295f..820308bd3b 100644
--- a/bindings/wasm/identity_wasm/lib/jose/jwk_type.ts
+++ b/bindings/wasm/identity_wasm/lib/jose/jwk_type.ts
@@ -10,4 +10,6 @@ export const enum JwkType {
Oct = "oct",
/** Octet string key pairs. */
Okp = "OKP",
+ /** Algorithm key pair. */
+ Akp = "AKP",
}
diff --git a/bindings/wasm/identity_wasm/lib/jose/jws_algorithm.ts b/bindings/wasm/identity_wasm/lib/jose/jws_algorithm.ts
index 54cd306a27..d18b7f6d46 100644
--- a/bindings/wasm/identity_wasm/lib/jose/jws_algorithm.ts
+++ b/bindings/wasm/identity_wasm/lib/jose/jws_algorithm.ts
@@ -32,4 +32,14 @@ export const enum JwsAlgorithm {
NONE = "none",
/** EdDSA signature algorithms */
EdDSA = "EdDSA",
+ /** ML-DSA-44 */
+ MLDSA44 = "ML-DSA-44",
+ /** ML-DSA-65 */
+ MLDSA65 = "ML-DSA-65",
+ /** ML-DSA-87 */
+ MLDSA87 = "ML-DSA-87",
+ /** ML-DSA-44 in hybrid signature*/
+ IdMldsa44Ed25519 = "id-MLDSA44-Ed25519",
+ /** ML-DSA-65 in hybrid signature*/
+ IdMldsa65Ed25519 = "id-MLDSA65-Ed25519",
}
diff --git a/bindings/wasm/identity_wasm/lib/jwk_storage_pq.ts b/bindings/wasm/identity_wasm/lib/jwk_storage_pq.ts
new file mode 100644
index 0000000000..76bf46f797
--- /dev/null
+++ b/bindings/wasm/identity_wasm/lib/jwk_storage_pq.ts
@@ -0,0 +1,269 @@
+// Copyright 2024 Fondazione Links
+// SPDX-License-Identifier: Apache-2.0
+import * as ed from "@noble/ed25519";
+import { ml_dsa44, ml_dsa65, ml_dsa87 } from "@noble/post-quantum/ml-dsa";
+import { decodeB64, encodeB64, Jwk, JwkGenOutput, JwkStorage, JwkStoragePQ } from "~identity_wasm";
+import { EdCurve, JwkType, JwsAlgorithm } from "./jose";
+
+type Ed25519PrivateKey = Uint8Array;
+type Ed25519PublicKey = Uint8Array;
+
+// JkwStorage for PQ and PQ/T examples
+export class JwkPqMemStore implements JwkStorage, JwkStoragePQ {
+ /** The map from key identifiers to Jwks. */
+ private _keys: Map;
+
+ /** Creates a new, empty `MemStore` instance. */
+ constructor() {
+ this._keys = new Map();
+ }
+
+ public static mldsaKeyType(): string {
+ return "AKP";
+ }
+
+ public static ed25519KeyType(): string {
+ return "Ed25519";
+ }
+
+ private _get_key(keyId: string): Jwk | undefined {
+ return this._keys.get(keyId);
+ }
+
+ public async generate(keyType: string, algorithm: JwsAlgorithm): Promise {
+ if (keyType !== JwkPqMemStore.ed25519KeyType()) {
+ throw new Error(`unsupported key type ${keyType}`);
+ }
+
+ if (algorithm !== JwsAlgorithm.EdDSA) {
+ throw new Error(`unsupported algorithm`);
+ }
+
+ const keyId = randomKeyId();
+ const privKey: Ed25519PrivateKey = ed.utils.randomPrivateKey();
+
+ const publicKey: Ed25519PublicKey = await ed.getPublicKey(privKey);
+ const jwk = await encodeJwk(privKey, publicKey, algorithm);
+
+ this._keys.set(keyId, jwk);
+
+ const publicJWK = jwk?.toPublic();
+ if (!publicJWK) {
+ throw new Error(`JWK is not a public key`);
+ }
+
+ return new JwkGenOutput(keyId, publicJWK);
+ }
+
+ public async generatePQKey(keyType: String, algorithm: JwsAlgorithm): Promise {
+ if (keyType !== JwkPqMemStore.mldsaKeyType()) {
+ throw new Error(`unsupported key type ${keyType}`);
+ }
+
+ const seed = new TextEncoder().encode(randomKeyId());
+ let keys;
+ if (algorithm === JwsAlgorithm.MLDSA44) {
+ keys = ml_dsa44.keygen(seed);
+ } else if (algorithm === JwsAlgorithm.MLDSA65) {
+ keys = ml_dsa65.keygen(seed);
+ } else if (algorithm === JwsAlgorithm.MLDSA87) {
+ keys = ml_dsa87.keygen(seed);
+ } else {
+ throw new Error(`unsupported algorithm`);
+ }
+
+ const keyId = randomKeyId();
+
+ const jwk = await encodeJwk(keys.secretKey, keys.publicKey, algorithm);
+
+ if (jwk == undefined) {
+ throw new Error("Unexpected error: await encodeJwk(privKey, publicKey, algorithm)");
+ }
+
+ this._keys.set(keyId, jwk);
+
+ const publicJWK = jwk?.toPublic();
+ if (!publicJWK) {
+ throw new Error(`JWK is not a public key`);
+ }
+
+ return new JwkGenOutput(keyId, publicJWK);
+ }
+
+ public async sign(keyId: string, data: Uint8Array, publicKey: Jwk): Promise {
+ let alg = publicKey.alg();
+ let signature = null;
+
+ if (alg === undefined) {
+ throw new Error("expected a Jwk with an `alg` parameter");
+ }
+
+ if (alg !== JwsAlgorithm.EdDSA) {
+ throw new Error("unsupported JWS algorithm");
+ } else {
+ if (publicKey.paramsOkp()?.crv !== (EdCurve.Ed25519 as string)) {
+ throw new Error("unsupported Okp parameter");
+ }
+ }
+
+ const jwk = this._keys.get(keyId);
+
+ if (jwk) {
+ const [privateKey, _] = decodeJwk(jwk);
+ signature = await ed.sign(data, privateKey);
+ } else {
+ throw new Error(`key with id ${keyId} not found`);
+ }
+ return signature;
+ }
+
+ public async signPQ(
+ keyId: string,
+ data: Uint8Array,
+ publicKey: Jwk,
+ ctx: Uint8Array | undefined,
+ ): Promise {
+ let alg = publicKey.alg();
+ let signature = null;
+
+ if (alg === undefined) {
+ throw new Error("expected a Jwk with an `alg` parameter");
+ }
+
+ if (alg !== JwsAlgorithm.MLDSA44 && alg !== JwsAlgorithm.MLDSA65 && alg !== JwsAlgorithm.MLDSA87) {
+ throw new Error("unsupported JWS algorithm");
+ }
+
+ const jwk = this._keys.get(keyId);
+
+ if (jwk) {
+ const [privateKey, _] = decodeJwk(jwk);
+
+ if (alg == JwsAlgorithm.MLDSA44) {
+ signature = ml_dsa44.sign(privateKey, data, ctx);
+ } else if (alg == JwsAlgorithm.MLDSA65) {
+ signature = ml_dsa65.sign(privateKey, data, ctx);
+ } else if (alg == JwsAlgorithm.MLDSA87) {
+ signature = ml_dsa87.sign(privateKey, data, ctx);
+ } else {
+ throw new Error("unsupported algorithm");
+ }
+ } else {
+ throw new Error(`key with id ${keyId} not found`);
+ }
+ return signature;
+ }
+
+ public async insert(jwk: Jwk): Promise {
+ const keyId = randomKeyId();
+
+ if (!jwk.isPrivate) {
+ throw new Error("expected a JWK with all private key components set");
+ }
+
+ if (!jwk.alg()) {
+ throw new Error("expected a Jwk with an `alg` parameter");
+ }
+
+ this._keys.set(keyId, jwk);
+
+ return keyId;
+ }
+
+ public async delete(keyId: string): Promise {
+ this._keys.delete(keyId);
+ }
+
+ public async exists(keyId: string): Promise {
+ return this._keys.has(keyId);
+ }
+
+ public count(): number {
+ return this._keys.size;
+ }
+}
+
+// Encodes a Ed25519 keypair into a Jwk.
+async function encodeJwk(privateKey: Uint8Array, publicKey: Uint8Array, alg: JwsAlgorithm): Promise {
+ let pub = encodeB64(publicKey);
+ let priv = encodeB64(privateKey);
+
+ if (alg === JwsAlgorithm.EdDSA) {
+ return new Jwk({
+ "kty": JwkType.Okp,
+ crv: "Ed25519",
+ d: priv,
+ x: pub,
+ alg,
+ });
+ } else {
+ return new Jwk({
+ "kty": JwkType.Akp,
+ pub: pub,
+ priv: priv,
+ alg,
+ });
+ }
+}
+
+function decodeJwk(jwk: Jwk): [Uint8Array, Uint8Array] {
+ if (
+ jwk.alg()! !== JwsAlgorithm.MLDSA44
+ && jwk.alg()! !== JwsAlgorithm.MLDSA65
+ && jwk.alg()! !== JwsAlgorithm.MLDSA87
+ && jwk.alg()! !== JwsAlgorithm.EdDSA
+ ) {
+ throw new Error("unsupported `alg`");
+ }
+ if (jwk.alg()! === JwsAlgorithm.EdDSA) {
+ const paramsOkp = jwk.paramsOkp();
+ if (paramsOkp) {
+ const d = paramsOkp.d;
+
+ if (d) {
+ const textEncoder = new TextEncoder();
+ const privateKey = decodeB64(textEncoder.encode(d));
+ const publicKey = decodeB64(textEncoder.encode(paramsOkp.x));
+ return [privateKey, publicKey];
+ } else {
+ throw new Error("missing private key component");
+ }
+ } else {
+ throw new Error("expected Okp params");
+ }
+ } else {
+ const paramsPQ = jwk.paramsAkp();
+
+ if (paramsPQ) {
+ const priv = paramsPQ.priv;
+
+ if (priv) {
+ let textEncoder = new TextEncoder();
+ const privateKey = decodeB64(textEncoder.encode(priv));
+ const publicKey = decodeB64(textEncoder.encode(paramsPQ.pub));
+ return [privateKey, publicKey];
+ } else {
+ throw new Error("missing private key component");
+ }
+ } else {
+ throw new Error("expected Okp params");
+ }
+ }
+}
+
+// Returns a random number between `min` and `max` (inclusive).
+// SAFETY NOTE: This is not cryptographically secure randomness and thus not suitable for production use.
+// It suffices for our testing implementation however and avoids an external dependency.
+function getRandomNumber(min: number, max: number): number {
+ return Math.floor(Math.random() * (max - min + 1)) + min;
+}
+
+// Returns a random key id.
+function randomKeyId(): string {
+ const randomness = new Uint8Array(20);
+ for (let index = 0; index < randomness.length; index++) {
+ randomness[index] = getRandomNumber(0, 255);
+ }
+
+ return encodeB64(randomness);
+}
diff --git a/bindings/wasm/identity_wasm/lib/pq_verifier.ts b/bindings/wasm/identity_wasm/lib/pq_verifier.ts
new file mode 100644
index 0000000000..7df5012d48
--- /dev/null
+++ b/bindings/wasm/identity_wasm/lib/pq_verifier.ts
@@ -0,0 +1,61 @@
+// Copyright 2024 Fondazione Links
+// SPDX-License-Identifier: Apache-2.0
+
+import { ml_dsa44, ml_dsa65, ml_dsa87 } from "@noble/post-quantum/ml-dsa";
+import { decodeB64, IJwsVerifier, Jwk } from "~identity_wasm";
+import { CompositeAlgIdDomain, JwsAlgorithm } from "./jose";
+
+export class PQJwsVerifier implements IJwsVerifier {
+ public verify(alg: JwsAlgorithm, signingInput: Uint8Array, decodedSignature: Uint8Array, publicKey: Jwk): void {
+ let res = false;
+ let ctx = undefined;
+
+ if (
+ alg !== JwsAlgorithm.MLDSA44
+ && alg !== JwsAlgorithm.MLDSA65
+ && alg !== JwsAlgorithm.MLDSA87
+ && alg !== JwsAlgorithm.IdMldsa44Ed25519
+ && alg !== JwsAlgorithm.IdMldsa65Ed25519
+ ) {
+ throw new Error("unsupported JWS algorithm");
+ }
+
+ const pubKey = decodeJwk(publicKey);
+
+ // Domain separator for hybrid signatures
+ if (alg === JwsAlgorithm.IdMldsa44Ed25519) {
+ ctx = Uint8Array.from(Buffer.from(CompositeAlgIdDomain.IdMldsa44Ed25519, "hex"));
+ } else if (alg === JwsAlgorithm.IdMldsa65Ed25519) {
+ ctx = Uint8Array.from(Buffer.from(CompositeAlgIdDomain.IdMldsa65Ed25519, "hex"));
+ }
+
+ if (alg === JwsAlgorithm.MLDSA44 || alg === JwsAlgorithm.IdMldsa44Ed25519) {
+ res = ml_dsa44.verify(pubKey, signingInput, decodedSignature, ctx);
+ } else if (alg === JwsAlgorithm.MLDSA65 || alg === JwsAlgorithm.IdMldsa65Ed25519) {
+ res = ml_dsa65.verify(pubKey, signingInput, decodedSignature, ctx);
+ } else if (alg === JwsAlgorithm.MLDSA87) {
+ res = ml_dsa87.verify(pubKey, signingInput, decodedSignature);
+ }
+ if (!res) {
+ throw new Error("signature verification failed");
+ }
+ }
+}
+
+function decodeJwk(jwk: Jwk): Uint8Array {
+ if (
+ jwk.alg()! !== JwsAlgorithm.MLDSA44 && jwk.alg()! !== JwsAlgorithm.MLDSA65
+ && jwk.alg()! !== JwsAlgorithm.MLDSA87
+ ) {
+ throw new Error("unsupported `alg`");
+ }
+
+ const paramsPQ = jwk.paramsAkp();
+
+ if (paramsPQ) {
+ let textEncoder = new TextEncoder();
+ return decodeB64(textEncoder.encode(paramsPQ.pub));
+ } else {
+ throw new Error("expected Okp params");
+ }
+}
diff --git a/bindings/wasm/identity_wasm/package-lock.json b/bindings/wasm/identity_wasm/package-lock.json
index 559ccb0827..8c5ddb73d5 100644
--- a/bindings/wasm/identity_wasm/package-lock.json
+++ b/bindings/wasm/identity_wasm/package-lock.json
@@ -1,17 +1,19 @@
{
"name": "@iota/identity-wasm",
- "version": "1.6.0-beta.7",
+ "version": "1.6.0-beta.10",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@iota/identity-wasm",
- "version": "1.6.0-beta.7",
+ "version": "1.6.0-beta.10",
"license": "Apache-2.0",
"dependencies": {
"@iota/iota-interaction-ts": "^0.8.0",
+ "@iota/notarization": "^0.1.6",
"@noble/ed25519": "^1.7.3",
"@noble/hashes": "^1.4.0",
+ "@noble/post-quantum": "^0.2",
"@types/node-fetch": "^2.6.2",
"base64-arraybuffer": "^1.0.2",
"jose": "^5.9.6",
@@ -43,7 +45,7 @@
"node": ">=20"
},
"peerDependencies": {
- "@iota/iota-sdk": "^1.2.0"
+ "@iota/iota-sdk": "^1.6.1"
}
},
"node_modules/@0no-co/graphql.web": {
@@ -241,9 +243,9 @@
}
},
"node_modules/@iota/bcs": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/@iota/bcs/-/bcs-1.0.0.tgz",
- "integrity": "sha512-oxXgpzlL0d2jbzZH/9XmLcM+jmPbYT1hbbPfkvzMZvGChIDw1zLvp1EFsJtb2hAcLHORIAHIV0xEhzhBpIlSqA==",
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@iota/bcs/-/bcs-1.2.0.tgz",
+ "integrity": "sha512-QdRSR0KpJ87tdjVNmM/j0+0DvE0aTxHIa02337iluaOsMqtJ8OdgUCfSyLduC/3qS+8tJE+UB1KOw55tF+sN2w==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
@@ -263,20 +265,21 @@
}
},
"node_modules/@iota/iota-sdk": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@iota/iota-sdk/-/iota-sdk-1.2.0.tgz",
- "integrity": "sha512-SzC3NFYuq2P3dNh79X679p9Frb1XylU8O3nfeOQL4NcEnf/44k5HY0ABeNuWHFoYoeuI0Uk+qSIEcIVSaaMoag==",
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/@iota/iota-sdk/-/iota-sdk-1.6.1.tgz",
+ "integrity": "sha512-V7rx7m9erCn9lr4hNZVMtwmka2NsoTZ9EFSE4ZqEDO44cWdheM61+i/y5HJhvvmYAb/kkDfSmfdmzLaGTbVVYg==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@graphql-typed-document-node/core": "^3.2.0",
- "@iota/bcs": "1.0.0",
+ "@iota/bcs": "1.2.0",
"@noble/curves": "^1.4.2",
"@noble/hashes": "^1.4.0",
"@scure/bip32": "^1.4.0",
"@scure/bip39": "^1.3.0",
"@suchipi/femver": "^1.0.0",
"bech32": "^2.0.0",
+ "bignumber.js": "^9.1.1",
"gql.tada": "^1.8.2",
"graphql": "^16.9.0",
"tweetnacl": "^1.0.3",
@@ -293,6 +296,21 @@
"license": "Unlicense",
"peer": true
},
+ "node_modules/@iota/notarization": {
+ "version": "0.1.6",
+ "resolved": "https://registry.npmjs.org/@iota/notarization/-/notarization-0.1.6.tgz",
+ "integrity": "sha512-EoQIrACsOckXhUZ5y1AJdOO1mPv0lkoqeX5yROH3mjDbSpzqRF0kszAN/hBwOQcopSQPiEvrH6+5E+bD5WSntw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@iota/iota-interaction-ts": "^0.8.0"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "peerDependencies": {
+ "@iota/iota-sdk": "^1.6.1"
+ }
+ },
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"dev": true,
@@ -494,6 +512,30 @@
"url": "https://paulmillr.com/funding/"
}
},
+ "node_modules/@noble/post-quantum": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/@noble/post-quantum/-/post-quantum-0.2.1.tgz",
+ "integrity": "sha512-ImgfMp9notXSEocz464o1AefYfFWEkkszKMGO+ZiTn73yIBFeNyEHKQUMS+SheJwSNymldSts6YyVcQDjcnVVg==",
+ "license": "MIT",
+ "dependencies": {
+ "@noble/hashes": "1.6.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/@noble/post-quantum/node_modules/@noble/hashes": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.6.0.tgz",
+ "integrity": "sha512-YUULf0Uk4/mAA89w+k3+yUYh6NrEvxZa5T6SY3wlMvE2chHkxFUUIDI8/XW1QSC357iA5pSnqt7XEhvFOqmDyQ==",
+ "license": "MIT",
+ "engines": {
+ "node": "^14.21.3 || >=16"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"dev": true,
@@ -1315,6 +1357,16 @@
"node": "*"
}
},
+ "node_modules/bignumber.js": {
+ "version": "9.3.1",
+ "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
+ "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/binary-extensions": {
"version": "2.2.0",
"dev": true,
diff --git a/bindings/wasm/identity_wasm/package.json b/bindings/wasm/identity_wasm/package.json
index 60204c18ba..58a8251c38 100644
--- a/bindings/wasm/identity_wasm/package.json
+++ b/bindings/wasm/identity_wasm/package.json
@@ -1,6 +1,6 @@
{
"name": "@iota/identity-wasm",
- "version": "1.6.0-beta.7",
+ "version": "1.6.0-beta.10",
"author": "IOTA Foundation ",
"description": "WASM bindings for IOTA Identity - A Self Sovereign Identity Framework implementing the DID and VC standards from W3C. To be used in JavaScript/TypeScript",
"homepage": "https://www.iota.org",
@@ -75,15 +75,17 @@
},
"dependencies": {
"@iota/iota-interaction-ts": "^0.8.0",
+ "@iota/notarization": "^0.1.6",
"@noble/ed25519": "^1.7.3",
"@noble/hashes": "^1.4.0",
+ "@noble/post-quantum": "^0.2",
"@types/node-fetch": "^2.6.2",
"base64-arraybuffer": "^1.0.2",
"jose": "^5.9.6",
"node-fetch": "^2.6.7"
},
"peerDependencies": {
- "@iota/iota-sdk": "^1.2.0"
+ "@iota/iota-sdk": "^1.6.1"
},
"engines": {
"node": ">=20"
diff --git a/bindings/wasm/identity_wasm/src/credential/jwt_credential_validation/jwt_credential_validator_hybrid.rs b/bindings/wasm/identity_wasm/src/credential/jwt_credential_validation/jwt_credential_validator_hybrid.rs
new file mode 100644
index 0000000000..a6e073ee7d
--- /dev/null
+++ b/bindings/wasm/identity_wasm/src/credential/jwt_credential_validation/jwt_credential_validator_hybrid.rs
@@ -0,0 +1,217 @@
+// Copyright 2020-2025 IOTA Stiftung, Fondazione Links
+// SPDX-License-Identifier: Apache-2.0
+
+use identity_iota::core::Object;
+use identity_iota::core::Url;
+use identity_iota::credential::JwtCredentialValidatorHybrid;
+use identity_iota::credential::JwtCredentialValidatorUtils;
+use identity_iota::credential::StatusCheck;
+use identity_iota::did::CoreDID;
+
+use super::options::WasmJwtCredentialValidationOptions;
+use crate::common::ImportedDocumentLock;
+use crate::common::ImportedDocumentReadGuard;
+use crate::common::WasmTimestamp;
+use crate::credential::options::WasmStatusCheck;
+use crate::credential::revocation::status_list_2021::WasmStatusList2021Credential;
+use crate::credential::WasmCredential;
+use crate::credential::WasmDecodedJwtCredential;
+use crate::credential::WasmFailFast;
+use crate::credential::WasmJwt;
+use crate::credential::WasmSubjectHolderRelationship;
+use crate::did::ArrayIToCoreDocument;
+use crate::did::IToCoreDocument;
+use crate::did::WasmCoreDID;
+use crate::did::WasmJwsVerificationOptions;
+use crate::error::Result;
+use crate::error::WasmResult;
+use crate::verification::IJwsVerifier;
+use crate::verification::WasmJwsVerifier;
+
+use wasm_bindgen::prelude::*;
+
+/// A type for decoding and validating PQ/T {@link Credential}.
+#[wasm_bindgen(js_name = JwtCredentialValidatorHybrid)]
+pub struct WasmJwtCredentialValidatorHybrid(JwtCredentialValidatorHybrid);
+
+#[wasm_bindgen(js_class = JwtCredentialValidatorHybrid)]
+impl WasmJwtCredentialValidatorHybrid {
+ /// Creates a new {@link JwtCredentialValidatorHybrid}.
+ #[wasm_bindgen(constructor)]
+ #[allow(non_snake_case)]
+ pub fn new(
+ traditionalSignatureVerifier: Option,
+ pqSignatureVerifier: Option,
+ ) -> WasmJwtCredentialValidatorHybrid {
+ let traditional_signature_verifier = WasmJwsVerifier::new(traditionalSignatureVerifier);
+ let pq_signature_verifier = WasmJwsVerifier::new(pqSignatureVerifier);
+ WasmJwtCredentialValidatorHybrid(JwtCredentialValidatorHybrid::with_signature_verifiers(
+ traditional_signature_verifier,
+ pq_signature_verifier,
+ ))
+ }
+
+ /// Decodes and validates a {@link Credential} issued as a JWS. A {@link DecodedJwtCredential} is returned upon
+ /// success.
+ ///
+ /// The following properties are validated according to `options`:
+ /// - the issuer's signature on the JWS,
+ /// - the expiration date,
+ /// - the issuance date,
+ /// - the semantic structure.
+ ///
+ /// # Warning
+ /// The lack of an error returned from this method is in of itself not enough to conclude that the credential can be
+ /// trusted. This section contains more information on additional checks that should be carried out before and after
+ /// calling this method.
+ ///
+ /// ## The state of the issuer's DID Document
+ /// The caller must ensure that `issuer` represents an up-to-date DID Document.
+ ///
+ /// ## Properties that are not validated
+ /// There are many properties defined in [The Verifiable Credentials Data Model](https://www.w3.org/TR/vc-data-model/) that are **not** validated, such as:
+ /// `proof`, `credentialStatus`, `type`, `credentialSchema`, `refreshService` **and more**.
+ /// These should be manually checked after validation, according to your requirements.
+ ///
+ /// # Errors
+ /// An error is returned whenever a validated condition is not satisfied.
+ #[wasm_bindgen]
+ pub fn validate(
+ &self,
+ credential_jwt: &WasmJwt,
+ issuer: &IToCoreDocument,
+ options: &WasmJwtCredentialValidationOptions,
+ fail_fast: WasmFailFast,
+ ) -> Result {
+ let issuer_lock = ImportedDocumentLock::from(issuer);
+ let issuer_guard = issuer_lock.try_read()?;
+
+ self
+ .0
+ .validate(&credential_jwt.0, &issuer_guard, &options.0, fail_fast.into())
+ .wasm_result()
+ .map(WasmDecodedJwtCredential)
+ }
+
+ /// Decode and verify the JWS signature of a {@link Credential} issued as a JWT using the DID Document of a trusted
+ /// issuer.
+ ///
+ /// A {@link DecodedJwtCredential} is returned upon success.
+ ///
+ /// # Warning
+ /// The caller must ensure that the DID Documents of the trusted issuers are up-to-date.
+ ///
+ /// ## Proofs
+ /// Only the JWS signature is verified. If the {@link Credential} contains a `proof` property this will not be
+ /// verified by this method.
+ ///
+ /// # Errors
+ /// This method immediately returns an error if
+ /// the credential issuer' url cannot be parsed to a DID belonging to one of the trusted issuers. Otherwise an attempt
+ /// to verify the credential's signature will be made and an error is returned upon failure.
+ #[wasm_bindgen(js_name = verifySignature)]
+ #[allow(non_snake_case)]
+ pub fn verify_signature(
+ &self,
+ credential: &WasmJwt,
+ trustedIssuers: &ArrayIToCoreDocument,
+ options: &WasmJwsVerificationOptions,
+ ) -> Result {
+ let issuer_locks: Vec = trustedIssuers.into();
+ let trusted_issuers: Vec> = issuer_locks
+ .iter()
+ .map(ImportedDocumentLock::try_read)
+ .collect::>>>(
+ )?;
+
+ self
+ .0
+ .verify_signature(&credential.0, &trusted_issuers, &options.0)
+ .wasm_result()
+ .map(WasmDecodedJwtCredential)
+ }
+
+ /// Validate that the credential expires on or after the specified timestamp.
+ #[wasm_bindgen(js_name = checkExpiresOnOrAfter)]
+ pub fn check_expires_on_or_after(credential: &WasmCredential, timestamp: &WasmTimestamp) -> Result<()> {
+ JwtCredentialValidatorUtils::check_expires_on_or_after(&credential.0, timestamp.0).wasm_result()
+ }
+
+ /// Validate that the credential is issued on or before the specified timestamp.
+ #[wasm_bindgen(js_name = checkIssuedOnOrBefore)]
+ pub fn check_issued_on_or_before(credential: &WasmCredential, timestamp: &WasmTimestamp) -> Result<()> {
+ JwtCredentialValidatorUtils::check_issued_on_or_before(&credential.0, timestamp.0).wasm_result()
+ }
+
+ /// Validate that the relationship between the `holder` and the credential subjects is in accordance with
+ /// `relationship`. The `holder` parameter is expected to be the URL of the holder.
+ #[wasm_bindgen(js_name = checkSubjectHolderRelationship)]
+ pub fn check_subject_holder_relationship(
+ credential: &WasmCredential,
+ holder: &str,
+ relationship: WasmSubjectHolderRelationship,
+ ) -> Result<()> {
+ let holder: Url = Url::parse(holder).wasm_result()?;
+ JwtCredentialValidatorUtils::check_subject_holder_relationship(&credential.0, &holder, relationship.into())
+ .wasm_result()
+ }
+
+ /// Checks whether the credential status has been revoked.
+ ///
+ /// Only supports `RevocationBitmap2022`.
+ #[wasm_bindgen(js_name = checkStatus)]
+ #[allow(non_snake_case)]
+ pub fn check_status(
+ credential: &WasmCredential,
+ trustedIssuers: &ArrayIToCoreDocument,
+ statusCheck: WasmStatusCheck,
+ ) -> Result<()> {
+ let issuer_locks: Vec = trustedIssuers.into();
+ let trusted_issuers: Vec> = issuer_locks
+ .iter()
+ .map(ImportedDocumentLock::try_read)
+ .collect::>>>(
+ )?;
+ let status_check: StatusCheck = statusCheck.into();
+ JwtCredentialValidatorUtils::check_status(&credential.0, &trusted_issuers, status_check).wasm_result()
+ }
+
+ /// Checks wheter the credential status has been revoked using `StatusList2021`.
+ #[wasm_bindgen(js_name = checkStatusWithStatusList2021)]
+ pub fn check_status_with_status_list_2021(
+ credential: &WasmCredential,
+ status_list: &WasmStatusList2021Credential,
+ status_check: WasmStatusCheck,
+ ) -> Result<()> {
+ JwtCredentialValidatorUtils::check_status_with_status_list_2021(
+ &credential.0,
+ &status_list.inner,
+ status_check.into(),
+ )
+ .wasm_result()
+ }
+
+ /// Utility for extracting the issuer field of a {@link Credential} as a DID.
+ ///
+ /// ### Errors
+ ///
+ /// Fails if the issuer field is not a valid DID.
+ #[wasm_bindgen(js_name = extractIssuer)]
+ pub fn extract_issuer(credential: &WasmCredential) -> Result {
+ JwtCredentialValidatorUtils::extract_issuer::(&credential.0)
+ .map(WasmCoreDID::from)
+ .wasm_result()
+ }
+
+ /// Utility for extracting the issuer field of a credential in JWT representation as DID.
+ ///
+ /// # Errors
+ ///
+ /// If the JWT decoding fails or the issuer field is not a valid DID.
+ #[wasm_bindgen(js_name = extractIssuerFromJwt)]
+ pub fn extract_issuer_from_jwt(credential: &WasmJwt) -> Result {
+ JwtCredentialValidatorUtils::extract_issuer_from_jwt::(&credential.0)
+ .map(WasmCoreDID::from)
+ .wasm_result()
+ }
+}
diff --git a/bindings/wasm/identity_wasm/src/credential/jwt_credential_validation/mod.rs b/bindings/wasm/identity_wasm/src/credential/jwt_credential_validation/mod.rs
index 826d0388d9..959f3fe0de 100644
--- a/bindings/wasm/identity_wasm/src/credential/jwt_credential_validation/mod.rs
+++ b/bindings/wasm/identity_wasm/src/credential/jwt_credential_validation/mod.rs
@@ -1,8 +1,9 @@
-// Copyright 2020-2023 IOTA Stiftung
+// Copyright 2020-2025 IOTA Stiftung, Fondazione LINKS
// SPDX-License-Identifier: Apache-2.0
mod decoded_jwt_credential;
mod jwt_credential_validator;
+mod jwt_credential_validator_hybrid;
mod kb_validation_options;
mod options;
mod sd_jwt_validator;
@@ -10,6 +11,7 @@ mod unknown_credential;
pub use self::decoded_jwt_credential::*;
pub use self::jwt_credential_validator::*;
+pub use self::jwt_credential_validator_hybrid::*;
pub use self::kb_validation_options::*;
pub use self::options::*;
pub use self::sd_jwt_validator::*;
diff --git a/bindings/wasm/identity_wasm/src/credential/jwt_presentation_validation/jwt_presentation_validator_hybrid.rs b/bindings/wasm/identity_wasm/src/credential/jwt_presentation_validation/jwt_presentation_validator_hybrid.rs
new file mode 100644
index 0000000000..22e69092f9
--- /dev/null
+++ b/bindings/wasm/identity_wasm/src/credential/jwt_presentation_validation/jwt_presentation_validator_hybrid.rs
@@ -0,0 +1,101 @@
+// Copyright 2020-2025 IOTA Stiftung, Fondazione Links
+// SPDX-License-Identifier: Apache-2.0
+
+use super::decoded_jwt_presentation::WasmDecodedJwtPresentation;
+use super::options::WasmJwtPresentationValidationOptions;
+use crate::common::ImportedDocumentLock;
+use crate::credential::WasmJwt;
+use crate::credential::WasmPresentation;
+use crate::did::IToCoreDocument;
+use crate::did::WasmCoreDID;
+use crate::error::Result;
+use crate::error::WasmResult;
+use crate::verification::IJwsVerifier;
+use crate::verification::WasmJwsVerifier;
+use identity_iota::credential::JwtPresentationValidatorHybrid;
+use identity_iota::credential::JwtPresentationValidatorUtils;
+use identity_iota::did::CoreDID;
+use wasm_bindgen::prelude::*;
+
+#[wasm_bindgen(js_name = JwtPresentationValidatorHybrid, inspectable)]
+pub struct WasmJwtPresentationValidatorHybrid(JwtPresentationValidatorHybrid);
+
+#[wasm_bindgen(js_class = JwtPresentationValidatorHybrid)]
+impl WasmJwtPresentationValidatorHybrid {
+ /// Creates a new {@link JwtPresentationValidatorHybrid}. If a `signatureVerifier` is provided it will be used when
+ /// verifying decoded JWS signatures, otherwise a default verifier capable of handling the `EdDSA`, `ES256`, `ES256K`
+ /// algorithms will be used.
+ #[wasm_bindgen(constructor)]
+ #[allow(non_snake_case)]
+ pub fn new(
+ traditionalSignatureVerifier: Option,
+ pqSignatureVerifier: Option,
+ ) -> WasmJwtPresentationValidatorHybrid {
+ let traditional_signature_verifier = WasmJwsVerifier::new(traditionalSignatureVerifier);
+ let pq_signature_verifier = WasmJwsVerifier::new(pqSignatureVerifier);
+ WasmJwtPresentationValidatorHybrid(JwtPresentationValidatorHybrid::with_signature_verifiers(
+ traditional_signature_verifier,
+ pq_signature_verifier,
+ ))
+ }
+
+ /// Validates a {@link Presentation} encoded as a {@link Jwt}.
+ ///
+ /// The following properties are validated according to `options`:
+ /// - the JWT can be decoded into a semantically valid presentation.
+ /// - the expiration and issuance date contained in the JWT claims.
+ /// - the holder's signature.
+ ///
+ /// Validation is done with respect to the properties set in `options`.
+ ///
+ /// # Warning
+ ///
+ /// * This method does NOT validate the constituent credentials and therefore also not the relationship between the
+ /// credentials' subjects and the presentation holder. This can be done with {@link JwtCredentialValidationOptions}.
+ /// * The lack of an error returned from this method is in of itself not enough to conclude that the presentation can
+ /// be trusted. This section contains more information on additional checks that should be carried out before and
+ /// after calling this method.
+ ///
+ /// ## The state of the supplied DID Documents.
+ ///
+ /// The caller must ensure that the DID Documents in `holder` are up-to-date.
+ ///
+ /// # Errors
+ ///
+ /// An error is returned whenever a validated condition is not satisfied or when decoding fails.
+ #[wasm_bindgen]
+ #[allow(non_snake_case)]
+ pub fn validate(
+ &self,
+ presentationJwt: &WasmJwt,
+ holder: &IToCoreDocument,
+ validation_options: &WasmJwtPresentationValidationOptions,
+ ) -> Result {
+ let holder_lock = ImportedDocumentLock::from(holder);
+ let holder_guard = holder_lock.try_read()?;
+
+ self
+ .0
+ .validate(&presentationJwt.0, &holder_guard, &validation_options.0)
+ .map(WasmDecodedJwtPresentation::from)
+ .wasm_result()
+ }
+
+ /// Validates the semantic structure of the {@link Presentation}.
+ #[wasm_bindgen(js_name = checkStructure)]
+ pub fn check_structure(presentation: &WasmPresentation) -> Result<()> {
+ JwtPresentationValidatorUtils::check_structure(&presentation.0).wasm_result()?;
+ Ok(())
+ }
+
+ /// Attempt to extract the holder of the presentation.
+ ///
+ /// # Errors:
+ /// * If deserialization/decoding of the presentation fails.
+ /// * If the holder can't be parsed as DIDs.
+ #[wasm_bindgen(js_name = extractHolder)]
+ pub fn extract_holder(presentation: &WasmJwt) -> Result {
+ let holder = JwtPresentationValidatorUtils::extract_holder::(&presentation.0).wasm_result()?;
+ Ok(WasmCoreDID(holder))
+ }
+}
diff --git a/bindings/wasm/identity_wasm/src/credential/jwt_presentation_validation/mod.rs b/bindings/wasm/identity_wasm/src/credential/jwt_presentation_validation/mod.rs
index 12c556852e..be4689df0f 100644
--- a/bindings/wasm/identity_wasm/src/credential/jwt_presentation_validation/mod.rs
+++ b/bindings/wasm/identity_wasm/src/credential/jwt_presentation_validation/mod.rs
@@ -1,10 +1,12 @@
-// Copyright 2020-2023 IOTA Stiftung
+// Copyright 2020-2025 IOTA Stiftung, Fondazione LINKS
// SPDX-License-Identifier: Apache-2.0
mod decoded_jwt_presentation;
mod jwt_presentation_validator;
+mod jwt_presentation_validator_hybrid;
mod options;
pub use self::decoded_jwt_presentation::*;
pub use self::jwt_presentation_validator::*;
+pub use self::jwt_presentation_validator_hybrid::*;
pub use self::options::*;
diff --git a/bindings/wasm/identity_wasm/src/credential/linked_verifiable_presentation_service.rs b/bindings/wasm/identity_wasm/src/credential/linked_verifiable_presentation_service.rs
index ec7d8f3a3d..7849279b0d 100644
--- a/bindings/wasm/identity_wasm/src/credential/linked_verifiable_presentation_service.rs
+++ b/bindings/wasm/identity_wasm/src/credential/linked_verifiable_presentation_service.rs
@@ -34,7 +34,7 @@ impl WasmLinkedVerifiablePresentationService {
.into_serde::()
.wasm_result()?;
Ok(Self(
- LinkedVerifiablePresentationService::new(id, linked_vp, properties).wasm_result()?,
+ LinkedVerifiablePresentationService::new(id, linked_vp.into_vec(), properties).wasm_result()?,
))
}
@@ -92,7 +92,7 @@ struct ILinkedVerifiablePresentationServiceHelper {
#[typescript(optional = false, type = "DIDUrl")]
id: DIDUrl,
/// A unique URI that may be used to identify the {@link Credential}.
- #[typescript(optional = false, type = "string | string[]")]
+ #[typescript(name = "linkedVp", optional = false, type = "string | string[]")]
linked_vp: OneOrSet,
/// Miscellaneous properties.
#[serde(flatten)]
diff --git a/bindings/wasm/identity_wasm/src/did/did_compositejwk.rs b/bindings/wasm/identity_wasm/src/did/did_compositejwk.rs
new file mode 100644
index 0000000000..5db195a609
--- /dev/null
+++ b/bindings/wasm/identity_wasm/src/did/did_compositejwk.rs
@@ -0,0 +1,105 @@
+// Copyright 2020-2025 IOTA Stiftung, Fondazione Links
+// SPDX-License-Identifier: Apache-2.0
+
+use identity_iota::did::DIDCompositeJwk;
+use identity_iota::did::DID as _;
+use wasm_bindgen::prelude::*;
+
+use super::wasm_core_did::get_core_did_clone;
+use super::IToCoreDID;
+use super::WasmCoreDID;
+use crate::error::Result;
+use crate::error::WasmResult;
+use crate::jose::WasmCompositeJwk;
+
+/// `did:compositejwk` DID.
+#[wasm_bindgen(js_name = DIDCompositeJwk)]
+pub struct WasmDIDCompositeJwk(pub(crate) DIDCompositeJwk);
+
+#[wasm_bindgen(js_class = DIDCompositeJwk)]
+impl WasmDIDCompositeJwk {
+ #[wasm_bindgen(constructor)]
+ /// Creates a new {@link DIDCompositeJwk} from a {@link CoreDID}.
+ ///
+ /// ### Errors
+ /// Throws an error if the given did is not a valid `did:compositejwk` DID.
+ pub fn new(did: IToCoreDID) -> Result {
+ let did = get_core_did_clone(&did).0;
+ DIDCompositeJwk::try_from(did).wasm_result().map(Self)
+ }
+ /// Parses a {@link DIDCompositeJwk} from the given `input`.
+ ///
+ /// ### Errors
+ ///
+ /// Throws an error if the input is not a valid {@link DIDCompositeJwk}.
+ #[wasm_bindgen]
+ pub fn parse(input: &str) -> Result {
+ DIDCompositeJwk::parse(input).wasm_result().map(Self)
+ }
+
+ /// Returns the JSON WEB KEY (JWK) encoded inside this `did:jwk`.
+ #[wasm_bindgen(js_name = compositeJwk)]
+ pub fn composite_jwk(&self) -> WasmCompositeJwk {
+ self.0.composite_jwk().into()
+ }
+
+ // ===========================================================================
+ // DID trait
+ // ===========================================================================
+
+ /// Returns the {@link CoreDID} scheme.
+ ///
+ /// E.g.
+ /// - `"did:example:12345678" -> "did"`
+ /// - `"did:iota:smr:12345678" -> "did"`
+ #[wasm_bindgen]
+ pub fn scheme(&self) -> String {
+ self.0.scheme().to_owned()
+ }
+
+ /// Returns the {@link CoreDID} authority: the method name and method-id.
+ ///
+ /// E.g.
+ /// - `"did:example:12345678" -> "example:12345678"`
+ /// - `"did:iota:smr:12345678" -> "iota:smr:12345678"`
+ #[wasm_bindgen]
+ pub fn authority(&self) -> String {
+ self.0.authority().to_owned()
+ }
+
+ /// Returns the {@link CoreDID} method name.
+ ///
+ /// E.g.
+ /// - `"did:example:12345678" -> "example"`
+ /// - `"did:iota:smr:12345678" -> "iota"`
+ #[wasm_bindgen]
+ pub fn method(&self) -> String {
+ self.0.method().to_owned()
+ }
+
+ /// Returns the {@link CoreDID} method-specific ID.
+ ///
+ /// E.g.
+ /// - `"did:example:12345678" -> "12345678"`
+ /// - `"did:iota:smr:12345678" -> "smr:12345678"`
+ #[wasm_bindgen(js_name = methodId)]
+ pub fn method_id(&self) -> String {
+ self.0.method_id().to_owned()
+ }
+
+ /// Returns the {@link CoreDID} as a string.
+ #[allow(clippy::inherent_to_string)]
+ #[wasm_bindgen(js_name = toString)]
+ pub fn to_string(&self) -> String {
+ self.0.to_string()
+ }
+
+ // Only intended to be called internally.
+ #[wasm_bindgen(js_name = toCoreDid, skip_typescript)]
+ pub fn to_core_did(&self) -> WasmCoreDID {
+ WasmCoreDID(self.0.clone().into())
+ }
+}
+
+impl_wasm_json!(WasmDIDCompositeJwk, DIDCompositeJwk);
+impl_wasm_clone!(WasmDIDCompositeJwk, DIDCompositeJwk);
diff --git a/bindings/wasm/identity_wasm/src/did/did_jwk.rs b/bindings/wasm/identity_wasm/src/did/did_jwk.rs
index 15ce291eca..fcd7dd7707 100644
--- a/bindings/wasm/identity_wasm/src/did/did_jwk.rs
+++ b/bindings/wasm/identity_wasm/src/did/did_jwk.rs
@@ -27,6 +27,13 @@ impl WasmDIDJwk {
let did = get_core_did_clone(&did).0;
DIDJwk::try_from(did).wasm_result().map(Self)
}
+
+ /// Creates a new {@link DIDJwk} from the given {@link Jwk}.
+ #[wasm_bindgen(js_name = fromJwk)]
+ pub fn from_jwk(jwk: WasmJwk) -> Self {
+ Self(DIDJwk::new(jwk.0))
+ }
+
/// Parses a {@link DIDJwk} from the given `input`.
///
/// ### Errors
diff --git a/bindings/wasm/identity_wasm/src/did/mod.rs b/bindings/wasm/identity_wasm/src/did/mod.rs
index ae2e89bc0c..427b4be863 100644
--- a/bindings/wasm/identity_wasm/src/did/mod.rs
+++ b/bindings/wasm/identity_wasm/src/did/mod.rs
@@ -1,13 +1,16 @@
-// Copyright 2020-2023 IOTA Stiftung
+// Copyright 2020-2025 IOTA Stiftung, Fondazione LINKS
// SPDX-License-Identifier: Apache-2.0
+mod did_compositejwk;
mod did_jwk;
mod jws_verification_options;
mod service;
mod wasm_core_did;
mod wasm_core_document;
+mod wasm_did_jwk_document_ext;
mod wasm_did_url;
+pub use self::jws_verification_options::*;
pub use self::service::IService;
pub use self::service::UServiceEndpoint;
pub use self::service::WasmService;
@@ -20,6 +23,5 @@ pub use self::wasm_core_document::PromiseJws;
pub use self::wasm_core_document::PromiseJwt;
pub use self::wasm_core_document::WasmCoreDocument;
pub use self::wasm_did_url::WasmDIDUrl;
+pub use did_compositejwk::*;
pub use did_jwk::*;
-
-pub use self::jws_verification_options::*;
diff --git a/bindings/wasm/identity_wasm/src/did/wasm_core_document.rs b/bindings/wasm/identity_wasm/src/did/wasm_core_document.rs
index fd66c4e7ca..9ca82769b4 100644
--- a/bindings/wasm/identity_wasm/src/did/wasm_core_document.rs
+++ b/bindings/wasm/identity_wasm/src/did/wasm_core_document.rs
@@ -1,4 +1,4 @@
-// Copyright 2020-2023 IOTA Stiftung
+// Copyright 2020-2025 IOTA Stiftung, Fondazione LINKS
// SPDX-License-Identifier: Apache-2.0
use std::rc::Rc;
@@ -24,6 +24,7 @@ use crate::credential::WasmJwt;
use crate::credential::WasmPresentation;
use crate::did::service::WasmService;
use crate::did::wasm_did_url::WasmDIDUrl;
+use crate::did::WasmDIDCompositeJwk;
use crate::did::WasmDIDJwk;
use crate::error::Result;
use crate::error::WasmResult;
@@ -773,6 +774,14 @@ impl WasmCoreDocument {
pub fn expand_did_jwk(did: WasmDIDJwk) -> Result {
CoreDocument::expand_did_jwk(did.0).wasm_result().map(Self::from)
}
+
+ /// Creates a {@link CoreDocument} from the given {@link DIDCompositeJwk}.
+ #[wasm_bindgen(js_name = expandDIDCompositeJwk)]
+ pub fn expand_did_compositejwk(did: WasmDIDCompositeJwk) -> Result {
+ CoreDocument::expand_did_compositejwk(did.0)
+ .wasm_result()
+ .map(Self::from)
+ }
}
#[wasm_bindgen]
diff --git a/bindings/wasm/identity_wasm/src/did/wasm_did_jwk_document_ext.rs b/bindings/wasm/identity_wasm/src/did/wasm_did_jwk_document_ext.rs
new file mode 100644
index 0000000000..863ae7b440
--- /dev/null
+++ b/bindings/wasm/identity_wasm/src/did/wasm_did_jwk_document_ext.rs
@@ -0,0 +1,331 @@
+// Copyright 2020-2025 IOTA Stiftung, Fondazione Links
+// SPDX-License-Identifier: Apache-2.0
+
+use super::CoreDocumentLock;
+use super::WasmCoreDocument;
+use crate::common::RecordStringAny;
+use crate::credential::PromiseJpt;
+use crate::credential::UnknownCredential;
+use crate::credential::WasmCredential;
+use crate::credential::WasmJpt;
+use crate::credential::WasmJwpPresentationOptions;
+use crate::credential::WasmJws;
+use crate::credential::WasmJwt;
+use crate::credential::WasmPresentation;
+use crate::did::PromiseJws;
+use crate::did::PromiseJwt;
+use crate::error::Result;
+use crate::error::WasmResult;
+use crate::jose::WasmCompositeAlgId;
+use crate::jose::WasmJwsAlgorithm;
+use crate::jpt::WasmProofAlgorithm;
+use crate::jpt::WasmSelectiveDisclosurePresentation;
+use crate::storage::WasmJwsSignatureOptions;
+use crate::storage::WasmJwtPresentationOptions;
+use crate::storage::WasmStorage;
+use crate::storage::WasmStorageInner;
+use identity_iota::core::Object;
+use identity_iota::credential::Credential;
+use identity_iota::credential::JwtPresentationOptions;
+use identity_iota::credential::Presentation;
+use identity_iota::document::CoreDocument;
+use identity_iota::storage::key_storage::KeyType;
+use identity_iota::storage::storage::JwkDocumentExtHybrid;
+use identity_iota::storage::storage::JwsDocumentExtPQC;
+use identity_iota::storage::DidJwkDocumentExt;
+use identity_iota::storage::JwpDocumentExt;
+use identity_iota::storage::JwsSignatureOptions;
+use identity_iota::verification::jwk::CompositeAlgId;
+use identity_iota::verification::jws::JwsAlgorithm;
+use js_sys::Promise;
+use jsonprooftoken::jpa::algs::ProofAlgorithm;
+use std::rc::Rc;
+use wasm_bindgen::prelude::*;
+use wasm_bindgen_futures::future_to_promise;
+
+#[wasm_bindgen(js_class = CoreDocument)]
+impl WasmCoreDocument {
+ /// Creates a new DID Document with the given `key_type` and `alg` with the JWK did method.
+ #[wasm_bindgen(js_name = newDidJwk)]
+ pub async fn _new_did_jwk(
+ storage: &WasmStorage,
+ key_type: String,
+ alg: WasmJwsAlgorithm,
+ ) -> Result {
+ let storage_clone: Rc = storage.0.clone();
+ let alg: JwsAlgorithm = alg.into_serde().wasm_result()?;
+ CoreDocument::new_did_jwk(&storage_clone, KeyType::from(key_type), alg)
+ .await
+ .map(|doc| WasmCoreDocument(Rc::new(CoreDocumentLock::new(doc.0))))
+ .wasm_result()
+ }
+
+ /// Creates a new PQ DID Document with the given `key_type` and `alg` with the JWK did method.
+ #[wasm_bindgen(js_name = newDidJwkPq)]
+ pub async fn _new_did_jwk_pqc(
+ storage: &WasmStorage,
+ key_type: String,
+ alg: WasmJwsAlgorithm,
+ ) -> Result {
+ let storage_clone: Rc = storage.0.clone();
+ let alg: JwsAlgorithm = alg.into_serde().wasm_result()?;
+ CoreDocument::new_did_jwk_pqc(&storage_clone, KeyType::from(key_type), alg)
+ .await
+ .map(|doc| WasmCoreDocument(Rc::new(CoreDocumentLock::new(doc.0))))
+ .wasm_result()
+ }
+
+ /// Creates a new hybrid DID Document with the given `key_type` and `alg`with the compositeJWK did method.
+ #[wasm_bindgen(js_name = newDidCompositeJwk)]
+ pub async fn _new_did_compositejwk(storage: &WasmStorage, alg: WasmCompositeAlgId) -> Result {
+ let storage_clone: Rc = storage.0.clone();
+ let alg: CompositeAlgId = alg.into_serde().wasm_result()?;
+ CoreDocument::new_did_compositejwk(&storage_clone, alg)
+ .await
+ .map(|doc| WasmCoreDocument(Rc::new(CoreDocumentLock::new(doc.0))))
+ .wasm_result()
+ }
+
+ /// Creates a new zk DID Document with the given `key_type` and `alg` with the JWK did method.
+ #[wasm_bindgen(js_name = newDidJwkZk)]
+ pub async fn _new_did_jwk_zk(storage: &WasmStorage, alg: WasmProofAlgorithm) -> Result {
+ let storage_clone: Rc = storage.0.clone();
+ let alg: ProofAlgorithm = alg.into();
+ CoreDocument::new_did_jwk_zk(&storage_clone, KeyType::from_static_str("BLS12381"), alg)
+ .await
+ .map(|doc| WasmCoreDocument(Rc::new(CoreDocumentLock::new(doc.0))))
+ .wasm_result()
+ }
+
+ #[wasm_bindgen(js_name = fragmentJwk)]
+ pub fn _fragment(self) -> String {
+ "0".to_string()
+ }
+ /// Produces a PQ JWS, from a document with a PQ method, where the payload is produced from the given `fragment` and
+ /// `payload`.
+ #[wasm_bindgen(js_name = createPqJws)]
+ pub fn _create_pq_jws(
+ &self,
+ storage: &WasmStorage,
+ fragment: String,
+ payload: String,
+ options: &WasmJwsSignatureOptions,
+ ) -> Result {
+ let storage_clone: Rc = storage.0.clone();
+ let options_clone: JwsSignatureOptions = options.0.clone();
+ let document_lock_clone: Rc = self.0.clone();
+ let promise: Promise = future_to_promise(async move {
+ document_lock_clone
+ .read()
+ .await
+ .create_jws_pqc(&storage_clone, &fragment, payload.as_bytes(), &options_clone)
+ .await
+ .wasm_result()
+ .map(WasmJws::new)
+ .map(JsValue::from)
+ });
+ Ok(promise.unchecked_into())
+ }
+
+ /// Produces an hybrid JWS, from a document with an hybrid method, where the payload is produced from the given
+ /// `fragment` and `payload`.
+ #[wasm_bindgen(js_name = createHybridJws)]
+ pub fn create_hybrid_jws(
+ &self,
+ storage: &WasmStorage,
+ fragment: String,
+ payload: String,
+ options: &WasmJwsSignatureOptions,
+ ) -> Result {
+ let storage_clone: Rc = storage.0.clone();
+ let options_clone: JwsSignatureOptions = options.0.clone();
+ let document_lock_clone: Rc = self.0.clone();
+ let promise: Promise = future_to_promise(async move {
+ document_lock_clone
+ .read()
+ .await
+ .create_jws(&storage_clone, &fragment, payload.as_bytes(), &options_clone)
+ .await
+ .wasm_result()
+ .map(WasmJws::new)
+ .map(JsValue::from)
+ });
+ Ok(promise.unchecked_into())
+ }
+
+ /// Produces a PQ JWT, from a document with a PQ method, where the payload is produced from the given `credential`
+ /// in accordance with [VC Data Model v1.1](https://www.w3.org/TR/vc-data-model/#json-web-token).
+ ///
+ /// Unless the `kid` is explicitly set in the options, the `kid` in the protected header is the `id`
+ /// of the method identified by `fragment` and the JWS signature will be produced by the corresponding
+ /// private key backed by the `storage` in accordance with the passed `options`.
+ ///
+ /// The `custom_claims` can be used to set additional claims on the resulting JWT.
+ #[wasm_bindgen(js_name = createCredentialJwtPqc)]
+ pub fn _create_credential_jwt_pqc(
+ &self,
+ storage: &WasmStorage,
+ fragment: String,
+ credential: &WasmCredential,
+ options: &WasmJwsSignatureOptions,
+ custom_claims: Option,
+ ) -> Result {
+ let storage_clone: Rc = storage.0.clone();
+ let options_clone: JwsSignatureOptions = options.0.clone();
+ let document_lock_clone: Rc = self.0.clone();
+ let credential_clone: Credential = credential.0.clone();
+ let custom: Option