diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000000..115fe4a561
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,3 @@
+target/
+bindings/wasm/
+bindings/grpc/target/
diff --git a/.github/actions/iota-sandbox/setup/action.yml b/.github/actions/iota-sandbox/setup/action.yml
new file mode 100644
index 0000000000..8b32b8608d
--- /dev/null
+++ b/.github/actions/iota-sandbox/setup/action.yml
@@ -0,0 +1,36 @@
+name: 'iota-sandbox-setup'
+description: 'Setup IOTA Sandbox'
+runs:
+ using: "composite"
+ steps:
+ - name: Setup iota sandbox
+ shell: bash
+ run: |
+ # Use next lines for using the GitHub release
+ mkdir iota-sandbox
+ cd iota-sandbox
+ mkdir sandbox
+ cd sandbox
+ # Use the output of https://api.github.com/repos/iotaledger/iota-sandbox/releases/latest
+ DOWNLOAD_URL=$(curl "https://api.github.com/repos/iotaledger/iota-sandbox/releases" | jq -r '.[0].assets[] | select(.name | contains("iota_sandbox")) | .browser_download_url')
+ echo "Downloading sandbox from $DOWNLOAD_URL"
+ curl -L -o iota_sandbox.tar.gz $DOWNLOAD_URL
+ tar -xf iota_sandbox.tar.gz
+
+ # Use the next lines to use the main branch
+ # git clone https://github.com/iotaledger/iota-sandbox
+ # cd iota-sandbox/sandbox
+
+ # Start Tangle
+ sudo ./bootstrap.sh
+ docker compose --profile inx-faucet up -d
+ - name: Wait for tangle to start
+ shell: bash
+ run: wget -qO- https://raw.githubusercontent.com/eficode/wait-for/$WAIT_FOR_VERSION/wait-for | sh -s -- -t 60 http://localhost/health -- echo "Tangle is up"
+ env:
+ WAIT_FOR_VERSION: 4df3f9262d84cab0039c07bf861045fbb3c20ab7 # v2.2.3
+ - name: Wait for faucet to start
+ shell: bash
+ run: wget -qO- https://raw.githubusercontent.com/eficode/wait-for/$WAIT_FOR_VERSION/wait-for | sh -s -- -t 60 http://localhost/faucet/api/info -- echo "Faucet is up"
+ env:
+ WAIT_FOR_VERSION: 4df3f9262d84cab0039c07bf861045fbb3c20ab7 # v2.2.3
diff --git a/.github/actions/iota-sandbox/tear-down/action.yml b/.github/actions/iota-sandbox/tear-down/action.yml
new file mode 100644
index 0000000000..8a0da1906e
--- /dev/null
+++ b/.github/actions/iota-sandbox/tear-down/action.yml
@@ -0,0 +1,12 @@
+name: 'iota-sandbox-tear-down'
+description: 'tear-down a iota sandbox'
+runs:
+ using: "composite"
+ steps:
+ - name: Tear down iota sandbox
+ shell: bash
+ run: |
+ cd iota-sandbox/sandbox
+ docker-compose down
+ cd ../..
+ sudo rm -rf iota-sandbox
diff --git a/.github/actions/private-tangle/setup/action.yml b/.github/actions/private-tangle/setup/action.yml
deleted file mode 100644
index 3d6feab379..0000000000
--- a/.github/actions/private-tangle/setup/action.yml
+++ /dev/null
@@ -1,44 +0,0 @@
-name: 'private-tangle-setup'
-description: 'Setup a private tangle'
-runs:
- using: "composite"
- steps:
- - name: Setup private tangle
- shell: bash
- run: |
- # TODO: use next lines when a working hornet release is published
- # # Download the private_tangle setup from the hornet repo.
- # mkdir private_tangle
- # cd private_tangle
- # # Use the output of https://api.github.com/repos/iotaledger/hornet/releases/latest once there's a 2.0 Hornet release.
- # DOWNLOAD_URL=$(curl "https://api.github.com/repos/iotaledger/hornet/releases" | jq -r '.[0].assets[] | select(.name | contains("private_tangle")) | .browser_download_url')
- # echo "Downloading private tangle from $DOWNLOAD_URL"
- # curl -L -o private_tangle.tar.gz $DOWNLOAD_URL
- # tar -xf private_tangle.tar.gz
-
- # TODO: remove next lines when a working hornet release is published
- git clone https://github.com/iotaledger/hornet.git
- cd hornet/private_tangle
-
- # Set minPoWScore = 1 since the default (0) doesn't work with wasm_miner.rs in iota.rs currently.
- jq '.minPoWScore = $val' --argjson val 1 protocol_parameters.json > tmp.json && mv tmp.json protocol_parameters.json
- jq --color-output . protocol_parameters.json
-
- # Manipulate and print config
- jq '.restAPI.pow.enabled = $newVal' --argjson newVal true config_private_tangle.json > tmp.$$.json && mv tmp.$$.json config_private_tangle.json
- jq --color-output . config_private_tangle.json
-
- # Start Tangle
- sudo ./cleanup.sh
- sudo ./bootstrap.sh
- sudo ./run.sh -d
- - name: Wait for tangle to start
- shell: bash
- run: wget -qO- https://raw.githubusercontent.com/eficode/wait-for/$WAIT_FOR_VERSION/wait-for | sh -s -- -t 60 http://localhost:14265/health -- echo "Tangle is up"
- env:
- WAIT_FOR_VERSION: 4df3f9262d84cab0039c07bf861045fbb3c20ab7 # v2.2.3
- - name: Wait for faucet to start
- shell: bash
- run: wget -qO- https://raw.githubusercontent.com/eficode/wait-for/$WAIT_FOR_VERSION/wait-for | sh -s -- -t 60 http://localhost:8091/api/info -- echo "Faucet is up"
- env:
- WAIT_FOR_VERSION: 4df3f9262d84cab0039c07bf861045fbb3c20ab7 # v2.2.3
diff --git a/.github/actions/private-tangle/tear-down/action.yml b/.github/actions/private-tangle/tear-down/action.yml
deleted file mode 100644
index bfd73f46ff..0000000000
--- a/.github/actions/private-tangle/tear-down/action.yml
+++ /dev/null
@@ -1,16 +0,0 @@
-name: 'private-tangle-tear-down'
-description: 'tear-down a private tangle'
-runs:
- using: "composite"
- steps:
- - name: Tear down private tangle
- shell: bash
- run: |
- # TODO: use next line when a working hornet release is published
- #cd private_tangle
-
- # TODO: remove next line when a working hornet release is published
- cd hornet/private_tangle
- docker-compose down
- cd ..
- sudo rm -rf private_tangle
diff --git a/.github/actions/rust/rust-setup/action.yml b/.github/actions/rust/rust-setup/action.yml
index b7b16a352a..5f783a98cc 100644
--- a/.github/actions/rust/rust-setup/action.yml
+++ b/.github/actions/rust/rust-setup/action.yml
@@ -48,7 +48,16 @@ runs:
shell: bash
run: |
- if ! rustup self update; then
+ # self update is currently broken on Windows runners:
+ # https://github.com/rust-lang/rustup/issues/3709
+ # so we'll skip self update for windows
+ OS=${{ inputs.os }}
+ IS_WINDOWS=false; [[ $OS =~ ^[wW]indows ]] && IS_WINDOWS=true
+
+ if [[ $IS_WINDOWS = true ]] ;
+ then
+ echo "skipping self update on windows runner due to https://github.com/rust-lang/rustup/issues/3709"
+ elif ! rustup self update; then
echo "rustup self update failed"
fi
@@ -57,7 +66,13 @@ runs:
rustup target add $TARGET
fi
- rustup update
+ if [[ $IS_WINDOWS = true ]] ;
+ then
+ echo "skipping self update on windows runner due to https://github.com/rust-lang/rustup/issues/3709"
+ rustup update --no-self-update
+ else
+ rustup update
+ fi
TOOLCHAIN=${{ inputs.toolchain }}
if [[ $TOOLCHAIN != 'stable' ]]; then
diff --git a/.github/workflows/build-and-test-grpc.yml b/.github/workflows/build-and-test-grpc.yml
new file mode 100644
index 0000000000..80311728c8
--- /dev/null
+++ b/.github/workflows/build-and-test-grpc.yml
@@ -0,0 +1,41 @@
+name: Build and run grpc tests
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ types: [ opened, synchronize, reopened, ready_for_review ]
+ branches:
+ - main
+ - 'epic/**'
+ - 'support/**'
+ paths:
+ - '.github/workflows/build-and-test.yml'
+ - '.github/actions/**'
+ - '**.rs'
+ - '**.toml'
+ - 'bindings/grpc/**'
+
+jobs:
+ check-for-run-condition:
+ runs-on: ubuntu-latest
+ outputs:
+ should-run: ${{ !github.event.pull_request || github.event.pull_request.draft == false }}
+ steps:
+ - run: |
+ # this run step does nothing, but is needed to get the job output
+
+ build-and-test:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out the repo
+ uses: actions/checkout@v4
+
+ - name: Build Docker image
+ uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671
+ with:
+ context: .
+ file: bindings/grpc/Dockerfile
+ push: false
+ labels: iotaledger/identity-grpc:latest
\ No newline at end of file
diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml
index 47e0e622d0..206c534962 100644
--- a/.github/workflows/build-and-test.yml
+++ b/.github/workflows/build-and-test.yml
@@ -82,6 +82,12 @@ jobs:
steps:
- uses: actions/checkout@v3
+ - name: Ensure, OpenSSL is available in Windows
+ if: matrix.os == 'windows-latest'
+ run: |
+ echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append
+ vcpkg install openssl:x64-windows-static-md
+
- name: Setup Rust and cache
uses: './.github/actions/rust/rust-setup'
with:
@@ -101,7 +107,7 @@ jobs:
if: matrix.os == 'ubuntu-latest'
run: |
cargo metadata --format-version 1 | \
- jq -r '.workspace_members[] | select(contains("examples") | not)' | \
+ jq -r '.workspace_members[]' | \
awk '{print $1}' | \
xargs -I {} cargo check -p {} --no-default-features
@@ -109,7 +115,7 @@ jobs:
if: matrix.os == 'ubuntu-latest'
run: |
cargo metadata --format-version 1 | \
- jq -r '.workspace_members[] | select(contains("examples") | not)' | \
+ jq -r '.workspace_members[]' | \
awk '{print $1}' | \
xargs -I {} cargo check -p {}
@@ -123,9 +129,9 @@ jobs:
- name: Build with all features
run: cargo build --workspace --tests --examples --all-features --release
- - name: Start private tangle
+ - name: Start iota sandbox
if: matrix.os == 'ubuntu-latest'
- uses: './.github/actions/private-tangle/setup'
+ uses: './.github/actions/iota-sandbox/setup'
- name: Run tests
run: cargo test --workspace --all-features --release
@@ -140,9 +146,17 @@ jobs:
parallel -k -j 4 --retries 3 --joblog report.log ./target/release/examples/{}
cat report.log
- - name: Tear down private tangle
+ - name: Run Rust Readme examples
+ # run examples only on ubuntu for now
+ if: matrix.os == 'ubuntu-latest'
+ run: |
+ cd bindings/wasm
+ npm ci
+ npm run test:readme:rust
+
+ - name: Tear down iota sandbox
if: matrix.os == 'ubuntu-latest' && always()
- uses: './.github/actions/private-tangle/tear-down'
+ uses: './.github/actions/iota-sandbox/tear-down'
- name: Stop sccache
uses: './.github/actions/rust/sccache/stop-sccache'
@@ -186,13 +200,13 @@ jobs:
name: identity-wasm-bindings-build
path: bindings/wasm
- - name: Start private tangle
- uses: './.github/actions/private-tangle/setup'
+ - name: Start iota sandbox
+ uses: './.github/actions/iota-sandbox/setup'
- name: Run Wasm examples
run: npm run test:examples
working-directory: bindings/wasm
- - name: Tear down private tangle
+ - name: Tear down iota sandbox
if: always()
- uses: './.github/actions/private-tangle/tear-down'
+ uses: './.github/actions/iota-sandbox/tear-down'
diff --git a/.github/workflows/grpc-publish-to-dockerhub.yml b/.github/workflows/grpc-publish-to-dockerhub.yml
new file mode 100644
index 0000000000..348bf8c564
--- /dev/null
+++ b/.github/workflows/grpc-publish-to-dockerhub.yml
@@ -0,0 +1,53 @@
+name: gRPC publish to dockerhub
+
+on:
+ workflow_dispatch:
+ inputs:
+ tag:
+ description: 'Tag to publish under, defaults to latest'
+ required: false
+ default: latest
+ branch:
+ description: 'Branch to run publish from'
+ required: true
+ dry-run:
+ description: 'Run in dry-run mode'
+ type: boolean
+ required: false
+ default: true
+
+jobs:
+ push_to_registry:
+ environment: release
+ name: Push Docker image to Docker Hub
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out the repo
+ uses: actions/checkout@v4
+ with:
+ ref: ${{ github.event.inputs.branch }}
+
+ - name: Log in to Docker Hub
+ uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
+ with:
+ username: ${{ secrets.IOTALEDGER_DOCKER_USERNAME }}
+ password: ${{ secrets.IOTALEDGER_DOCKER_PASSWORD }}
+
+ - name: Build and push Docker image
+ uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671
+ with:
+ context: .
+ file: bindings/grpc/Dockerfile
+ push: ${{ !inputs.dry-run }}
+ tags: iotaledger/identity-grpc:${{ inputs.tag }}
+
+ - name: Docker Hub Description
+ uses: peter-evans/dockerhub-description@e98e4d1628a5f3be2be7c231e50981aee98723ae
+ if: ${{ !inputs.dry-run }}
+ with:
+ username: ${{ secrets.IOTALEDGER_DOCKER_USERNAME }}
+ password: ${{ secrets.IOTALEDGER_DOCKER_PASSWORD }}
+ repository: iotaledger/identity-grpc
+ readme-filepath: ./bindings/grpc/README.md
+ short-description: ${{ github.event.repository.description }}
+
diff --git a/.license_template b/.license_template
index 30334ddc0c..a437281e00 100644
--- a/.license_template
+++ b/.license_template
@@ -1,2 +1,2 @@
-// Copyright {20\d{2}(-20\d{2})?} IOTA Stiftung
+// Copyright {20\d{2}(-20\d{2})?} IOTA Stiftung{(?:, .+)?}
// SPDX-License-Identifier: Apache-2.0
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 90b76a914f..2f42479f29 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,372 +1,101 @@
# Changelog
-## [1.0.0](https://github.com/iotaledger/identity.rs/tree/v1.0.0) (2023-11-02)
-
-[Full Changelog](https://github.com/iotaledger/identity.rs/compare/v0.6.3...v1.0.0)
-
-This version introduces a new DID method targeting the IOTA UTXO ledger. This method works fundamentally differently from the previous method and introduces new capabilities to interact with Layer 1 assets like Native Tokens, NFTs and various Output types.
-
-This version changes the credential and presentation format to JWT, as specified by the [VC Data Model v1.1](https://www.w3.org/TR/vc-data-model/#json-web-token).
-
-Note: Identities and credentials created with the earlier versions cannot be resolved with this version of the library.
-
-### Changed
-
-- Add dedicated stronghold crate [#1243](https://github.com/iotaledger/identity.rs/pull/1243)
-- Add dedicated EdDSA verifier crate [#1238](https://github.com/iotaledger/identity.rs/pull/1238)
-- Use `VC Data Model v1.1` JWT encoding instead of `VC-JWT` [#1234](https://github.com/iotaledger/identity.rs/pull/1234)
-- Remove `vp` and `vc` from JWT claims in JOSE [#1233](https://github.com/iotaledger/identity.rs/pull/1233)
-- Mark error enums as non-exhaustive [#1227](https://github.com/iotaledger/identity.rs/pull/1227)
-- Change `verifiable_credential` to type `Vec` in `Presentation` [#1231](https://github.com/iotaledger/identity.rs/pull/1231)
-- Bring JwkDocumentExt names in line with Wasm [#1233](https://github.com/iotaledger/identity.rs/pull/1223)
-- Add lints for all crates [#1222](https://github.com/iotaledger/identity.rs/pull/1222)
-- Bump `iota-sdk` and other dependencies [#1208](https://github.com/iotaledger/identity.rs/pull/1208)
-- Polish `identity_credential` [#1205](https://github.com/iotaledger/identity.rs/pull/1205)
-- Polish `identity_resolver` and`identity_storage` [#1204](https://github.com/iotaledger/identity.rs/pull/1204)
-- Polish `identity_iota_core` [#1203](https://github.com/iotaledger/identity.rs/pull/1203)
-- Rename `JwtPresentation` to `Presentation` [#1200](https://github.com/iotaledger/identity.rs/pull/1200)
-- Polish `identity_document` [#1198](https://github.com/iotaledger/identity.rs/pull/1198)
-- Polish `identity_did` & `identity_verification` [#1197](https://github.com/iotaledger/identity.rs/pull/1197)
-- Polish `identity_core` [#1196](https://github.com/iotaledger/identity.rs/pull/1196)
-- Remove identity-diff remains [#1195](https://github.com/iotaledger/identity.rs/pull/1195)
-- Remove legacy signing and verification APIs [#1194](https://github.com/iotaledger/identity.rs/pull/1194)
-- Remove old `Presentation` type [#1190](https://github.com/iotaledger/identity.rs/pull/1190)
-- Remove reexported `Resolver` validation APIs [#1183](https://github.com/iotaledger/identity.rs/pull/1183)
-- Use JWT credentials for Domain Linkage [#1180](https://github.com/iotaledger/identity.rs/pull/1180)
-- Remove `identity_agent` & `identity_comm` [#1168](https://github.com/iotaledger/identity.rs/pull/1168)
-- Remove `identity-diff` crate [#1167](https://github.com/iotaledger/identity.rs/pull/1167)
-- JwkStorageDocument & JwtCredential validation [#1152](https://github.com/iotaledger/identity.rs/pull/1152)
-- Adapt StorageError to be more generic [#1144](https://github.com/iotaledger/identity.rs/pull/1144)
-- Add initial PublicKeyJwk support [#1143](https://github.com/iotaledger/identity.rs/pull/1143)
-- Split JWS `Decoder` functionality [#1133](https://github.com/iotaledger/identity.rs/pull/1133)
-- `CoreDocument` & `Service` and `VerificationMethod` are now in the `document` and `verification` modules respectively [#1104](https://github.com/iotaledger/identity.rs/pull/1104)
-- Remove generics in CoreDocument, VerificationMethod, Service, DIDUrl and LinkedDomainService [#1110](https://github.com/iotaledger/identity.rs/pull/1110)
-- Updated `iota-types` dependency to `1.0.0-rc.6` [#1121](https://github.com/iotaledger/identity.rs/pull/1121)
-- Refactor `MethodType` to make it extensible [#1112](https://github.com/iotaledger/identity.rs/pull/1112)
-- More identifier checks in `CoreDocument` [#1067](https://github.com/iotaledger/identity.rs/pull/1067)
-- Update to `iota-client` 2.0.1-rc.4 and `iota-client-wasm` 0.5.0-alpha.6 [#1088](https://github.com/iotaledger/identity.rs/pull/1088)
-- Update iota client 2.0.1 rc.3 [\#1062](https://github.com/iotaledger/identity.rs/pull/1062)
-- Use Bech32-encoded state controller and governor addresses [\#1044](https://github.com/iotaledger/identity.rs/pull/1044)
-- Expose iteration over verification relationship fields [\#1024](https://github.com/iotaledger/identity.rs/pull/1024)
-- Remove `identity_agent` reexport [\#1031](https://github.com/iotaledger/identity.rs/pull/1031)
-- Rename `MixedResolver` to `Resolver` in Wasm [\#1026](https://github.com/iotaledger/identity.rs/pull/1026)
-- Add length prefix to DID Document payloads [\#1010](https://github.com/iotaledger/identity.rs/pull/1010)
-- Feature-gate `Resolver` [\#1007](https://github.com/iotaledger/identity.rs/pull/1007)
-- Rename `Stardust` types to `Iota` [\#1000](https://github.com/iotaledger/identity.rs/pull/1000)
-- Change Stardust DID method to IOTA [\#982](https://github.com/iotaledger/identity.rs/pull/982)
-- Add Wasm Stardust Client [\#975](https://github.com/iotaledger/identity.rs/pull/975)
-- Generalized Resolver [\#970](https://github.com/iotaledger/identity.rs/pull/970)
-- Change `Storage` to handle `CoreDID` [\#968](https://github.com/iotaledger/identity.rs/pull/968)
-- Feature-gate `iota-client` dependency, integrate `StardustDID` [\#958](https://github.com/iotaledger/identity.rs/pull/958)
-- Change `Storage` to store arbitrary blobs [\#953](https://github.com/iotaledger/identity.rs/pull/953)
-- Add `StardustDocumentMetadata`, implement `StardustDocument` methods [\#951](https://github.com/iotaledger/identity.rs/pull/951)
-- Fix stack overflow in `CoreDID` `PartialEq` impl [\#946](https://github.com/iotaledger/identity.rs/pull/946)
-- Change `Service` `type` field to allow sets [\#944](https://github.com/iotaledger/identity.rs/pull/944)
-- Generalise `CredentialValidator`, `PresentationValidator` to support arbitrary DID Documents [\#935](https://github.com/iotaledger/identity.rs/pull/935)
+## [v1.3.0](https://github.com/iotaledger/identity.rs/tree/v1.3.0) (2024-05-28)
+[Full Changelog](https://github.com/iotaledger/identity.rs/compare/v1.2.0...v1.3.0)
### Added
-
-- Allow arbitrary JWS header parameters [#1245](https://github.com/iotaledger/identity.rs/pull/1245)
-- Allow custom JWT claims for presentations [#1244](https://github.com/iotaledger/identity.rs/pull/1244)
-- Allow custom `kid` to be set in JWS [#1239](https://github.com/iotaledger/identity.rs/pull/1239)
-- Allow custom JWT claims for credentials [#1237](https://github.com/iotaledger/identity.rs/pull/1237)
-- Improve `Proof` [#1209](https://github.com/iotaledger/identity.rs/pull/1209)
-- Polish `identity_jose` [#1201](https://github.com/iotaledger/identity.rs/pull/1201)
-- Add `resolve_multiple` to Resolver [#1189](https://github.com/iotaledger/identity.rs/pull/1189)
-- Make JWT presentations generic [#1186](https://github.com/iotaledger/identity.rs/pull/1186)
-- Support JWT presentations [#1175](https://github.com/iotaledger/identity.rs/pull/1175)
-- Polish JWK thumbprint and document extension API [#1173](https://github.com/iotaledger/identity.rs/pull/1173)
-- Stronghold Storage Implementation [#1157](https://github.com/iotaledger/identity.rs/pull/1157)
-- Implement `KeyIdStorage` in Rust [#1134](https://github.com/iotaledger/identity.rs/pull/1134)
-- Implement `JwkStorage` [#1116](https://github.com/iotaledger/identity.rs/pull/1133)
-- Add Wasm Bindings for Domain Linkage [#1115](https://github.com/iotaledger/identity.rs/pull/1115)
-- Introduce `IToCoreDocument` and document locks in the bindings [#1120](https://github.com/iotaledger/identity.rs/pull/1120)
-- Add Support for Domain Linkage in Rust [#1094](https://github.com/iotaledger/identity.rs/pull/1094)
-- Add JSON Object Signing capabilities [#1105](https://github.com/iotaledger/identity.rs/pull/1105)
-- Make `StateMetadataDocument` public [#1085](https://github.com/iotaledger/identity.rs/pull/1085)
-- Add v. credentials and presentations examples [#1070](https://github.com/iotaledger/identity.rs/pull/1070)
-- Add revocation examples [#1076](https://github.com/iotaledger/identity.rs/pull/1076)
-- Expose Controller and Governor Addresses in metadata [\#1023](https://github.com/iotaledger/identity.rs/pull/1023)
-- Add Stardust Client Extension Trait [\#963](https://github.com/iotaledger/identity.rs/pull/963)
-- Add StardustDID [\#949](https://github.com/iotaledger/identity.rs/pull/949)
-- State metadata serialization for the stardust DID method [\#947](https://github.com/iotaledger/identity.rs/pull/947)
-- Stardust DID Method Proof-of-Concept [\#940](https://github.com/iotaledger/identity.rs/pull/940)
-- Implement the Identity Agent [\#322](https://github.com/iotaledger/identity.rs/pull/322)
-
+- Add ZK BBS+-based selectively disclosable credentials (JPT) [\#1355](https://github.com/iotaledger/identity.rs/pull/1355)
+- Add EcDSA verifier [\#1353](https://github.com/iotaledger/identity.rs/pull/1353)
### Patch
+- Support for specification-compliant verification method type `JsonWebKey2020` [\#1367](https://github.com/iotaledger/identity.rs/pull/1367)
-- Fix holder claim check in VP [#1236](https://github.com/iotaledger/identity.rs/pull/1236)
-- Fix issuer claim check in VC [#1235](https://github.com/iotaledger/identity.rs/pull/1235)
-- Feature-gate Domain Linkage [#1184](https://github.com/iotaledger/identity.rs/pull/1184)
-- Update method spec and JWK method type [#1176](https://github.com/iotaledger/identity.rs/pull/1176)
-- Replace `iota-client` with `iota-sdk` [#1161](https://github.com/iotaledger/identity.rs/pull/1161)
-- Pin `form_urlencoded` to `1.1.0` [#1136](https://github.com/iotaledger/identity.rs/pull/1136)
-- Remove legacy crates [#1080](https://github.com/iotaledger/identity.rs/pull/1080)
-- Recommend unique `credentialStatus.id` in `RevocationBitmap2022` [\#1039](https://github.com/iotaledger/identity.rs/pull/1039)
-- Pin agent dev-dependencies to crates versions [\#1029](https://github.com/iotaledger/identity.rs/pull/1029)
-- Support case insensitive serialization of `RentStructure` [\#1012](https://github.com/iotaledger/identity.rs/pull/1012)
-- Update stronghold to 0.6.4 [\#928](https://github.com/iotaledger/identity.rs/pull/928)
-
-## [1.0.0-rc.1](https://github.com/iotaledger/identity.rs/tree/v1.0.0-rc.1) (2023-09-29)
-
-[Full Changelog](https://github.com/iotaledger/identity.rs/compare/v0.6.3...v1.0.0-rc.1)
-This version introduces a new DID method targeting the IOTA UTXO ledger. This method works fundamentally differently from the previous method and introduces new capabilities to interact with Layer 1 assets like Native Tokens, NFTs and various Output types.
+## [v1.2.0](https://github.com/iotaledger/identity.rs/tree/v1.2.0) (2024-03-27)
-This version changes the credential and presentation format to JWT, as specified by the [VC Data Model v1.1](https://www.w3.org/TR/vc-data-model/#json-web-token).
-
-Note: Identities and credentials created with the earlier versions cannot be resolved with this version of the library.
-
-### Changed
-
-- Add dedicated stronghold crate [#1243](https://github.com/iotaledger/identity.rs/pull/1243)
-- Add dedicated EdDSA verifier crate [#1238](https://github.com/iotaledger/identity.rs/pull/1238)
-- Use `VC Data Model v1.1` JWT encoding instead of `VC-JWT` [#1234](https://github.com/iotaledger/identity.rs/pull/1234)
-- Remove `vp` and `vc` from JWT claims in JOSE [#1233](https://github.com/iotaledger/identity.rs/pull/1233)
-- Mark error enums as non-exhaustive [#1227](https://github.com/iotaledger/identity.rs/pull/1227)
-- Change `verifiable_credential` to type `Vec` in `Presentation` [#1231](https://github.com/iotaledger/identity.rs/pull/1231)
-- Bring JwkDocumentExt names in line with Wasm [#1233](https://github.com/iotaledger/identity.rs/pull/1223)
-- Add lints for all crates [#1222](https://github.com/iotaledger/identity.rs/pull/1222)
-- Bump `iota-sdk` and other dependencies [#1208](https://github.com/iotaledger/identity.rs/pull/1208)
-- Polish `identity_credential` [#1205](https://github.com/iotaledger/identity.rs/pull/1205)
-- Polish `identity_resolver` and`identity_storage` [#1204](https://github.com/iotaledger/identity.rs/pull/1204)
-- Polish `identity_iota_core` [#1203](https://github.com/iotaledger/identity.rs/pull/1203)
-- Rename `JwtPresentation` to `Presentation` [#1200](https://github.com/iotaledger/identity.rs/pull/1200)
-- Polish `identity_document` [#1198](https://github.com/iotaledger/identity.rs/pull/1198)
-- Polish `identity_did` & `identity_verification` [#1197](https://github.com/iotaledger/identity.rs/pull/1197)
-- Polish `identity_core` [#1196](https://github.com/iotaledger/identity.rs/pull/1196)
-- Remove identity-diff remains [#1195](https://github.com/iotaledger/identity.rs/pull/1195)
-- Remove legacy signing and verification APIs [#1194](https://github.com/iotaledger/identity.rs/pull/1194)
-- Remove old `Presentation` type [#1190](https://github.com/iotaledger/identity.rs/pull/1190)
-- Remove reexported `Resolver` validation APIs [#1183](https://github.com/iotaledger/identity.rs/pull/1183)
-- Use JWT credentials for Domain Linkage [#1180](https://github.com/iotaledger/identity.rs/pull/1180)
-- Remove `identity_agent` & `identity_comm` [#1168](https://github.com/iotaledger/identity.rs/pull/1168)
-- Remove `identity-diff` crate [#1167](https://github.com/iotaledger/identity.rs/pull/1167)
-- JwkStorageDocument & JwtCredential validation [#1152](https://github.com/iotaledger/identity.rs/pull/1152)
-- Adapt StorageError to be more generic [#1144](https://github.com/iotaledger/identity.rs/pull/1144)
-- Add initial PublicKeyJwk support [#1143](https://github.com/iotaledger/identity.rs/pull/1143)
-- Split JWS `Decoder` functionality [#1133](https://github.com/iotaledger/identity.rs/pull/1133)
-- `CoreDocument` & `Service` and `VerificationMethod` are now in the `document` and `verification` modules respectively [#1104](https://github.com/iotaledger/identity.rs/pull/1104)
-- Remove generics in CoreDocument, VerificationMethod, Service, DIDUrl and LinkedDomainService [#1110](https://github.com/iotaledger/identity.rs/pull/1110)
-- Updated `iota-types` dependency to `1.0.0-rc.6` [#1121](https://github.com/iotaledger/identity.rs/pull/1121)
-- Refactor `MethodType` to make it extensible [#1112](https://github.com/iotaledger/identity.rs/pull/1112)
-- More identifier checks in `CoreDocument` [#1067](https://github.com/iotaledger/identity.rs/pull/1067)
-- Update to `iota-client` 2.0.1-rc.4 and `iota-client-wasm` 0.5.0-alpha.6 [#1088](https://github.com/iotaledger/identity.rs/pull/1088)
-- Update iota client 2.0.1 rc.3 [\#1062](https://github.com/iotaledger/identity.rs/pull/1062)
-- Use Bech32-encoded state controller and governor addresses [\#1044](https://github.com/iotaledger/identity.rs/pull/1044)
-- Expose iteration over verification relationship fields [\#1024](https://github.com/iotaledger/identity.rs/pull/1024)
-- Remove `identity_agent` reexport [\#1031](https://github.com/iotaledger/identity.rs/pull/1031)
-- Rename `MixedResolver` to `Resolver` in Wasm [\#1026](https://github.com/iotaledger/identity.rs/pull/1026)
-- Add length prefix to DID Document payloads [\#1010](https://github.com/iotaledger/identity.rs/pull/1010)
-- Feature-gate `Resolver` [\#1007](https://github.com/iotaledger/identity.rs/pull/1007)
-- Rename `Stardust` types to `Iota` [\#1000](https://github.com/iotaledger/identity.rs/pull/1000)
-- Change Stardust DID method to IOTA [\#982](https://github.com/iotaledger/identity.rs/pull/982)
-- Add Wasm Stardust Client [\#975](https://github.com/iotaledger/identity.rs/pull/975)
-- Generalized Resolver [\#970](https://github.com/iotaledger/identity.rs/pull/970)
-- Change `Storage` to handle `CoreDID` [\#968](https://github.com/iotaledger/identity.rs/pull/968)
-- Feature-gate `iota-client` dependency, integrate `StardustDID` [\#958](https://github.com/iotaledger/identity.rs/pull/958)
-- Change `Storage` to store arbitrary blobs [\#953](https://github.com/iotaledger/identity.rs/pull/953)
-- Add `StardustDocumentMetadata`, implement `StardustDocument` methods [\#951](https://github.com/iotaledger/identity.rs/pull/951)
-- Fix stack overflow in `CoreDID` `PartialEq` impl [\#946](https://github.com/iotaledger/identity.rs/pull/946)
-- Change `Service` `type` field to allow sets [\#944](https://github.com/iotaledger/identity.rs/pull/944)
-- Generalise `CredentialValidator`, `PresentationValidator` to support arbitrary DID Documents [\#935](https://github.com/iotaledger/identity.rs/pull/935)
+[Full Changelog](https://github.com/iotaledger/identity.rs/compare/v1.1.1...v1.2.0)
### Added
-- Allow arbitrary JWS header parameters [#1245](https://github.com/iotaledger/identity.rs/pull/1245)
-- Allow custom JWT claims for presentations [#1244](https://github.com/iotaledger/identity.rs/pull/1244)
-- Allow custom `kid` to be set in JWS [#1239](https://github.com/iotaledger/identity.rs/pull/1239)
-- Allow custom JWT claims for credentials [#1237](https://github.com/iotaledger/identity.rs/pull/1237)
-- Improve `Proof` [#1209](https://github.com/iotaledger/identity.rs/pull/1209)
-- Polish `identity_jose` [#1201](https://github.com/iotaledger/identity.rs/pull/1201)
-- Add `resolve_multiple` to Resolver [#1189](https://github.com/iotaledger/identity.rs/pull/1189)
-- Make JWT presentations generic [#1186](https://github.com/iotaledger/identity.rs/pull/1186)
-- Support JWT presentations [#1175](https://github.com/iotaledger/identity.rs/pull/1175)
-- Polish JWK thumbprint and document extension API [#1173](https://github.com/iotaledger/identity.rs/pull/1173)
-- Stronghold Storage Implementation [#1157](https://github.com/iotaledger/identity.rs/pull/1157)
-- Implement `KeyIdStorage` in Rust [#1134](https://github.com/iotaledger/identity.rs/pull/1134)
-- Implement `JwkStorage` [#1116](https://github.com/iotaledger/identity.rs/pull/1133)
-- Add Wasm Bindings for Domain Linkage [#1115](https://github.com/iotaledger/identity.rs/pull/1115)
-- Introduce `IToCoreDocument` and document locks in the bindings [#1120](https://github.com/iotaledger/identity.rs/pull/1120)
-- Add Support for Domain Linkage in Rust [#1094](https://github.com/iotaledger/identity.rs/pull/1094)
-- Add JSON Object Signing capabilities [#1105](https://github.com/iotaledger/identity.rs/pull/1105)
-- Make `StateMetadataDocument` public [#1085](https://github.com/iotaledger/identity.rs/pull/1085)
-- Add v. credentials and presentations examples [#1070](https://github.com/iotaledger/identity.rs/pull/1070)
-- Add revocation examples [#1076](https://github.com/iotaledger/identity.rs/pull/1076)
-- Expose Controller and Governor Addresses in metadata [\#1023](https://github.com/iotaledger/identity.rs/pull/1023)
-- Add Stardust Client Extension Trait [\#963](https://github.com/iotaledger/identity.rs/pull/963)
-- Add StardustDID [\#949](https://github.com/iotaledger/identity.rs/pull/949)
-- State metadata serialization for the stardust DID method [\#947](https://github.com/iotaledger/identity.rs/pull/947)
-- Stardust DID Method Proof-of-Concept [\#940](https://github.com/iotaledger/identity.rs/pull/940)
-- Implement the Identity Agent [\#322](https://github.com/iotaledger/identity.rs/pull/322)
+- Allow arbitrary verification methods [\#1334](https://github.com/iotaledger/identity.rs/pull/1334)
+- use latest release of sd-jwt-payload [\#1333](https://github.com/iotaledger/identity.rs/pull/1333)
+- Allow setting additional controllers for `IotaDocument` [\#1314](https://github.com/iotaledger/identity.rs/pull/1314)
+- Add `get_public_key` for `StrongholdStorage` [\#1311](https://github.com/iotaledger/identity.rs/pull/1311)
+- Support multiple IOTA networks in the `Resolver` [\#1304](https://github.com/iotaledger/identity.rs/pull/1304)
### Patch
-- Fix holder claim check in VP [#1236](https://github.com/iotaledger/identity.rs/pull/1236)
-- Fix issuer claim check in VC [#1235](https://github.com/iotaledger/identity.rs/pull/1235)
-- Feature-gate Domain Linkage [#1184](https://github.com/iotaledger/identity.rs/pull/1184)
-- Update method spec and JWK method type [#1176](https://github.com/iotaledger/identity.rs/pull/1176)
-- Replace `iota-client` with `iota-sdk` [#1161](https://github.com/iotaledger/identity.rs/pull/1161)
-- Pin `form_urlencoded` to `1.1.0` [#1136](https://github.com/iotaledger/identity.rs/pull/1136)
-- Remove legacy crates [#1080](https://github.com/iotaledger/identity.rs/pull/1080)
-- Recommend unique `credentialStatus.id` in `RevocationBitmap2022` [\#1039](https://github.com/iotaledger/identity.rs/pull/1039)
-- Pin agent dev-dependencies to crates versions [\#1029](https://github.com/iotaledger/identity.rs/pull/1029)
-- Support case insensitive serialization of `RentStructure` [\#1012](https://github.com/iotaledger/identity.rs/pull/1012)
-- Update stronghold to 0.6.4 [\#928](https://github.com/iotaledger/identity.rs/pull/928)
-
-## [0.7.0-alpha.8](https://github.com/iotaledger/identity.rs/tree/v0.7.0-alpha.8) (2023-09-28)
-
-[Full Changelog](https://github.com/iotaledger/identity.rs/compare/v0.7.0-alpha.7...v0.7.0-alpha.8)
-
+- Support %-encoded characters in DID method id [\#1303](https://github.com/iotaledger/identity.rs/pull/1303)
-### Changed
-- Add dedicated stronghold crate [#1243](https://github.com/iotaledger/identity.rs/pull/1243)
-- Add dedicated EdDSA verifier crate [#1238](https://github.com/iotaledger/identity.rs/pull/1238)
-- Use `VC Data Model v1.1` JWT encoding instead of `VC-JWT` [#1234](https://github.com/iotaledger/identity.rs/pull/1234)
-- Remove `vp` and `vc` from JWT claims in JOSE [#1233](https://github.com/iotaledger/identity.rs/pull/1233)
-- Mark error enums as non-exhaustive [#1227](https://github.com/iotaledger/identity.rs/pull/1227)
-- Change `verifiable_credential` to type `Vec` in `Presentation` [#1231](https://github.com/iotaledger/identity.rs/pull/1231)
-- Bring JwkDocumentExt names in line with Wasm [#1233](https://github.com/iotaledger/identity.rs/pull/1223)
-- Add lints for all crates [#1222](https://github.com/iotaledger/identity.rs/pull/1222)
+## [v1.1.1](https://github.com/iotaledger/identity.rs/tree/v1.1.1) (2024-02-19)
-### Added
-- Allow arbitrary JWS header parameters [#1245](https://github.com/iotaledger/identity.rs/pull/1245)
-- Allow custom JWT claims for presentations [#1244](https://github.com/iotaledger/identity.rs/pull/1244)
-- Allow custom `kid` to be set in JWS [#1239](https://github.com/iotaledger/identity.rs/pull/1239)
-- Allow custom JWT claims for credentials [#1237](https://github.com/iotaledger/identity.rs/pull/1237)
+[Full Changelog](https://github.com/iotaledger/identity.rs/compare/v1.1.0...v1.1.1)
### Patch
-- Fix holder claim check in VP [#1236](https://github.com/iotaledger/identity.rs/pull/1236)
-- Fix issuer claim check in VC [#1235](https://github.com/iotaledger/identity.rs/pull/1235)
-## [v0.7.0-alpha.7](https://github.com/iotaledger/identity.rs/tree/v0.7.0-alpha.7) (2023-08-15)
+- Fix compilation error caused by the `roaring` crate [\#1306](https://github.com/iotaledger/identity.rs/pull/1306)
-[Full Changelog](https://github.com/iotaledger/identity.rs/compare/v0.7.0-alpha.6...v0.7.0-alpha.7)
-
-### Added
+## [v1.1.0](https://github.com/iotaledger/identity.rs/tree/v1.1.0) (2024-02-07)
-- Improve `Proof` [#1209](https://github.com/iotaledger/identity.rs/pull/1209)
-- Polish `identity_jose` [#1201](https://github.com/iotaledger/identity.rs/pull/1201)
-- Add `resolve_multiple` to Resolver [#1189](https://github.com/iotaledger/identity.rs/pull/1189)
-- Make JWT presentations generic [#1186](https://github.com/iotaledger/identity.rs/pull/1186)
-- Support JWT presentations [#1175](https://github.com/iotaledger/identity.rs/pull/1175)
-- Polish JWK thumbprint and document extension API [#1173](https://github.com/iotaledger/identity.rs/pull/1173)
-- Stronghold Storage Implementation [#1157](https://github.com/iotaledger/identity.rs/pull/1157)
-- Implement `KeyIdStorage` in Rust [#1134](https://github.com/iotaledger/identity.rs/pull/1134)
-
-### Changed
-
-- Bump `iota-sdk` and other dependencies [#1208](https://github.com/iotaledger/identity.rs/pull/1208)
-- Polish `identity_credential` [#1205](https://github.com/iotaledger/identity.rs/pull/1205)
-- Polish `identity_resolver` and`identity_storage` [#1204](https://github.com/iotaledger/identity.rs/pull/1204)
-- Polish `identity_iota_core` [#1203](https://github.com/iotaledger/identity.rs/pull/1203)
-- Rename `JwtPresentation` to `Presentation` [#1200](https://github.com/iotaledger/identity.rs/pull/1200)
-- Polish `identity_document` [#1198](https://github.com/iotaledger/identity.rs/pull/1198)
-- Polish `identity_did` & `identity_verification` [#1197](https://github.com/iotaledger/identity.rs/pull/1197)
-- Polish `identity_core` [#1196](https://github.com/iotaledger/identity.rs/pull/1196)
-- Remove identity-diff remains [#1195](https://github.com/iotaledger/identity.rs/pull/1195)
-- Remove legacy signing and verification APIs [#1194](https://github.com/iotaledger/identity.rs/pull/1194)
-- Remove old `Presentation` type [#1190](https://github.com/iotaledger/identity.rs/pull/1190)
-- Remove reexported `Resolver` validation APIs [#1183](https://github.com/iotaledger/identity.rs/pull/1183)
-- Use JWT credentials for Domain Linkage [#1180](https://github.com/iotaledger/identity.rs/pull/1180)
-- Remove `identity_agent` & `identity_comm` [#1168](https://github.com/iotaledger/identity.rs/pull/1168)
-- Remove `identity-diff` crate [#1167](https://github.com/iotaledger/identity.rs/pull/1167)
-- JwkStorageDocument & JwtCredential validation [#1152](https://github.com/iotaledger/identity.rs/pull/1152)
-- Adapt StorageError to be more generic [#1144](https://github.com/iotaledger/identity.rs/pull/1144)
-- Add initial PublicKeyJwk support [#1143](https://github.com/iotaledger/identity.rs/pull/1143)
-- Split JWS `Decoder` functionality [#1133](https://github.com/iotaledger/identity.rs/pull/1133)
-
-### Patch
-
-- Feature-gate Domain Linkage [#1184](https://github.com/iotaledger/identity.rs/pull/1184)
-- Update method spec and JWK method type [#1176](https://github.com/iotaledger/identity.rs/pull/1176)
-- Replace `iota-client` with `iota-sdk` [#1161](https://github.com/iotaledger/identity.rs/pull/1161)
-- Pin `form_urlencoded` to `1.1.0` [#1136](https://github.com/iotaledger/identity.rs/pull/1136)
-
-## [v0.7.0-alpha.6](https://github.com/iotaledger/identity.rs/tree/v0.7.0-alpha.6) (2023-03-03)
-
-[Full Changelog](https://github.com/iotaledger/identity.rs/compare/v0.7.0-alpha.5...v0.7.0-alpha.6)
-
-### Added
-- Implement `JwkStorage` [#1116](https://github.com/iotaledger/identity.rs/pull/1133)
-- Add Wasm Bindings for Domain Linkage [#1115](https://github.com/iotaledger/identity.rs/pull/1115)
-- Introduce `IToCoreDocument` and document locks in the bindings [#1120](https://github.com/iotaledger/identity.rs/pull/1120)
-
-### Patch
-
-- Pin `form_urlencoded` to `1.1.0` [#1136](https://github.com/iotaledger/identity.rs/pull/1136)
-
-## [v0.7.0-alpha.5](https://github.com/iotaledger/identity.rs/tree/v0.7.0-alpha.5) (2023-02-15)
-
-[Full Changelog](https://github.com/iotaledger/identity.rs/compare/v0.7.0-alpha.4...v0.7.0-alpha.5)
+[Full Changelog](https://github.com/iotaledger/identity.rs/compare/v1.0.0...v1.1.0)
### Added
-- Add Support for Domain Linkage in Rust [#1094](https://github.com/iotaledger/identity.rs/pull/1094)
-- Add JSON Object Signing capabilities [#1105](https://github.com/iotaledger/identity.rs/pull/1105)
-
-### Changed
-- `CoreDocument` & `Service` and `VerificationMethod` are now in the `document` and `verification` modules respectively [#1104](https://github.com/iotaledger/identity.rs/pull/1104)
-- Remove generics in CoreDocument, VerificationMethod, Service, DIDUrl and LinkedDomainService [#1110](https://github.com/iotaledger/identity.rs/pull/1110)
-- Updated `iota-types` dependency to `1.0.0-rc.6` [#1121](https://github.com/iotaledger/identity.rs/pull/1121)
-- Refactor `MethodType` to make it extensible [#1112](https://github.com/iotaledger/identity.rs/pull/1112)
-## [v0.7.0-alpha.4](https://github.com/iotaledger/identity.rs/tree/v0.7.0-alpha.4) (2022-11-24)
+- Update `sd-jwt-payload` dependency [\#1296](https://github.com/iotaledger/identity.rs/pull/1296)
+- Add support for StatusList2021 [\#1273](https://github.com/iotaledger/identity.rs/pull/1273)
+- Support Selective Disclosure SD-JWT [\#1268](https://github.com/iotaledger/identity.rs/pull/1268)
-[Full Changelog](https://github.com/iotaledger/identity.rs/compare/v0.7.0-alpha.3...v0.7.0-alpha.4)
-
-### Added
-
-- Make `StateMetadataDocument` public [#1085](https://github.com/iotaledger/identity.rs/pull/1085)
-- Add v. credentials and presentations examples [#1070](https://github.com/iotaledger/identity.rs/pull/1070)
-- Add revocation examples [#1076](https://github.com/iotaledger/identity.rs/pull/1076)
-
-### Changed
-- More identifier checks in `CoreDocument` [#1067](https://github.com/iotaledger/identity.rs/pull/1067)
-- Update to `iota-client` 2.0.1-rc.4 and `iota-client-wasm` 0.5.0-alpha.6 [#1088](https://github.com/iotaledger/identity.rs/pull/1088)
### Patch
-- Remove legacy crates [#1080](https://github.com/iotaledger/identity.rs/pull/1080)
+- Fix RevocationBitmap2022 encoding bug [\#1292](https://github.com/iotaledger/identity.rs/pull/1292)
+- Credentials cannot be unrevoked with StatusList2021 [\#1284](https://github.com/iotaledger/identity.rs/pull/1284)
+- Validate domain-linkage URL making sure they only include an origin [\#1267](https://github.com/iotaledger/identity.rs/pull/1267)
-## [v0.7.0-alpha.3](https://github.com/iotaledger/identity.rs/tree/v0.7.0-alpha.3) (2022-09-30)
+## [v1.0.0](https://github.com/iotaledger/identity.rs/tree/v1.0.0) (2023-11-02)
-[Full Changelog](https://github.com/iotaledger/identity.rs/compare/v0.7.0-alpha.2...v0.7.0-alpha.3)
-
-### Changed
-
-- Update iota client 2.0.1 rc.3 [\#1062](https://github.com/iotaledger/identity.rs/pull/1062)
-## [v0.7.0-alpha.2](https://github.com/iotaledger/identity.rs/tree/v0.7.0-alpha.2) (2022-09-30)
-
-[Full Changelog](https://github.com/iotaledger/identity.rs/compare/v0.7.0-alpha.1...v0.7.0-alpha.2)
+[Full Changelog](https://github.com/iotaledger/identity.rs/compare/v0.6.0...v1.0.0)
### Changed
+- Add dedicated stronghold crate [\#1243](https://github.com/iotaledger/identity.rs/pull/1243)
+- Allow custom `kid` to be set in JWS [\#1239](https://github.com/iotaledger/identity.rs/pull/1239)
+- Add dedicated EdDSA verifier crate [\#1238](https://github.com/iotaledger/identity.rs/pull/1238)
+- Remove `vp` and `vc` from JWT claims in JOSE [\#1233](https://github.com/iotaledger/identity.rs/pull/1233)
+- Change `verifiable_credential` to type `Vec` in `Presentation` [\#1231](https://github.com/iotaledger/identity.rs/pull/1231)
+- Mark error enums as non-exhaustive [\#1227](https://github.com/iotaledger/identity.rs/pull/1227)
+- Bring `JwkDocumentExt` names in line with Wasm [\#1223](https://github.com/iotaledger/identity.rs/pull/1223)
+- Add lints for all crates [\#1222](https://github.com/iotaledger/identity.rs/pull/1222)
+- Bump `iota-sdk` and other dependencies [\#1208](https://github.com/iotaledger/identity.rs/pull/1208)
+- Polish `identity_credential` [\#1205](https://github.com/iotaledger/identity.rs/pull/1205)
+- Polish `identity_resolver` and `identity_storage` [\#1204](https://github.com/iotaledger/identity.rs/pull/1204)
+- Polish `identity_iota_core` [\#1203](https://github.com/iotaledger/identity.rs/pull/1203)
+- Rename `JwtPresentation` to `Presentation` [\#1200](https://github.com/iotaledger/identity.rs/pull/1200)
+- Polish `identity_document` [\#1198](https://github.com/iotaledger/identity.rs/pull/1198)
+- Polish `identity_did` & `identity_verification` [\#1197](https://github.com/iotaledger/identity.rs/pull/1197)
+- Polish `identity_core` [\#1196](https://github.com/iotaledger/identity.rs/pull/1196)
+- Remove identity-diff remains [\#1195](https://github.com/iotaledger/identity.rs/pull/1195)
+- Remove legacy signing and verification APIs [\#1194](https://github.com/iotaledger/identity.rs/pull/1194)
+- Remove old `Presentation` type [\#1190](https://github.com/iotaledger/identity.rs/pull/1190)
+- Remove reexported `Resolver` validation APIs [\#1183](https://github.com/iotaledger/identity.rs/pull/1183)
+- Use JWT credentials for Domain Linkage [\#1180](https://github.com/iotaledger/identity.rs/pull/1180)
+- Remove `identity_agent` & `identity_comm` [\#1168](https://github.com/iotaledger/identity.rs/pull/1168)
+- Remove `identity-diff` crate [\#1167](https://github.com/iotaledger/identity.rs/pull/1167)
+- JwkStorageDocument & JwtCredential validation [\#1152](https://github.com/iotaledger/identity.rs/pull/1152)
+- Adapt StorageError to be more generic [\#1144](https://github.com/iotaledger/identity.rs/pull/1144)
+- Add initial PublicKeyJwk support [\#1143](https://github.com/iotaledger/identity.rs/pull/1143)
+- Split JWS `Decoder` functionality [\#1133](https://github.com/iotaledger/identity.rs/pull/1133)
+- Refactor `MethodType` to make it extensible [\#1112](https://github.com/iotaledger/identity.rs/pull/1112)
+- Remove generics in `CoreDocument`, `VerificationMethod`, `Service`, `DIDUrl` and `LinkedDomainService` [\#1110](https://github.com/iotaledger/identity.rs/pull/1110)
+- `CoreDocument` & `Service` and `VerificationMethod` are now in the `document` and `verification` modules respectively [\#1104](https://github.com/iotaledger/identity.rs/pull/1104)
+- Update to `iota-client` 2.0.1-rc.4 and `iota-client-wasm` 0.5.0-alpha.6 [\#1088](https://github.com/iotaledger/identity.rs/pull/1088)
+- Fix clippy lints [\#1069](https://github.com/iotaledger/identity.rs/pull/1069)
+- More identifier checks in `CoreDocument` [\#1067](https://github.com/iotaledger/identity.rs/pull/1067)
+- Update iota client 2.0.1 rc.3 [\#1062](https://github.com/iotaledger/identity.rs/pull/1062)
- Use Bech32-encoded state controller and governor addresses [\#1044](https://github.com/iotaledger/identity.rs/pull/1044)
-- Expose iteration over verification relationship fields [\#1024](https://github.com/iotaledger/identity.rs/pull/1024)
-
-### Added
-
-- Expose Controller and Governor Addresses in metadata [\#1023](https://github.com/iotaledger/identity.rs/pull/1023)
-
-### Patch
-
-- Recommend unique `credentialStatus.id` in `RevocationBitmap2022` [\#1039](https://github.com/iotaledger/identity.rs/pull/1039)
-
-## [v0.7.0-alpha.1](https://github.com/iotaledger/identity.rs/tree/v0.7.0-alpha.1) (2022-09-19)
-
-[Full Changelog](https://github.com/iotaledger/identity.rs/compare/v0.6.0...v0.7.0-alpha.1)
-
-This version introduces a new DID method targeting the IOTA UTXO ledger. This method works fundamentally differently from the previous method and introduces new capabilities to interact with Layer 1 entities like native tokens, NFTs and smart contracts.
-
- This is an early alpha release, so there may be breaking changes in upcoming versions that invalidate DIDs created with this version. The method at this point is only intended for experimentation.
-
- Note: Identities created with the earlier versions cannot be resolved with this version of the library.
-
-### Changed
-
- Remove `identity_agent` reexport [\#1031](https://github.com/iotaledger/identity.rs/pull/1031)
- Rename `MixedResolver` to `Resolver` in Wasm [\#1026](https://github.com/iotaledger/identity.rs/pull/1026)
+- Expose iteration over verification relationship fields [\#1024](https://github.com/iotaledger/identity.rs/pull/1024)
- Add length prefix to DID Document payloads [\#1010](https://github.com/iotaledger/identity.rs/pull/1010)
- Feature-gate `Resolver` [\#1007](https://github.com/iotaledger/identity.rs/pull/1007)
- Rename `Stardust` types to `Iota` [\#1000](https://github.com/iotaledger/identity.rs/pull/1000)
@@ -383,6 +112,26 @@ This version introduces a new DID method targeting the IOTA UTXO ledger. This me
### Added
+- Allow arbitrary JWS header parameters [\#1245](https://github.com/iotaledger/identity.rs/pull/1245)
+- Allow custom JWT claims for presentations [\#1244](https://github.com/iotaledger/identity.rs/pull/1244)
+- Allow custom JWT claims for credentials [\#1237](https://github.com/iotaledger/identity.rs/pull/1237)
+- Use `VC Data Model v1.1` JWT encoding instead of `VC-JWT` [\#1234](https://github.com/iotaledger/identity.rs/pull/1234)
+- Improve `Proof` [\#1209](https://github.com/iotaledger/identity.rs/pull/1209)
+- Polish `identity_jose` [\#1201](https://github.com/iotaledger/identity.rs/pull/1201)
+- Add `resolve_multiple` to Resolver [\#1189](https://github.com/iotaledger/identity.rs/pull/1189)
+- Make JWT presentations generic [\#1186](https://github.com/iotaledger/identity.rs/pull/1186)
+- Support JWT Presentations [\#1175](https://github.com/iotaledger/identity.rs/pull/1175)
+- Polish JWK thumbprint and document extension API [\#1173](https://github.com/iotaledger/identity.rs/pull/1173)
+- Stronghold Storage Implementation [\#1157](https://github.com/iotaledger/identity.rs/pull/1157)
+- Implement `KeyIdStorage` in Rust [\#1134](https://github.com/iotaledger/identity.rs/pull/1134)
+- Introduce `IToCoreDocument` and document locks in the bindings [\#1120](https://github.com/iotaledger/identity.rs/pull/1120)
+- Implement `JwkStorage` [\#1116](https://github.com/iotaledger/identity.rs/pull/1116)
+- Add JSON Object Signing capabilities [\#1105](https://github.com/iotaledger/identity.rs/pull/1105)
+- Add Support for Domain Linkage in Rust [\#1094](https://github.com/iotaledger/identity.rs/pull/1094)
+- Make `StateMetadataDocument` public [\#1085](https://github.com/iotaledger/identity.rs/pull/1085)
+- Add revocation examples [\#1076](https://github.com/iotaledger/identity.rs/pull/1076)
+- Add v. credentials and presentations examples [\#1070](https://github.com/iotaledger/identity.rs/pull/1070)
+- Expose Controller and Governor Addresses in metadata [\#1023](https://github.com/iotaledger/identity.rs/pull/1023)
- Add Stardust Client Extension Trait [\#963](https://github.com/iotaledger/identity.rs/pull/963)
- Add StardustDID [\#949](https://github.com/iotaledger/identity.rs/pull/949)
- State metadata serialization for the stardust DID method [\#947](https://github.com/iotaledger/identity.rs/pull/947)
@@ -391,6 +140,13 @@ This version introduces a new DID method targeting the IOTA UTXO ledger. This me
### Patch
+- Fix holder claim check in VP [\#1236](https://github.com/iotaledger/identity.rs/pull/1236)
+- Fix issuer claim check in VC [\#1235](https://github.com/iotaledger/identity.rs/pull/1235)
+- Feature-gate Domain Linkage [\#1184](https://github.com/iotaledger/identity.rs/pull/1184)
+- Replace `iota-client` with `iota-sdk` [\#1161](https://github.com/iotaledger/identity.rs/pull/1161)
+- Pin `form_urlencoded` to `1.1.0` [\#1136](https://github.com/iotaledger/identity.rs/pull/1136)
+- Remove legacy crates [\#1080](https://github.com/iotaledger/identity.rs/pull/1080)
+- Recommend unique `credentialStatus.id` in `RevocationBitmap2022` [\#1039](https://github.com/iotaledger/identity.rs/pull/1039)
- Pin agent dev-dependencies to crates versions [\#1029](https://github.com/iotaledger/identity.rs/pull/1029)
- Support case insensitive serialization of `RentStructure` [\#1012](https://github.com/iotaledger/identity.rs/pull/1012)
- Update stronghold to 0.6.4 [\#928](https://github.com/iotaledger/identity.rs/pull/928)
diff --git a/Cargo.toml b/Cargo.toml
index d4726e0d4c..a0375aa810 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -12,17 +12,20 @@ members = [
"identity_verification",
"identity_stronghold",
"identity_jose",
+ "identity_ecdsa_verifier",
"identity_eddsa_verifier",
"examples",
]
-exclude = ["bindings/wasm"]
+exclude = ["bindings/wasm", "bindings/grpc"]
[workspace.dependencies]
serde = { version = "1.0", default-features = false, features = ["alloc", "derive"] }
thiserror = { version = "1.0", default-features = false }
strum = { version = "0.25", default-features = false, features = ["std", "derive"] }
serde_json = { version = "1.0", default-features = false }
+json-proof-token = { version = "0.3.5" }
+zkryptium = { version = "0.2.2", default-features = false, features = ["bbsplus"] }
[workspace.package]
authors = ["IOTA Stiftung"]
@@ -31,3 +34,6 @@ homepage = "https://www.iota.org"
license = "Apache-2.0"
repository = "https://github.com/iotaledger/identity.rs"
rust-version = "1.65"
+
+[workspace.lints.clippy]
+result_large_err = "allow"
diff --git a/README.md b/README.md
index 564817faa8..af03f510af 100644
--- a/README.md
+++ b/README.md
@@ -24,7 +24,7 @@
## Introduction
-IOTA Identity is a [Rust](https://www.rust-lang.org/) implementation of decentralized digital identity, also known as Self-Sovereign Identity (SSI). It implements the W3C [Decentralized Identifiers (DID)](https://www.w3.org/TR/did-core/) and [Verifiable Credentials](https://www.w3.org/TR/vc-data-model/) specifications. This library can be used to create, resolve and authenticate digital identities and to create verifiable credentials and presentations in order to share information in a verifiable manner and establish trust in the digital world. It does so while supporting secure storage of cryptographic keys, which can be implemented for your preferred key management system. Many of the individual libraries (Rust crates) are agnostic over the concrete DID method, with the exception of some libraries dedicated to implement the [IOTA DID method](https://wiki.iota.org/shimmer/identity.rs/specs/did/iota_did_method_spec/), which is an implementation of decentralized digital identity on the IOTA and Shimmer networks. Written in stable Rust, IOTA Identity has strong guarantees of memory safety and process integrity while maintaining exceptional performance.
+IOTA Identity is a [Rust](https://www.rust-lang.org/) implementation of decentralized digital identity, also known as Self-Sovereign Identity (SSI). It implements the W3C [Decentralized Identifiers (DID)](https://www.w3.org/TR/did-core/) and [Verifiable Credentials](https://www.w3.org/TR/vc-data-model/) specifications. This library can be used to create, resolve and authenticate digital identities and to create verifiable credentials and presentations in order to share information in a verifiable manner and establish trust in the digital world. It does so while supporting secure storage of cryptographic keys, which can be implemented for your preferred key management system. Many of the individual libraries (Rust crates) are agnostic over the concrete DID method, with the exception of some libraries dedicated to implement the [IOTA DID method](https://wiki.iota.org/identity.rs/specs/did/iota_did_method_spec/), which is an implementation of decentralized digital identity on the IOTA and Shimmer networks. Written in stable Rust, IOTA Identity has strong guarantees of memory safety and process integrity while maintaining exceptional performance.
## Bindings
@@ -32,12 +32,15 @@ IOTA Identity is a [Rust](https://www.rust-lang.org/) implementation of decentra
- [Web Assembly](https://github.com/iotaledger/identity.rs/blob/HEAD/bindings/wasm/) (JavaScript/TypeScript)
+## gRPC
+
+We provide a collection of experimental [gRPC services](https://github.com/iotaledger/identity.rs/blob/HEAD/bindings/grpc/)
## Documentation and Resources
- API References:
- [Rust API Reference](https://docs.rs/identity_iota/latest/identity_iota/): Package documentation (cargo docs).
- - [Wasm API Reference](https://wiki.iota.org/shimmer/identity.rs/libraries/wasm/api_reference/): Wasm Package documentation.
-- [Identity Documentation Pages](https://wiki.iota.org/shimmer/identity.rs/introduction): Supplementing documentation with context around identity and simple examples on library usage.
+ - [Wasm API Reference](https://wiki.iota.org/identity.rs/libraries/wasm/api_reference/): Wasm Package documentation.
+- [Identity Documentation Pages](https://wiki.iota.org/identity.rs/introduction): Supplementing documentation with context around identity and simple examples on library usage.
- [Examples](https://github.com/iotaledger/identity.rs/blob/HEAD/examples): Practical code snippets to get you started with the library.
## Prerequisites
@@ -51,22 +54,33 @@ If you want to include IOTA Identity in your project, simply add it as a depende
```toml
[dependencies]
-identity_iota = { version = "1.0.0" }
+identity_iota = { version = "1.3.0" }
```
To try out the [examples](https://github.com/iotaledger/identity.rs/blob/HEAD/examples), you can also do this:
1. Clone the repository, e.g. through `git clone https://github.com/iotaledger/identity.rs`
-2. Start a private Tangle as described in the [next section](#example-creating-an-identity)
+2. Start IOTA Sandbox as described in the [next section](#example-creating-an-identity)
3. Run the example to create a DID using `cargo run --release --example 0_create_did`
## Example: Creating an Identity
The following code creates and publishes a new IOTA DID Document to a locally running private network.
-See the [instructions](https://github.com/iotaledger/hornet/tree/develop/private_tangle) on running your own private network.
+See the [instructions](https://github.com/iotaledger/iota-sandbox) on running your own private network for development.
_Cargo.toml_
+
+
+
```toml
[package]
name = "iota_identity_example"
@@ -74,13 +88,28 @@ version = "1.0.0"
edition = "2021"
[dependencies]
-identity_iota = {version = "1.0.0", features = ["memstore"]}
+identity_iota = { version = "1.3.0", features = ["memstore"] }
iota-sdk = { version = "1.0.2", default-features = true, features = ["tls", "client", "stronghold"] }
tokio = { version = "1", features = ["full"] }
+anyhow = "1.0.62"
+rand = "0.8.5"
```
_main._ _rs_
+
+
+
+
```rust,no_run
use identity_iota::core::ToJson;
use identity_iota::iota::IotaClientExt;
@@ -104,7 +133,7 @@ use iota_sdk::types::block::output::dto::AliasOutputDto;
use tokio::io::AsyncReadExt;
// The endpoint of the IOTA node to use.
-static API_ENDPOINT: &str = "http://127.0.0.1:14265";
+static API_ENDPOINT: &str = "http://localhost";
/// Demonstrates how to create a DID Document and publish it in a new Alias Output.
#[tokio::main]
@@ -142,7 +171,7 @@ async fn main() -> anyhow::Result<()> {
.await?[0];
println!("Your wallet address is: {}", address);
- println!("Please request funds from http://127.0.0.1:8091/, wait for a couple of seconds and then press Enter.");
+ println!("Please request funds from http://localhost/faucet/, wait for a couple of seconds and then press Enter.");
tokio::io::stdin().read_u8().await?;
// Create a new DID document with a placeholder DID.
@@ -212,7 +241,7 @@ For detailed development progress, see the IOTA Identity development [kanban boa
We would love to have you help us with the development of IOTA Identity. Each and every contribution is greatly valued!
-Please review the [contribution](https://wiki.iota.org/shimmer/identity.rs/contribute) and [workflow](https://wiki.iota.org/shimmer/identity.rs/workflow) sections in the [IOTA Wiki](https://wiki.iota.org/).
+Please review the [contribution](https://wiki.iota.org/identity.rs/contribute) and [workflow](https://wiki.iota.org/identity.rs/workflow) sections in the [IOTA Wiki](https://wiki.iota.org/).
To contribute directly to the repository, simply fork the project, push your changes to your fork and create a pull request to get them included!
diff --git a/bindings/grpc/Cargo.toml b/bindings/grpc/Cargo.toml
new file mode 100644
index 0000000000..2b542712db
--- /dev/null
+++ b/bindings/grpc/Cargo.toml
@@ -0,0 +1,43 @@
+[package]
+name = "identity-grpc"
+version = "0.1.0"
+authors = ["IOTA Stiftung"]
+edition = "2021"
+homepage = "https://www.iota.org"
+license = "Apache-2.0"
+repository = "https://github.com/iotaledger/identity.rs"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[lib]
+path = "src/lib.rs"
+
+[[bin]]
+name = "identity-grpc"
+path = "src/main.rs"
+
+[dependencies]
+anyhow = "1.0.75"
+futures = { version = "0.3" }
+identity_eddsa_verifier = { path = "../../identity_eddsa_verifier" }
+identity_iota = { path = "../../identity_iota", features = ["resolver", "sd-jwt", "domain-linkage", "domain-linkage-fetch", "status-list-2021"] }
+identity_stronghold = { path = "../../identity_stronghold", features = ["send-sync-storage"] }
+iota-sdk = { version = "1.1.5", features = ["stronghold"] }
+openssl = { version = "0.10", features = ["vendored"] }
+prost = "0.12"
+rand = "0.8.5"
+serde = { version = "1.0.193", features = ["derive", "alloc"] }
+serde_json = { version = "1.0.108", features = ["alloc"] }
+thiserror = "1.0.50"
+tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] }
+tokio-stream = { version = "0.1.14", features = ["net"] }
+tonic = "0.10"
+tracing = { version = "0.1.40", features = ["async-await"] }
+tracing-subscriber = "0.3.18"
+url = { version = "2.5", default-features = false }
+
+[dev-dependencies]
+identity_storage = { path = "../../identity_storage", features = ["memstore"] }
+
+[build-dependencies]
+tonic-build = "0.10"
diff --git a/bindings/grpc/Dockerfile b/bindings/grpc/Dockerfile
new file mode 100644
index 0000000000..b7faca7c63
--- /dev/null
+++ b/bindings/grpc/Dockerfile
@@ -0,0 +1,20 @@
+FROM rust:bookworm as builder
+
+# install protobuf
+RUN apt-get update && apt-get install -y protobuf-compiler libprotobuf-dev musl-tools
+
+COPY . /usr/src/app/
+WORKDIR /usr/src/app/bindings/grpc
+RUN rustup target add x86_64-unknown-linux-musl
+RUN cargo build --target x86_64-unknown-linux-musl --release --bin identity-grpc
+
+FROM gcr.io/distroless/static-debian11 as runner
+
+# get binary
+COPY --from=builder /usr/src/app/bindings/grpc/target/x86_64-unknown-linux-musl/release/identity-grpc /
+
+# set run env
+EXPOSE 50051
+
+# run it
+CMD ["/identity-grpc"]
\ No newline at end of file
diff --git a/bindings/grpc/README.md b/bindings/grpc/README.md
new file mode 100644
index 0000000000..f94f0add17
--- /dev/null
+++ b/bindings/grpc/README.md
@@ -0,0 +1,130 @@
+# Identity.rs gRPC Bindings
+This project provides the functionalities of [Identity.rs](https://github.com/iotaledger/identity.rs) in a language-agnostic way through a [gRPC](https://grpc.io) server.
+
+The server can easily be run with docker using [this dockerfile](https://github.com/iotaledger/identity.rs/blob/main/bindings/grpc/Dockerfile).
+
+## Build
+Run `docker build -f bindings/grpc/Dockerfile -t iotaleger/identity-grpc .` from the project root.
+
+### Dockerimage env variables and volume binds
+The provided docker image requires the following variables to be set in order to properly work:
+- `API_ENDPOINT`: IOTA node address.
+- `STRONGHOLD_PWD`: Stronghold password.
+- `SNAPSHOT_PATH`: Stronghold's snapshot location.
+
+Make sure to provide a valid stronghold snapshot at the provided `SNAPSHOT_PATH` prefilled with all the needed key material.
+
+### Available services
+| Service description | Service Id | Proto File |
+| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------|
+| Credential Revocation Checking | `credentials/CredentialRevocation.check` | [credentials.proto](https://github.com/iotaledger/identity.rs/blob/main/bindings/grpc/proto/credentials.proto) |
+| SD-JWT Validation | `sd_jwt/Verification.verify` | [sd_jwt.proto](https://github.com/iotaledger/identity.rs/blob/main/bindings/grpc/proto/sd_jwt.proto) |
+| Credential JWT creation | `credentials/Jwt.create` | [credentials.proto](https://github.com/iotaledger/identity.rs/blob/main/bindings/grpc/proto/credentials.proto) |
+| Credential JWT validation | `credentials/VcValidation.validate` | [credentials.proto](https://github.com/iotaledger/identity.rs/blob/main/bindings/grpc/proto/credentials.proto) |
+| DID Document Creation | `document/DocumentService.create` | [document.proto](https://github.com/iotaledger/identity.rs/blob/main/bindings/grpc/proto/document.proto) |
+| Domain Linkage - validate domain, let server fetch did-configuration | `domain_linkage/DomainLinkage.validate_domain` | [domain_linkage.proto](https://github.com/iotaledger/identity.rs/blob/main/bindings/grpc/proto/domain_linkage.proto) |
+| Domain Linkage - validate domain, pass did-configuration to service | `domain_linkage/DomainLinkage.validate_domain_against_did_configuration` | [domain_linkage.proto](https://github.com/iotaledger/identity.rs/blob/main/bindings/grpc/proto/domain_linkage.proto) |
+| Domain Linkage - validate endpoints in DID, let server fetch did-configuration | `domain_linkage/DomainLinkage.validate_did` | [domain_linkage.proto](https://github.com/iotaledger/identity.rs/blob/main/bindings/grpc/proto/domain_linkage.proto) |
+| Domain Linkage - validate endpoints in DID, pass did-configuration to service | `domain_linkage/DomainLinkage.validate_did_against_did_configurations` | [domain_linkage.proto](https://github.com/iotaledger/identity.rs/blob/main/bindings/grpc/proto/domain_linkage.proto) |
+| `StatusList2021Credential` creation | `status_list_2021/StatusList2021Svc.create` | [status_list_2021.proto](https://github.com/iotaledger/identity.rs/blob/main/bindings/grpc/proto/status_list_2021.proto) |
+| `StatusList2021Credential` update | `status_list_2021/StatusList2021Svc.update` | [status_list_2021.proto](https://github.com/iotaledger/identity.rs/blob/main/bindings/grpc/proto/status_list_2021.proto) |
+
+## Testing
+
+### Domain Linkage
+
+#### Http server
+In order to test domain linkage, you need access to a server that is reachable via HTTPS. If you already have one, you can ignore the server setup steps here and and provide the `did-configuration.json` on your server.
+
+1. create a folder with did configuration in it, e.g. (you can also use the template in `./tooling/domain-linkage-test-server`)
+ ```raw
+ test-server/
+ └── .well-known
+ └── did-configuration.json
+ ```
+
+ the `did-configuration` should look like this for now:
+
+ ```json
+ {
+ "@context": "https://identity.foundation/.well-known/did-configuration/v1",
+ "linked_dids": [
+ "add your domain linkage credential here"
+ ]
+ }
+ ```
+1. start a server that will serve this folder, e.g. with a NodeJs "http-server": `http-server ./test-server/`, in this example the server should now be running on local port 8080
+1. tunnel your server's port (here 8080) to a public domain with https, e.g. with ngrok:
+ `ngrok http http://127.0.0.1:8080`
+ the output should now have a line like
+ `Forwarding https://0d40-2003-d3-2710-e200-485f-e8bb-7431-79a7.ngrok-free.app -> http://127.0.0.1:8080`
+ check that the https url is reachable, this will be used in the next step. You can also start ngrok with a static domain, which means you don't have to update credentials after each http server restart
+1. for convenience, you can find a script to start the HTTP server, that you can adjust in `tooling/start-http-server.sh`, don't forget to insert your static domain or to remove the `--domain` parameter
+
+#### Domain linkage credential
+1. copy the public url and insert it into [6_domain_linkage.rs](https://github.com/iotaledger/identity.rs/blob/main/examples/1_advanced/6_domain_linkage.rs) as domain 1, e.g. `let domain_1: Url = Url::parse("https://0d40-2003-d3-2710-e200-485f-e8bb-7431-79a7.ngrok-free.app")?;`
+.1 run the example with `cargo run --release --example 6_domain_linkage`
+
+#### GRPC server
+1. grab the configuration resource from the log and replace the contents of your `did-configuration.json` with it
+1. you now have a publicly reachable (sub)domain, that serves a `did-configuration` file containing a credential pointing to your DID
+1. to verify this, run the server via Docker or with the following command, remember to replace the placeholders ;) `API_ENDPOINT=replace_me STRONGHOLD_PWD=replace_me SNAPSHOT_PATH=replace_me cargo run --release`
+The arguments can be taken from examples, e.g. after running a `6_domain_linkage.rs`, which also logs snapshot path passed to secret manager (`let snapshot_path = random_stronghold_path(); dbg!(&snapshot_path.to_str());`), for example
+ - API_ENDPOINT: `"http://localhost"`
+ - STRONGHOLD_PWD: `"secure_password"`
+ - SNAPSHOT_PATH: `"/var/folders/41/s1sm86jx0xl4x435t81j81440000gn/T/test_strongholds/8o2Nyiv5ENBi7Ik3dEDq9gNzSrqeUdqi.stronghold"`
+1. for convenience, you can find a script to start the GRPC server, that you can adjust in `tooling/start-rpc-server.sh`, don't forget to insert the env variables as described above
+
+#### Calling the endpoints
+1. call the `validate_domain` endpoint with your domain, e.g with:
+
+ ```json
+ {
+ "domain": "https://0d40-2003-d3-2710-e200-485f-e8bb-7431-79a7.ngrok-free.app"
+ }
+ ```
+
+ you should now receive a response like this:
+
+ ```json
+ {
+ "linked_dids": [
+ {
+ "document": "... (compact JWT domain linkage credential)",
+ "status": "ok"
+ }
+ ]
+ }
+ ```
+
+1. to call the `validate_did` endpoint, you need a DID to check, you can find a testable in you domain linkage credential. for this just decode it (e.g. on jwt.io) and get the `iss` value, then you can submit as "did" like following
+
+ ```json
+ {
+ "did": "did:iota:snd:0x967bf8f0c7487f61378611b6a1c6a59cb99e65b839681ee70be691b09a024ab9"
+ }
+ ```
+
+ you should not receive a response like this:
+
+ ```json
+ {
+ "service": [
+ {
+ "service_endpoint": [
+ {
+ "valid": true,
+ "document": "eyJraWQiOiJkaWQ6aW90YTpzbmQ6MHg5NjdiZjhmMGM3NDg3ZjYxMzc4NjExYjZhMWM2YTU5Y2I5OWU2NWI4Mzk2ODFlZTcwYmU2OTFiMDlhMDI0YWI5IzA3QjVWRkxBa0FabkRhaC1OTnYwYUN3TzJ5ZnRzX09ZZ0YzNFNudUloMlUiLCJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSJ9.eyJleHAiOjE3NDE2NzgyNzUsImlzcyI6ImRpZDppb3RhOnNuZDoweDk2N2JmOGYwYzc0ODdmNjEzNzg2MTFiNmExYzZhNTljYjk5ZTY1YjgzOTY4MWVlNzBiZTY5MWIwOWEwMjRhYjkiLCJuYmYiOjE3MTAxNDIyNzUsInN1YiI6ImRpZDppb3RhOnNuZDoweDk2N2JmOGYwYzc0ODdmNjEzNzg2MTFiNmExYzZhNTljYjk5ZTY1YjgzOTY4MWVlNzBiZTY5MWIwOWEwMjRhYjkiLCJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vaWRlbnRpdHkuZm91bmRhdGlvbi8ud2VsbC1rbm93bi9kaWQtY29uZmlndXJhdGlvbi92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiRG9tYWluTGlua2FnZUNyZWRlbnRpYWwiXSwiY3JlZGVudGlhbFN1YmplY3QiOnsib3JpZ2luIjoiaHR0cHM6Ly9ob3QtYnVsbGRvZy1wcm9mb3VuZC5uZ3Jvay1mcmVlLmFwcC8ifX19.69e7T0DbRw9Kz7eEQ96P9E5HWbEo5F1fLuMjyQN6_Oa1lwBdbfj0wLlhS1j_d8AuNmvu60lMdLVixjMZJLQ5AA"
+ },
+ {
+ "valid": false,
+ "error": "domain linkage error: error sending request for url (https://bar.example.com/.well-known/did-configuration.json): error trying to connect: dns error: failed to lookup address information: nodename nor servname provided, or not known"
+ }
+ ],
+ "id": "did:iota:snd:0x967bf8f0c7487f61378611b6a1c6a59cb99e65b839681ee70be691b09a024ab9"
+ }
+ ]
+ }
+ ```
+
+ Which tells us that it found a DID document with one matching service with a serviceEndpoint, that contains two domains. Out of these domains one links back to the given DID, the other domain could not be resolved.
diff --git a/bindings/grpc/build.rs b/bindings/grpc/build.rs
new file mode 100644
index 0000000000..c48bbdce41
--- /dev/null
+++ b/bindings/grpc/build.rs
@@ -0,0 +1,14 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+fn main() -> Result<(), Box> {
+ let proto_files = std::fs::read_dir("./proto")?
+ .filter_map(|entry| entry.ok().map(|e| e.path()))
+ .filter(|path| path.extension().and_then(|ext| ext.to_str()) == Some("proto"));
+
+ for proto in proto_files {
+ tonic_build::compile_protos(proto)?;
+ }
+
+ Ok(())
+}
diff --git a/bindings/grpc/proto/credentials.proto b/bindings/grpc/proto/credentials.proto
new file mode 100644
index 0000000000..ae34c7b4b6
--- /dev/null
+++ b/bindings/grpc/proto/credentials.proto
@@ -0,0 +1,61 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+syntax = "proto3";
+package credentials;
+
+// -- CREDENTIALS REVOCATION ---------------------------------------------
+
+// The States a credential can be in.
+enum RevocationStatus {
+ REVOKED = 0;
+ SUSPENDED = 1;
+ VALID = 2;
+}
+
+message RevocationCheckRequest {
+ string type = 1;
+ string url = 2;
+ map properties = 3;
+}
+
+message RevocationCheckResponse {
+ RevocationStatus status = 1;
+}
+
+service CredentialRevocation {
+ // Checks whether a credential has been revoked with `RevocationBitmap2022`.
+ rpc check(RevocationCheckRequest) returns (RevocationCheckResponse);
+}
+
+message JwtCreationRequest {
+ string credential_json = 1;
+ string issuer_fragment = 2;
+}
+
+message JwtCreationResponse {
+ string jwt = 1;
+}
+
+service Jwt {
+ // Encodes a given JSON credential into JWT, using the issuer's fragment to fetch the key from stronghold.
+ rpc create(JwtCreationRequest) returns (JwtCreationResponse);
+}
+
+message VcValidationRequest {
+ // JWT encoded credential.
+ string credential_jwt = 1;
+ // JSON encoded `StatusList2021Credential`, used for status checking.
+ // If missing, status checking will be performed with `RevocationBitmap2022`.
+ optional string status_list_credential_json = 2;
+}
+
+message VcValidationResponse {
+ // JSON encoded credential (extracted from request's JWT).
+ string credential_json = 1;
+}
+
+service VcValidation {
+ // Performs encoding, syntax, signature, time constraints and status checking on the provided credential.
+ rpc validate(VcValidationRequest) returns (VcValidationResponse);
+}
\ No newline at end of file
diff --git a/bindings/grpc/proto/document.proto b/bindings/grpc/proto/document.proto
new file mode 100644
index 0000000000..d25558c243
--- /dev/null
+++ b/bindings/grpc/proto/document.proto
@@ -0,0 +1,24 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+syntax = "proto3";
+package document;
+
+message CreateDIDRequest {
+ // An IOTA's bech32 encoded address.
+ string bech32_address = 1;
+}
+
+message CreateDIDResponse {
+ // The created DID document, encoded as JSON.
+ string document_json = 1;
+ // The stronghold's fragment for the generated document's auth method.
+ string fragment = 2;
+ // The DID of the created document.
+ string did = 3;
+}
+
+service DocumentService {
+ /// Creates a new DID document stored on Tangle.
+ rpc create(CreateDIDRequest) returns (CreateDIDResponse);
+}
\ No newline at end of file
diff --git a/bindings/grpc/proto/domain_linkage.proto b/bindings/grpc/proto/domain_linkage.proto
new file mode 100644
index 0000000000..f2fe3426df
--- /dev/null
+++ b/bindings/grpc/proto/domain_linkage.proto
@@ -0,0 +1,63 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+syntax = "proto3";
+package domain_linkage;
+
+message ValidateDomainRequest {
+ // domain to validate
+ string domain = 1;
+}
+
+message ValidateDomainAgainstDidConfigurationRequest {
+ // domain to validate
+ string domain = 1;
+ // already resolved domain linkage config
+ string did_configuration = 2;
+}
+
+message LinkedDidValidationStatus {
+ // validation succeeded or not, `error` property is added for `false` cases
+ bool valid = 1;
+ // credential from `linked_dids` as compact JWT domain linkage credential if it could be retrieved
+ optional string document = 2;
+ // an error message, that occurred when validated, omitted if valid
+ optional string error = 3;
+}
+
+message ValidateDomainResponse {
+ // list of JWT domain linkage credential, uses the same order as the `did-configuration.json` file for domain
+ repeated LinkedDidValidationStatus linked_dids = 1;
+}
+
+message LinkedDidEndpointValidationStatus {
+ // id of service endpoint entry
+ string id = 1;
+ // list of JWT domain linkage credential, uses the same order as the `did-configuration.json` file for domain
+ repeated LinkedDidValidationStatus service_endpoint = 2;
+}
+
+message ValidateDidRequest {
+ // DID to validate
+ string did = 1;
+}
+
+message ValidateDidAgainstDidConfigurationsRequest {
+ // DID to validate
+ string did = 1;
+ // already resolved domain linkage configs
+ repeated ValidateDomainAgainstDidConfigurationRequest did_configurations = 2;
+}
+
+message ValidateDidResponse {
+ // mapping of service entries from DID with validation status for endpoint URLs
+ repeated LinkedDidEndpointValidationStatus service = 1;
+}
+
+service DomainLinkage {
+ rpc validate_domain(ValidateDomainRequest) returns (ValidateDomainResponse);
+ rpc validate_domain_against_did_configuration(ValidateDomainAgainstDidConfigurationRequest) returns (ValidateDomainResponse);
+
+ rpc validate_did(ValidateDidRequest) returns (ValidateDidResponse);
+ rpc validate_did_against_did_configurations(ValidateDidAgainstDidConfigurationsRequest) returns (ValidateDidResponse);
+}
\ No newline at end of file
diff --git a/bindings/grpc/proto/health_check.proto b/bindings/grpc/proto/health_check.proto
new file mode 100644
index 0000000000..0c4bee8ba5
--- /dev/null
+++ b/bindings/grpc/proto/health_check.proto
@@ -0,0 +1,15 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+syntax = "proto3";
+package health_check;
+
+message HealthCheckRequest {}
+
+message HealthCheckResponse {
+ string status = 1;
+}
+
+service HealthCheck {
+ rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
+}
\ No newline at end of file
diff --git a/bindings/grpc/proto/sd_jwt.proto b/bindings/grpc/proto/sd_jwt.proto
new file mode 100644
index 0000000000..86d6b5f7fe
--- /dev/null
+++ b/bindings/grpc/proto/sd_jwt.proto
@@ -0,0 +1,30 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+syntax = "proto3";
+package sd_jwt;
+
+message KeyBindingOptions {
+ optional string nonce = 1;
+ optional string aud = 2;
+ // TODO: add JWS validation options
+ optional string earliest_issuance_date = 3;
+ optional string latest_issuance_date = 4;
+ string holder_did = 5;
+}
+
+message VerificationRequest {
+ // SD-JWT encoded credential.
+ string jwt = 1;
+ optional KeyBindingOptions kb_options = 2;
+}
+
+message VerificationResponse {
+ // JSON encoded credential, extracted from the request's SD-JWT.
+ string credential = 1;
+}
+
+service Verification {
+ // Performs all validation steps on a SD-JWT encoded credential.
+ rpc verify(VerificationRequest) returns (VerificationResponse);
+}
\ No newline at end of file
diff --git a/bindings/grpc/proto/status_list_2021.proto b/bindings/grpc/proto/status_list_2021.proto
new file mode 100644
index 0000000000..f84eb738b1
--- /dev/null
+++ b/bindings/grpc/proto/status_list_2021.proto
@@ -0,0 +1,50 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+syntax = "proto3";
+package status_list_2021;
+
+enum Purpose {
+ REVOCATION = 0;
+ SUSPENSION = 1;
+}
+
+message CreateRequest {
+ // Whether this status list will be used for revoking or suspending credentials.
+ Purpose purpose = 1;
+ // Amount of entries in the status list (a minimum of 131072 entries is required).
+ optional uint64 length = 2;
+ // The URL that identifies the credential.
+ optional string id = 3;
+ // Timestamp representing the expiration date for this credential, if it has to expire.
+ optional string expiration_date = 4;
+ // A list of credential's contexts, used to fill the credential's "@context" property.
+ // "https://www.w3.org/2018/credentials/v1" is provided by default.
+ repeated string contexts = 5;
+ // A list of credential's types, used to fill the credential's "type" property.
+ // "VerifiableCredential" is provided by default.
+ repeated string types = 6;
+ // The issuer DID URL.
+ string issuer = 7;
+}
+
+message StatusListCredential {
+ // JSON encoded `StatusList2021Credential`.
+ string credential_json = 1;
+}
+
+message UpdateRequest {
+ // JSON encoded `StatusList2021Credential`.
+ string credential_json = 1;
+ // Changes to apply to the status list represented as the map "entry-index -> bool value"
+ // where `true` means that the entry at the given index is revoked/suspended depending on
+ // the list's purpose.
+ map entries = 2;
+}
+
+service StatusList2021Svc {
+ // Creates a new `StatusList2021Credential`.
+ rpc create(CreateRequest) returns(StatusListCredential);
+ // Sets the value for a list of entries in the provided `StatusList2021Credential`.
+ rpc update(UpdateRequest) returns(StatusListCredential);
+}
diff --git a/bindings/grpc/proto/utils.proto b/bindings/grpc/proto/utils.proto
new file mode 100644
index 0000000000..87ea3f7054
--- /dev/null
+++ b/bindings/grpc/proto/utils.proto
@@ -0,0 +1,23 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+syntax = "proto3";
+package utils;
+
+message DataSigningRequest {
+ // Raw data that will be signed.
+ bytes data = 1;
+ // Signing key's ID.
+ string key_id = 2;
+}
+
+message DataSigningResponse {
+ // Raw data signature.
+ bytes signature = 1;
+}
+
+// Service that handles signing operations on raw data.
+service Signing {
+ rpc sign(DataSigningRequest) returns (DataSigningResponse);
+}
+
diff --git a/bindings/grpc/src/lib.rs b/bindings/grpc/src/lib.rs
new file mode 100644
index 0000000000..d26756e597
--- /dev/null
+++ b/bindings/grpc/src/lib.rs
@@ -0,0 +1,7 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+#![allow(clippy::blocks_in_conditions)]
+
+pub mod server;
+pub mod services;
diff --git a/bindings/grpc/src/main.rs b/bindings/grpc/src/main.rs
new file mode 100644
index 0000000000..04927b1c9c
--- /dev/null
+++ b/bindings/grpc/src/main.rs
@@ -0,0 +1,63 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+use anyhow::Context;
+use identity_grpc::server::GRpcServer;
+use identity_stronghold::StrongholdStorage;
+use iota_sdk::client::stronghold::StrongholdAdapter;
+use iota_sdk::client::Client;
+
+#[tokio::main]
+#[tracing::instrument(err)]
+async fn main() -> anyhow::Result<()> {
+ tracing::subscriber::set_global_default(tracing_subscriber::fmt().compact().finish())
+ .expect("Failed to setup global tracing subscriber.");
+
+ let api_endpoint = std::env::var("API_ENDPOINT")?;
+
+ let client: Client = Client::builder()
+ .with_primary_node(&api_endpoint, None)?
+ .finish()
+ .await?;
+ let stronghold = init_stronghold()?;
+
+ let addr = "0.0.0.0:50051".parse()?;
+ tracing::info!("gRPC server listening on {}", addr);
+ GRpcServer::new(client, stronghold).serve(addr).await?;
+
+ Ok(())
+}
+
+#[tracing::instrument]
+fn init_stronghold() -> anyhow::Result {
+ use std::env;
+ use std::fs;
+ let stronghold_password = env::var("STRONGHOLD_PWD_FILE")
+ .context("Unset \"STRONGHOLD_PWD_FILE\" env variable")
+ .and_then(|path| fs::read_to_string(&path).context(format!("{path} does not exists")))
+ .map(sanitize_pwd)
+ .or(env::var("STRONGHOLD_PWD"))
+ .context("No password for stronghold was provided")?;
+ let snapshot_path = env::var("SNAPSHOT_PATH")?;
+
+ // Check for snapshot file at specified path
+ let metadata = fs::metadata(&snapshot_path)?;
+ if !metadata.is_file() {
+ return Err(anyhow::anyhow!("No snapshot at provided path \"{}\"", &snapshot_path));
+ }
+
+ Ok(
+ StrongholdAdapter::builder()
+ .password(stronghold_password)
+ .build(snapshot_path)
+ .map(StrongholdStorage::new)?,
+ )
+}
+
+/// Remove any trailing whitespace in-place.
+fn sanitize_pwd(mut pwd: String) -> String {
+ let trimmed = pwd.trim_end();
+ pwd.truncate(trimmed.len());
+ pwd.shrink_to_fit();
+ pwd
+}
diff --git a/bindings/grpc/src/server.rs b/bindings/grpc/src/server.rs
new file mode 100644
index 0000000000..c7fa5b527c
--- /dev/null
+++ b/bindings/grpc/src/server.rs
@@ -0,0 +1,33 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+use std::net::SocketAddr;
+
+use identity_stronghold::StrongholdStorage;
+use iota_sdk::client::Client;
+use tonic::transport::server::Router;
+use tonic::transport::server::Server;
+
+use crate::services;
+
+#[derive(Debug)]
+pub struct GRpcServer {
+ router: Router,
+ stronghold: StrongholdStorage,
+}
+
+impl GRpcServer {
+ pub fn new(client: Client, stronghold: StrongholdStorage) -> Self {
+ let router = Server::builder().add_routes(services::routes(&client, &stronghold));
+ Self { router, stronghold }
+ }
+ pub async fn serve(self, addr: SocketAddr) -> Result<(), tonic::transport::Error> {
+ self.router.serve(addr).await
+ }
+ pub fn into_router(self) -> Router {
+ self.router
+ }
+ pub fn stronghold(&self) -> StrongholdStorage {
+ self.stronghold.clone()
+ }
+}
diff --git a/bindings/grpc/src/services/credential/jwt.rs b/bindings/grpc/src/services/credential/jwt.rs
new file mode 100644
index 0000000000..6cfb3368e6
--- /dev/null
+++ b/bindings/grpc/src/services/credential/jwt.rs
@@ -0,0 +1,85 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+use _credentials::jwt_server::Jwt as JwtSvc;
+use identity_iota::core::FromJson;
+use identity_iota::core::Object;
+use identity_iota::credential::Credential;
+use identity_iota::iota::IotaDID;
+use identity_iota::iota::IotaDocument;
+use identity_iota::resolver::Resolver;
+use identity_iota::storage::JwkDocumentExt;
+use identity_iota::storage::JwsSignatureOptions;
+use identity_iota::storage::Storage;
+use identity_stronghold::StrongholdStorage;
+use iota_sdk::client::Client;
+use tonic::Request;
+use tonic::Response;
+use tonic::Status;
+
+use self::_credentials::jwt_server::JwtServer;
+use self::_credentials::JwtCreationRequest;
+use self::_credentials::JwtCreationResponse;
+
+mod _credentials {
+ tonic::include_proto!("credentials");
+}
+
+pub struct JwtService {
+ resolver: Resolver,
+ storage: Storage,
+}
+
+impl JwtService {
+ pub fn new(client: &Client, stronghold: &StrongholdStorage) -> Self {
+ let mut resolver = Resolver::new();
+ resolver.attach_iota_handler(client.clone());
+ Self {
+ resolver,
+ storage: Storage::new(stronghold.clone(), stronghold.clone()),
+ }
+ }
+}
+
+#[tonic::async_trait]
+impl JwtSvc for JwtService {
+ #[tracing::instrument(
+ name = "create_jwt_credential",
+ skip_all,
+ fields(request = ?req.get_ref())
+ ret,
+ err,
+ )]
+ async fn create(&self, req: Request) -> Result, Status> {
+ let JwtCreationRequest {
+ credential_json,
+ issuer_fragment,
+ } = req.into_inner();
+ let credential =
+ Credential::::from_json(credential_json.as_str()).map_err(|e| Status::invalid_argument(e.to_string()))?;
+ let issuer_did =
+ IotaDID::parse(credential.issuer.url().as_str()).map_err(|e| Status::invalid_argument(e.to_string()))?;
+ let issuer_document = self
+ .resolver
+ .resolve(&issuer_did)
+ .await
+ .map_err(|e| Status::not_found(e.to_string()))?;
+
+ let jwt = issuer_document
+ .create_credential_jwt(
+ &credential,
+ &self.storage,
+ &issuer_fragment,
+ &JwsSignatureOptions::default(),
+ None,
+ )
+ .await
+ .map_err(|e| Status::internal(e.to_string()))?;
+
+ Ok(Response::new(JwtCreationResponse { jwt: jwt.into() }))
+ }
+}
+
+pub fn service(client: &Client, stronghold: &StrongholdStorage) -> JwtServer {
+ JwtServer::new(JwtService::new(client, stronghold))
+}
diff --git a/bindings/grpc/src/services/credential/mod.rs b/bindings/grpc/src/services/credential/mod.rs
new file mode 100644
index 0000000000..8d71ccacee
--- /dev/null
+++ b/bindings/grpc/src/services/credential/mod.rs
@@ -0,0 +1,16 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+pub mod jwt;
+pub mod revocation;
+pub mod validation;
+
+use identity_stronghold::StrongholdStorage;
+use iota_sdk::client::Client;
+use tonic::transport::server::RoutesBuilder;
+
+pub fn init_services(routes: &mut RoutesBuilder, client: &Client, stronghold: &StrongholdStorage) {
+ routes.add_service(revocation::service(client));
+ routes.add_service(jwt::service(client, stronghold));
+ routes.add_service(validation::service(client));
+}
diff --git a/bindings/grpc/src/services/credential/revocation.rs b/bindings/grpc/src/services/credential/revocation.rs
new file mode 100644
index 0000000000..d637bce22e
--- /dev/null
+++ b/bindings/grpc/src/services/credential/revocation.rs
@@ -0,0 +1,161 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+use credential_verification::credential_revocation_server::CredentialRevocation;
+use credential_verification::credential_revocation_server::CredentialRevocationServer;
+use credential_verification::RevocationCheckRequest;
+use credential_verification::RevocationCheckResponse;
+use credential_verification::RevocationStatus;
+use identity_iota::credential::JwtCredentialValidatorUtils;
+use identity_iota::credential::JwtValidationError;
+use identity_iota::credential::RevocationBitmapStatus;
+use identity_iota::credential::{self};
+use identity_iota::prelude::IotaDocument;
+use identity_iota::prelude::Resolver;
+use iota_sdk::client::Client;
+use prost::bytes::Bytes;
+use serde::Deserialize;
+use serde::Serialize;
+
+use thiserror::Error;
+use tonic::Request;
+use tonic::Response;
+use tonic::{self};
+
+mod credential_verification {
+ use super::RevocationCheckError;
+ use identity_iota::credential::RevocationBitmapStatus;
+ use identity_iota::credential::Status;
+
+ tonic::include_proto!("credentials");
+
+ impl TryFrom for Status {
+ type Error = RevocationCheckError;
+ fn try_from(req: RevocationCheckRequest) -> Result {
+ use identity_iota::core::Object;
+ use identity_iota::core::Url;
+
+ if req.r#type.as_str() != RevocationBitmapStatus::TYPE {
+ Err(Self::Error::UnknownRevocationType(req.r#type))
+ } else {
+ let parsed_url = req
+ .url
+ .parse::()
+ .map_err(|_| Self::Error::InvalidRevocationUrl(req.url))?;
+ let properties = req
+ .properties
+ .into_iter()
+ .map(|(k, v)| serde_json::to_value(v).map(|v| (k, v)))
+ .collect::>()
+ .map_err(|_| Self::Error::MalformedPropertiesObject)?;
+
+ Ok(Status {
+ id: parsed_url,
+ type_: req.r#type,
+ properties,
+ })
+ }
+ }
+ }
+}
+
+#[derive(Debug, Error, Serialize, Deserialize)]
+#[serde(tag = "error_type", content = "reason")]
+#[serde(rename_all = "snake_case")]
+pub enum RevocationCheckError {
+ #[error("Unknown revocation type {0}")]
+ UnknownRevocationType(String),
+ #[error("Could not parse {0} into a valid URL")]
+ InvalidRevocationUrl(String),
+ #[error("Properties isn't a valid JSON object")]
+ MalformedPropertiesObject,
+ #[error("Invalid credential status: {0}")]
+ InvalidCredentialStatus(String),
+ #[error("Issuer's DID resolution error: {0}")]
+ ResolutionError(String),
+ #[error("Revocation map not found")]
+ RevocationMapNotFound,
+}
+
+impl From for tonic::Status {
+ fn from(e: RevocationCheckError) -> Self {
+ let message = e.to_string();
+ let code = match &e {
+ RevocationCheckError::InvalidCredentialStatus(_)
+ | RevocationCheckError::MalformedPropertiesObject
+ | RevocationCheckError::UnknownRevocationType(_)
+ | RevocationCheckError::InvalidRevocationUrl(_) => tonic::Code::InvalidArgument,
+ RevocationCheckError::ResolutionError(_) => tonic::Code::Internal,
+ RevocationCheckError::RevocationMapNotFound => tonic::Code::NotFound,
+ };
+ let error_json = serde_json::to_vec(&e).unwrap_or_default();
+
+ tonic::Status::with_details(code, message, Bytes::from(error_json))
+ }
+}
+
+impl TryFrom for RevocationCheckError {
+ type Error = ();
+ fn try_from(value: tonic::Status) -> Result {
+ serde_json::from_slice(value.details()).map_err(|_| ())
+ }
+}
+
+#[derive(Debug)]
+pub struct CredentialVerifier {
+ resolver: Resolver,
+}
+
+impl CredentialVerifier {
+ pub fn new(client: &Client) -> Self {
+ let mut resolver = Resolver::new();
+ resolver.attach_iota_handler(client.clone());
+ Self { resolver }
+ }
+}
+
+#[tonic::async_trait]
+impl CredentialRevocation for CredentialVerifier {
+ #[tracing::instrument(
+ name = "credential_check",
+ skip_all,
+ fields(request = ?req.get_ref())
+ ret,
+ err,
+ )]
+ async fn check(
+ &self,
+ req: Request,
+ ) -> Result, tonic::Status> {
+ let credential_revocation_status = {
+ let revocation_status = credential::Status::try_from(req.into_inner())?;
+ RevocationBitmapStatus::try_from(revocation_status)
+ .map_err(|e| RevocationCheckError::InvalidCredentialStatus(e.to_string()))?
+ };
+ let issuer_did = credential_revocation_status.id().unwrap(); // Safety: already parsed as a valid URL
+ let issuer_doc = self
+ .resolver
+ .resolve(issuer_did.did())
+ .await
+ .map_err(|e| RevocationCheckError::ResolutionError(e.to_string()))?;
+
+ if let Err(e) =
+ JwtCredentialValidatorUtils::check_revocation_bitmap_status(&issuer_doc, credential_revocation_status)
+ {
+ match &e {
+ JwtValidationError::Revoked => Ok(Response::new(RevocationCheckResponse {
+ status: RevocationStatus::Revoked.into(),
+ })),
+ _ => Err(RevocationCheckError::RevocationMapNotFound.into()),
+ }
+ } else {
+ Ok(Response::new(RevocationCheckResponse {
+ status: RevocationStatus::Valid.into(),
+ }))
+ }
+ }
+}
+
+pub fn service(client: &Client) -> CredentialRevocationServer {
+ CredentialRevocationServer::new(CredentialVerifier::new(client))
+}
diff --git a/bindings/grpc/src/services/credential/validation.rs b/bindings/grpc/src/services/credential/validation.rs
new file mode 100644
index 0000000000..fb218b727b
--- /dev/null
+++ b/bindings/grpc/src/services/credential/validation.rs
@@ -0,0 +1,135 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+use identity_eddsa_verifier::EdDSAJwsVerifier;
+use identity_iota::core::FromJson;
+use identity_iota::core::Object;
+use identity_iota::core::ToJson;
+use identity_iota::credential::status_list_2021::StatusList2021Credential;
+use identity_iota::credential::FailFast;
+use identity_iota::credential::Jwt;
+use identity_iota::credential::JwtCredentialValidationOptions;
+use identity_iota::credential::JwtCredentialValidator;
+use identity_iota::credential::JwtCredentialValidatorUtils;
+use identity_iota::credential::JwtValidationError;
+use identity_iota::credential::StatusCheck;
+use identity_iota::iota::IotaDID;
+use identity_iota::resolver;
+use identity_iota::resolver::Resolver;
+use iota_sdk::client::Client;
+
+use _credentials::vc_validation_server::VcValidation;
+use _credentials::vc_validation_server::VcValidationServer;
+use _credentials::VcValidationRequest;
+use _credentials::VcValidationResponse;
+use tonic::Code;
+use tonic::Request;
+use tonic::Response;
+use tonic::Status;
+
+mod _credentials {
+ tonic::include_proto!("credentials");
+}
+
+#[derive(Debug, thiserror::Error)]
+pub enum VcValidationError {
+ #[error(transparent)]
+ JwtValidationError(#[from] JwtValidationError),
+ #[error("DID resolution error")]
+ DidResolutionError(#[source] resolver::Error),
+ #[error("Provided an invalid StatusList2021Credential")]
+ InvalidStatusList2021Credential(#[source] identity_iota::core::Error),
+ #[error("The provided credential has been revoked")]
+ RevokedCredential,
+ #[error("The provided credential has expired")]
+ ExpiredCredential,
+ #[error("The provided credential has been suspended")]
+ SuspendedCredential,
+}
+
+impl From for Status {
+ fn from(error: VcValidationError) -> Self {
+ let code = match &error {
+ VcValidationError::InvalidStatusList2021Credential(_) => Code::InvalidArgument,
+ _ => Code::Internal,
+ };
+
+ Status::new(code, error.to_string())
+ }
+}
+
+pub struct VcValidator {
+ resolver: Resolver,
+}
+
+impl VcValidator {
+ pub fn new(client: &Client) -> Self {
+ let mut resolver = Resolver::new();
+ resolver.attach_iota_handler(client.clone());
+ Self { resolver }
+ }
+}
+
+#[tonic::async_trait]
+impl VcValidation for VcValidator {
+ #[tracing::instrument(
+ name = "validate_jwt_credential",
+ skip_all,
+ fields(request = ?req.get_ref())
+ ret,
+ err,
+)]
+ async fn validate(&self, req: Request) -> Result, Status> {
+ let VcValidationRequest {
+ credential_jwt,
+ status_list_credential_json,
+ } = req.into_inner();
+ let jwt = Jwt::new(credential_jwt);
+ let issuer_did = JwtCredentialValidatorUtils::extract_issuer_from_jwt::(&jwt)
+ .map_err(VcValidationError::JwtValidationError)?;
+ let issuer_doc = self
+ .resolver
+ .resolve(&issuer_did)
+ .await
+ .map_err(VcValidationError::DidResolutionError)?;
+
+ let mut validation_option = JwtCredentialValidationOptions::default();
+ if status_list_credential_json.is_some() {
+ validation_option = validation_option.status_check(StatusCheck::SkipAll);
+ }
+
+ let validator = JwtCredentialValidator::with_signature_verifier(EdDSAJwsVerifier::default());
+ let decoded_credential = validator
+ .validate::<_, Object>(&jwt, &issuer_doc, &validation_option, FailFast::FirstError)
+ .map_err(|mut e| match e.validation_errors.swap_remove(0) {
+ JwtValidationError::Revoked => VcValidationError::RevokedCredential,
+ JwtValidationError::ExpirationDate | JwtValidationError::IssuanceDate => VcValidationError::ExpiredCredential,
+ e => VcValidationError::JwtValidationError(e),
+ })?;
+
+ if let Some(status_list_json) = status_list_credential_json {
+ let status_list = StatusList2021Credential::from_json(&status_list_json)
+ .map_err(VcValidationError::InvalidStatusList2021Credential)?;
+ JwtCredentialValidatorUtils::check_status_with_status_list_2021(
+ &decoded_credential.credential,
+ &status_list,
+ StatusCheck::Strict,
+ )
+ .map_err(|e| match e {
+ JwtValidationError::Revoked => VcValidationError::RevokedCredential,
+ JwtValidationError::Suspended => VcValidationError::SuspendedCredential,
+ e => VcValidationError::JwtValidationError(e),
+ })?;
+ }
+
+ let response = Response::new(VcValidationResponse {
+ credential_json: decoded_credential.credential.to_json().unwrap(),
+ });
+
+ Ok(response)
+ }
+}
+
+pub fn service(client: &Client) -> VcValidationServer {
+ VcValidationServer::new(VcValidator::new(client))
+}
diff --git a/bindings/grpc/src/services/document.rs b/bindings/grpc/src/services/document.rs
new file mode 100644
index 0000000000..0ed1298637
--- /dev/null
+++ b/bindings/grpc/src/services/document.rs
@@ -0,0 +1,115 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+use _document::document_service_server::DocumentService;
+use _document::document_service_server::DocumentServiceServer;
+use _document::CreateDidRequest;
+use _document::CreateDidResponse;
+use identity_iota::core::ToJson;
+use identity_iota::iota::IotaClientExt;
+use identity_iota::iota::IotaDocument;
+use identity_iota::iota::IotaIdentityClientExt;
+use identity_iota::storage::JwkDocumentExt;
+use identity_iota::storage::JwkStorageDocumentError;
+use identity_iota::storage::Storage;
+use identity_iota::verification::jws::JwsAlgorithm;
+use identity_iota::verification::MethodScope;
+use identity_stronghold::StrongholdStorage;
+use identity_stronghold::ED25519_KEY_TYPE;
+use iota_sdk::client::Client;
+use iota_sdk::types::block::address::Address;
+use std::error::Error as _;
+use tonic::Code;
+use tonic::Request;
+use tonic::Response;
+use tonic::Status;
+
+mod _document {
+ tonic::include_proto!("document");
+}
+
+#[derive(thiserror::Error, Debug)]
+pub enum Error {
+ #[error("The provided address is not a valid bech32 encoded address")]
+ InvalidAddress,
+ #[error(transparent)]
+ IotaClientError(identity_iota::iota::Error),
+ #[error(transparent)]
+ StorageError(JwkStorageDocumentError),
+}
+
+impl From for Status {
+ fn from(value: Error) -> Self {
+ let code = match &value {
+ Error::InvalidAddress => Code::InvalidArgument,
+ _ => Code::Internal,
+ };
+ Status::new(code, value.to_string())
+ }
+}
+
+pub struct DocumentSvc {
+ storage: Storage,
+ client: Client,
+}
+
+impl DocumentSvc {
+ pub fn new(client: &Client, stronghold: &StrongholdStorage) -> Self {
+ Self {
+ storage: Storage::new(stronghold.clone(), stronghold.clone()),
+ client: client.clone(),
+ }
+ }
+}
+
+#[tonic::async_trait]
+impl DocumentService for DocumentSvc {
+ #[tracing::instrument(
+ name = "create_did_document",
+ skip_all,
+ fields(request = ?req.get_ref())
+ ret,
+ err,
+ )]
+ async fn create(&self, req: Request) -> Result, Status> {
+ let CreateDidRequest { bech32_address } = req.into_inner();
+ let address = Address::try_from_bech32(&bech32_address).map_err(|_| Error::InvalidAddress)?;
+ let network_name = self.client.network_name().await.map_err(Error::IotaClientError)?;
+
+ let mut document = IotaDocument::new(&network_name);
+ let fragment = document
+ .generate_method(
+ &self.storage,
+ ED25519_KEY_TYPE.clone(),
+ JwsAlgorithm::EdDSA,
+ None,
+ MethodScope::VerificationMethod,
+ )
+ .await
+ .map_err(Error::StorageError)?;
+
+ let alias_output = self
+ .client
+ .new_did_output(address, document, None)
+ .await
+ .map_err(Error::IotaClientError)?;
+
+ let document = self
+ .client
+ .publish_did_output(self.storage.key_storage().as_secret_manager(), alias_output)
+ .await
+ .map_err(Error::IotaClientError)
+ .inspect_err(|e| tracing::error!("{:?}", e.source()))?;
+ let did = document.id();
+
+ Ok(Response::new(CreateDidResponse {
+ document_json: document.to_json().unwrap(),
+ fragment,
+ did: did.to_string(),
+ }))
+ }
+}
+
+pub fn service(client: &Client, stronghold: &StrongholdStorage) -> DocumentServiceServer {
+ DocumentServiceServer::new(DocumentSvc::new(client, stronghold))
+}
diff --git a/bindings/grpc/src/services/domain_linkage.rs b/bindings/grpc/src/services/domain_linkage.rs
new file mode 100644
index 0000000000..3c3935a413
--- /dev/null
+++ b/bindings/grpc/src/services/domain_linkage.rs
@@ -0,0 +1,377 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+use std::collections::HashMap;
+use std::error::Error;
+
+use domain_linkage::domain_linkage_server::DomainLinkage;
+use domain_linkage::domain_linkage_server::DomainLinkageServer;
+use domain_linkage::LinkedDidEndpointValidationStatus;
+use domain_linkage::LinkedDidValidationStatus;
+use domain_linkage::ValidateDidAgainstDidConfigurationsRequest;
+use domain_linkage::ValidateDidRequest;
+use domain_linkage::ValidateDidResponse;
+use domain_linkage::ValidateDomainAgainstDidConfigurationRequest;
+use domain_linkage::ValidateDomainRequest;
+use domain_linkage::ValidateDomainResponse;
+use futures::stream::FuturesOrdered;
+use futures::TryStreamExt;
+use identity_eddsa_verifier::EdDSAJwsVerifier;
+use identity_iota::core::FromJson;
+use identity_iota::core::Url;
+use identity_iota::credential::DomainLinkageConfiguration;
+use identity_iota::credential::JwtCredentialValidationOptions;
+use identity_iota::credential::JwtDomainLinkageValidator;
+use identity_iota::credential::LinkedDomainService;
+use identity_iota::did::CoreDID;
+use identity_iota::iota::IotaDID;
+use identity_iota::iota::IotaDocument;
+use identity_iota::resolver::Resolver;
+use iota_sdk::client::Client;
+use serde::Deserialize;
+use serde::Serialize;
+use thiserror::Error;
+use tonic::Request;
+use tonic::Response;
+use tonic::Status;
+use url::Origin;
+
+#[allow(clippy::module_inception)]
+mod domain_linkage {
+ tonic::include_proto!("domain_linkage");
+}
+
+#[derive(Debug, Error, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+#[serde(tag = "error", content = "reason")]
+enum DomainLinkageError {
+ #[error("domain argument invalid: {0}")]
+ DomainParsing(String),
+ #[error("did configuration argument invalid: {0}")]
+ DidConfigurationParsing(String),
+ #[error("did resolving failed: {0}")]
+ DidResolving(String),
+}
+
+impl From for tonic::Status {
+ fn from(value: DomainLinkageError) -> Self {
+ let code = match &value {
+ DomainLinkageError::DomainParsing(_) => tonic::Code::InvalidArgument,
+ DomainLinkageError::DidConfigurationParsing(_) => tonic::Code::InvalidArgument,
+ DomainLinkageError::DidResolving(_) => tonic::Code::Internal,
+ };
+ let message = value.to_string();
+ let error_json = serde_json::to_vec(&value).expect("plenty of memory!"); // ?
+
+ tonic::Status::with_details(code, message, error_json.into())
+ }
+}
+
+/// Helper struct that allows to convert `ValidateDomainAgainstDidConfigurationRequest` input struct
+/// with `String` config to a struct with `DomainLinkageService` config.
+struct DomainValidationConfig {
+ domain: Url,
+ config: DomainLinkageConfiguration,
+}
+
+impl DomainValidationConfig {
+ /// Parses did-configuration inputs from:
+ ///
+ /// - `validate_domain_against_did_configuration`
+ /// - `validate_did_against_did_configurations`
+ pub fn try_parse(request_config: &ValidateDomainAgainstDidConfigurationRequest) -> Result {
+ Ok(Self {
+ domain: Url::parse(&request_config.domain).map_err(|e| DomainLinkageError::DomainParsing(e.to_string()))?,
+ config: DomainLinkageConfiguration::from_json(&request_config.did_configuration).map_err(|err| {
+ DomainLinkageError::DidConfigurationParsing(format!("could not parse given DID configuration; {}", &err))
+ })?,
+ })
+ }
+}
+
+/// Builds a validation status for a failed validation from an `Error`.
+fn get_validation_failed_status(message: &str, err: &impl Error) -> LinkedDidValidationStatus {
+ LinkedDidValidationStatus {
+ valid: false,
+ document: None,
+ error: Some(format!("{}; {}", message, &err.to_string())),
+ }
+}
+
+#[derive(Debug)]
+pub struct DomainLinkageService {
+ resolver: Resolver,
+}
+
+impl DomainLinkageService {
+ pub fn new(client: &Client) -> Self {
+ let mut resolver = Resolver::new();
+ resolver.attach_iota_handler(client.clone());
+ Self { resolver }
+ }
+
+ /// Validates a DID' `LinkedDomains` service endpoints. Pre-fetched did-configurations can be passed to skip fetching
+ /// them on server.
+ ///
+ /// Arguments:
+ ///
+ /// * `did`: DID to validate
+ /// * `did_configurations`: A list of domains and their did-configuration, if omitted config will be fetched
+ async fn validate_did_with_optional_configurations(
+ &self,
+ did: &IotaDID,
+ did_configurations: Option>,
+ ) -> Result, DomainLinkageError> {
+ // fetch DID document for given DID
+ let did_document = self
+ .resolver
+ .resolve(did)
+ .await
+ .map_err(|e| DomainLinkageError::DidResolving(e.to_string()))?;
+
+ let services: Vec = did_document
+ .service()
+ .iter()
+ .cloned()
+ .filter_map(|service| LinkedDomainService::try_from(service).ok())
+ .collect();
+
+ let config_map: HashMap = match did_configurations {
+ Some(configurations) => configurations
+ .into_iter()
+ .map(|value| (value.domain.origin(), value.config))
+ .collect::>(),
+ None => HashMap::new(),
+ };
+
+ // check validation for all services and endpoints in them
+ let mut service_futures = FuturesOrdered::new();
+ for service in services {
+ let service_id: CoreDID = did.clone().into();
+ let domains: Vec = service.domains().into();
+ let local_config_map = config_map.clone();
+ service_futures.push_back(async move {
+ let mut domain_futures = FuturesOrdered::new();
+ for domain in domains {
+ let config = local_config_map.get(&domain.origin()).map(|value| value.to_owned());
+ domain_futures.push_back(self.validate_domains_with_optional_configuration(
+ domain.clone(),
+ Some(did.clone().into()),
+ config,
+ ));
+ }
+ domain_futures
+ .try_collect::>>()
+ .await
+ .map(|value| LinkedDidEndpointValidationStatus {
+ id: service_id.to_string(),
+ service_endpoint: value.into_iter().flatten().collect(),
+ })
+ });
+ }
+ let endpoint_validation_status = service_futures
+ .try_collect::>()
+ .await?;
+
+ Ok(endpoint_validation_status)
+ }
+
+ /// Validates domain linkage for given origin.
+ ///
+ /// Arguments:
+ ///
+ /// * `domain`: An origin to validate domain linkage for
+ /// * `did`: A DID to restrict validation to, if omitted all DIDs from config will be validated
+ /// * `config`: A domain linkage configuration can be passed if already loaded, if omitted config will be fetched from
+ /// origin
+ async fn validate_domains_with_optional_configuration(
+ &self,
+ domain: Url,
+ did: Option,
+ config: Option,
+ ) -> Result, DomainLinkageError> {
+ // get domain linkage config
+ let domain_linkage_configuration: DomainLinkageConfiguration = if let Some(config_value) = config {
+ config_value
+ } else {
+ match DomainLinkageConfiguration::fetch_configuration(domain.clone()).await {
+ Ok(value) => value,
+ Err(err) => {
+ return Ok(vec![get_validation_failed_status(
+ "could not get domain linkage config",
+ &err,
+ )]);
+ }
+ }
+ };
+
+ // get issuers of `linked_dids` credentials
+ let linked_dids: Vec = if let Some(issuer_did) = did {
+ vec![issuer_did]
+ } else {
+ match domain_linkage_configuration.issuers() {
+ Ok(value) => value,
+ Err(err) => {
+ return Ok(vec![get_validation_failed_status(
+ "could not get issuers from domain linkage config credential",
+ &err,
+ )]);
+ }
+ }
+ };
+
+ // resolve all issuers
+ let resolved = match self.resolver.resolve_multiple(&linked_dids).await {
+ Ok(value) => value,
+ Err(err) => {
+ return Ok(vec![get_validation_failed_status(
+ "could not resolve linked DIDs from domain linkage config",
+ &err,
+ )]);
+ }
+ };
+
+ // check linked DIDs separately
+ let errors: Vec> = resolved
+ .values()
+ .map(|issuer_did_doc| {
+ JwtDomainLinkageValidator::with_signature_verifier(EdDSAJwsVerifier::default())
+ .validate_linkage(
+ &issuer_did_doc,
+ &domain_linkage_configuration,
+ &domain.clone(),
+ &JwtCredentialValidationOptions::default(),
+ )
+ .err()
+ .map(|err| err.to_string())
+ })
+ .collect();
+
+ // collect resolved documents and their validation status into array following the order of `linked_dids`
+ let status_infos = domain_linkage_configuration
+ .linked_dids()
+ .iter()
+ .zip(errors.iter())
+ .map(|(credential, error)| LinkedDidValidationStatus {
+ valid: error.is_none(),
+ document: Some(credential.as_str().to_string()),
+ error: error.clone(),
+ })
+ .collect();
+
+ Ok(status_infos)
+ }
+}
+
+#[tonic::async_trait]
+impl DomainLinkage for DomainLinkageService {
+ #[tracing::instrument(
+ name = "validate_domain",
+ skip_all,
+ fields(request = ?req.get_ref())
+ ret,
+ err,
+ )]
+ async fn validate_domain(
+ &self,
+ req: Request,
+ ) -> Result, Status> {
+ let request_data = &req.into_inner();
+ // parse given domain
+ let domain: Url =
+ Url::parse(&request_data.domain).map_err(|err| DomainLinkageError::DomainParsing(err.to_string()))?;
+
+ // get validation status for all issuer dids
+ let status_infos = self
+ .validate_domains_with_optional_configuration(domain, None, None)
+ .await?;
+
+ Ok(Response::new(ValidateDomainResponse {
+ linked_dids: status_infos,
+ }))
+ }
+
+ #[tracing::instrument(
+ name = "validate_domain_against_did_configuration",
+ skip_all,
+ fields(request = ?req.get_ref())
+ ret,
+ err,
+ )]
+ async fn validate_domain_against_did_configuration(
+ &self,
+ req: Request,
+ ) -> Result, Status> {
+ let request_data = &req.into_inner();
+ // parse given domain
+ let domain: Url =
+ Url::parse(&request_data.domain).map_err(|err| DomainLinkageError::DomainParsing(err.to_string()))?;
+ // parse config
+ let config = DomainLinkageConfiguration::from_json(&request_data.did_configuration.to_string()).map_err(|err| {
+ DomainLinkageError::DidConfigurationParsing(format!("could not parse given DID configuration; {}", &err))
+ })?;
+
+ // get validation status for all issuer dids
+ let status_infos = self
+ .validate_domains_with_optional_configuration(domain, None, Some(config))
+ .await?;
+
+ Ok(Response::new(ValidateDomainResponse {
+ linked_dids: status_infos,
+ }))
+ }
+
+ #[tracing::instrument(
+ name = "validate_did",
+ skip_all,
+ fields(request = ?req.get_ref())
+ ret,
+ err,
+ )]
+ async fn validate_did(&self, req: Request) -> Result, Status> {
+ // fetch DID document for given DID
+ let did: IotaDID = IotaDID::parse(req.into_inner().did).map_err(|e| Status::internal(e.to_string()))?;
+
+ let endpoint_validation_status = self.validate_did_with_optional_configurations(&did, None).await?;
+
+ let response = ValidateDidResponse {
+ service: endpoint_validation_status,
+ };
+
+ Ok(Response::new(response))
+ }
+
+ #[tracing::instrument(
+ name = "validate_did_against_did_configurations",
+ skip_all,
+ fields(request = ?req.get_ref())
+ ret,
+ err,
+ )]
+ async fn validate_did_against_did_configurations(
+ &self,
+ req: Request,
+ ) -> Result, Status> {
+ let request_data = &req.into_inner();
+ let did: IotaDID = IotaDID::parse(&request_data.did).map_err(|e| Status::internal(e.to_string()))?;
+ let did_configurations = request_data
+ .did_configurations
+ .iter()
+ .map(DomainValidationConfig::try_parse)
+ .collect::, DomainLinkageError>>()?;
+
+ let endpoint_validation_status = self
+ .validate_did_with_optional_configurations(&did, Some(did_configurations))
+ .await?;
+
+ let response = ValidateDidResponse {
+ service: endpoint_validation_status,
+ };
+
+ Ok(Response::new(response))
+ }
+}
+
+pub fn service(client: &Client) -> DomainLinkageServer {
+ DomainLinkageServer::new(DomainLinkageService::new(client))
+}
diff --git a/bindings/grpc/src/services/health_check.rs b/bindings/grpc/src/services/health_check.rs
new file mode 100644
index 0000000000..27cf808c4f
--- /dev/null
+++ b/bindings/grpc/src/services/health_check.rs
@@ -0,0 +1,36 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+use health_check::health_check_server::HealthCheck;
+use health_check::health_check_server::HealthCheckServer;
+use health_check::HealthCheckRequest;
+use health_check::HealthCheckResponse;
+use tonic::Request;
+use tonic::Response;
+use tonic::Status;
+
+#[allow(clippy::module_inception)]
+mod health_check {
+ tonic::include_proto!("health_check");
+}
+
+#[derive(Debug, Default)]
+pub struct HealthChecker {}
+
+#[tonic::async_trait]
+impl HealthCheck for HealthChecker {
+ #[tracing::instrument(
+ name = "health_check",
+ skip_all,
+ fields(request = ?_req.get_ref())
+ ret,
+ err,
+ )]
+ async fn check(&self, _req: Request) -> Result, Status> {
+ Ok(Response::new(HealthCheckResponse { status: "OK".into() }))
+ }
+}
+
+pub fn service() -> HealthCheckServer {
+ HealthCheckServer::new(HealthChecker::default())
+}
diff --git a/bindings/grpc/src/services/mod.rs b/bindings/grpc/src/services/mod.rs
new file mode 100644
index 0000000000..00abe17ce1
--- /dev/null
+++ b/bindings/grpc/src/services/mod.rs
@@ -0,0 +1,28 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+pub mod credential;
+pub mod document;
+pub mod domain_linkage;
+pub mod health_check;
+pub mod sd_jwt;
+pub mod status_list_2021;
+pub mod utils;
+
+use identity_stronghold::StrongholdStorage;
+use iota_sdk::client::Client;
+use tonic::transport::server::Routes;
+use tonic::transport::server::RoutesBuilder;
+
+pub fn routes(client: &Client, stronghold: &StrongholdStorage) -> Routes {
+ let mut routes = RoutesBuilder::default();
+ routes.add_service(health_check::service());
+ credential::init_services(&mut routes, client, stronghold);
+ routes.add_service(sd_jwt::service(client));
+ routes.add_service(domain_linkage::service(client));
+ routes.add_service(document::service(client, stronghold));
+ routes.add_service(status_list_2021::service());
+ routes.add_service(utils::service(stronghold));
+
+ routes.routes()
+}
diff --git a/bindings/grpc/src/services/sd_jwt.rs b/bindings/grpc/src/services/sd_jwt.rs
new file mode 100644
index 0000000000..af792e51f6
--- /dev/null
+++ b/bindings/grpc/src/services/sd_jwt.rs
@@ -0,0 +1,164 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+use _sd_jwt::verification_server::Verification;
+use _sd_jwt::verification_server::VerificationServer;
+use _sd_jwt::VerificationRequest;
+use _sd_jwt::VerificationResponse;
+use identity_eddsa_verifier::EdDSAJwsVerifier;
+use identity_iota::core::Object;
+use identity_iota::core::Timestamp;
+use identity_iota::core::ToJson;
+use identity_iota::credential::FailFast;
+use identity_iota::credential::Jwt;
+use identity_iota::credential::JwtCredentialValidationOptions;
+use identity_iota::credential::JwtCredentialValidatorUtils;
+use identity_iota::credential::KeyBindingJWTValidationOptions;
+use identity_iota::credential::SdJwtCredentialValidator;
+use identity_iota::iota::IotaDID;
+use identity_iota::iota::IotaDocument;
+use identity_iota::resolver::Resolver;
+use identity_iota::sd_jwt_payload::SdJwt;
+use identity_iota::sd_jwt_payload::SdObjectDecoder;
+use iota_sdk::client::Client;
+use serde::Deserialize;
+use serde::Serialize;
+use thiserror::Error;
+
+use self::_sd_jwt::KeyBindingOptions;
+
+mod _sd_jwt {
+ tonic::include_proto!("sd_jwt");
+}
+
+impl From for KeyBindingJWTValidationOptions {
+ fn from(value: KeyBindingOptions) -> Self {
+ let mut kb_options = Self::default();
+ kb_options.nonce = value.nonce;
+ kb_options.aud = value.aud;
+ kb_options.earliest_issuance_date = value
+ .earliest_issuance_date
+ .and_then(|t| Timestamp::parse(t.as_str()).ok());
+ kb_options.latest_issuance_date = value
+ .latest_issuance_date
+ .and_then(|t| Timestamp::parse(t.as_str()).ok());
+
+ kb_options
+ }
+}
+
+#[derive(Debug, Error, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+#[serde(tag = "error", content = "reason")]
+enum SdJwtVerificationError {
+ #[error("Failed to parse SD-JWT: {0}")]
+ DeserializationError(String),
+ #[error("Failed to parse JWT: {0}")]
+ JwtError(String),
+ #[error("Credential verification failed: {0}")]
+ VerificationError(String),
+ #[error("Failed to resolve DID Document: {0}")]
+ DidResolutionError(String),
+ #[error("Missing \"kb_options\".")]
+ MissingKbOptions,
+ #[error("{0}")]
+ KeyBindingJwtError(String),
+ #[error("Provided an invalid holder's id.")]
+ InvalidHolderDid,
+}
+
+impl From for tonic::Status {
+ fn from(value: SdJwtVerificationError) -> Self {
+ let code = match &value {
+ SdJwtVerificationError::DeserializationError(_) => tonic::Code::InvalidArgument,
+ SdJwtVerificationError::JwtError(_) => tonic::Code::InvalidArgument,
+ SdJwtVerificationError::VerificationError(_) => tonic::Code::InvalidArgument,
+ SdJwtVerificationError::DidResolutionError(_) => tonic::Code::NotFound,
+ SdJwtVerificationError::MissingKbOptions => tonic::Code::InvalidArgument,
+ SdJwtVerificationError::KeyBindingJwtError(_) => tonic::Code::Internal,
+ SdJwtVerificationError::InvalidHolderDid => tonic::Code::InvalidArgument,
+ };
+ let message = value.to_string();
+ let error_json = serde_json::to_vec(&value).expect("plenty of memory!");
+
+ tonic::Status::with_details(code, message, error_json.into())
+ }
+}
+
+#[derive(Debug)]
+pub struct SdJwtService {
+ resolver: Resolver,
+}
+
+impl SdJwtService {
+ pub fn new(client: &Client) -> Self {
+ let mut resolver = Resolver::new();
+ resolver.attach_iota_handler(client.clone());
+ Self { resolver }
+ }
+}
+
+#[tonic::async_trait]
+impl Verification for SdJwtService {
+ #[tracing::instrument(
+ name = "sd_jwt_verification",
+ skip_all,
+ fields(request = ?request.get_ref())
+ ret,
+ err,
+ )]
+ async fn verify(
+ &self,
+ request: tonic::Request,
+ ) -> Result, tonic::Status> {
+ let VerificationRequest { jwt, kb_options } = request.into_inner();
+ let mut sd_jwt = SdJwt::parse(&jwt).map_err(|e| SdJwtVerificationError::DeserializationError(e.to_string()))?;
+ let jwt = Jwt::new(sd_jwt.jwt);
+
+ let issuer_did = JwtCredentialValidatorUtils::extract_issuer_from_jwt::(&jwt)
+ .map_err(|e| SdJwtVerificationError::VerificationError(e.to_string()))?;
+ let issuer_document = self
+ .resolver
+ .resolve(&issuer_did)
+ .await
+ .map_err(|e| SdJwtVerificationError::DidResolutionError(e.to_string()))?;
+ sd_jwt.jwt = jwt.into();
+
+ let decoder = SdObjectDecoder::new_with_sha256();
+ let validator = SdJwtCredentialValidator::with_signature_verifier(EdDSAJwsVerifier::default(), decoder);
+ let credential = validator
+ .validate_credential::<_, Object>(
+ &sd_jwt,
+ &issuer_document,
+ &JwtCredentialValidationOptions::default(),
+ FailFast::FirstError,
+ )
+ .map_err(|e| SdJwtVerificationError::VerificationError(e.to_string()))?;
+
+ if sd_jwt.key_binding_jwt.is_some() {
+ let Some(kb_options) = kb_options else {
+ return Err(SdJwtVerificationError::MissingKbOptions.into());
+ };
+ let holder = {
+ let did =
+ IotaDID::parse(kb_options.holder_did.as_str()).map_err(|_| SdJwtVerificationError::InvalidHolderDid)?;
+ self
+ .resolver
+ .resolve(&did)
+ .await
+ .map_err(|e| SdJwtVerificationError::DidResolutionError(e.to_string()))?
+ };
+ let _ = validator
+ .validate_key_binding_jwt(&sd_jwt, &holder, &kb_options.into())
+ .map_err(|e| SdJwtVerificationError::KeyBindingJwtError(e.to_string()))?;
+ }
+
+ Ok(tonic::Response::new(VerificationResponse {
+ credential: credential.credential.to_json().unwrap(),
+ }))
+ }
+}
+
+pub fn service(client: &Client) -> VerificationServer {
+ VerificationServer::new(SdJwtService::new(client))
+}
diff --git a/bindings/grpc/src/services/status_list_2021.rs b/bindings/grpc/src/services/status_list_2021.rs
new file mode 100644
index 0000000000..be0595c9a4
--- /dev/null
+++ b/bindings/grpc/src/services/status_list_2021.rs
@@ -0,0 +1,170 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+use std::collections::HashSet;
+
+use identity_iota::core::Context;
+use identity_iota::core::FromJson;
+use identity_iota::core::Timestamp;
+use identity_iota::core::ToJson;
+use identity_iota::core::Url;
+use identity_iota::credential::status_list_2021::StatusList2021;
+use identity_iota::credential::status_list_2021::StatusList2021Credential;
+use identity_iota::credential::status_list_2021::StatusList2021CredentialBuilder;
+use identity_iota::credential::status_list_2021::StatusList2021CredentialError;
+use identity_iota::credential::status_list_2021::StatusPurpose;
+use identity_iota::credential::Issuer;
+use identity_iota::credential::{self};
+
+use _status_list_2021::status_list2021_svc_server::StatusList2021Svc;
+use _status_list_2021::status_list2021_svc_server::StatusList2021SvcServer;
+use _status_list_2021::CreateRequest;
+use _status_list_2021::Purpose;
+use _status_list_2021::StatusListCredential;
+use _status_list_2021::UpdateRequest;
+use tonic::Code;
+use tonic::Request;
+use tonic::Response;
+use tonic::Status;
+
+mod _status_list_2021 {
+ use identity_iota::credential::status_list_2021::StatusPurpose;
+
+ tonic::include_proto!("status_list_2021");
+
+ impl From for StatusPurpose {
+ fn from(value: Purpose) -> Self {
+ match value {
+ Purpose::Revocation => StatusPurpose::Revocation,
+ Purpose::Suspension => StatusPurpose::Suspension,
+ }
+ }
+ }
+}
+
+#[derive(Debug, thiserror::Error)]
+pub enum Error {
+ #[error("A valid status list must have at least 16KB entries")]
+ InvalidStatusListLength,
+ #[error("\"{0}\" is not a valid context")]
+ InvalidContext(String),
+ #[error("\"{0}\" is not a valid issuer")]
+ InvalidIssuer(String),
+ #[error("\"{0}\" is not a valid timestamp")]
+ InvalidTimestamp(String),
+ #[error("\"{0}\" is not a valid id")]
+ InvalidId(String),
+ #[error("Failed to deserialize into a valid StatusList2021Credential")]
+ CredentialDeserializationError(#[source] identity_iota::core::Error),
+ #[error(transparent)]
+ CredentialError(#[from] credential::Error),
+ #[error(transparent)]
+ StatusListError(StatusList2021CredentialError),
+}
+
+impl From for Status {
+ fn from(value: Error) -> Self {
+ let code = match &value {
+ Error::InvalidStatusListLength
+ | Error::InvalidContext(_)
+ | Error::InvalidIssuer(_)
+ | Error::InvalidTimestamp(_) => Code::InvalidArgument,
+ _ => Code::Internal,
+ };
+
+ Status::new(code, value.to_string())
+ }
+}
+
+pub struct StatusList2021Service;
+
+#[tonic::async_trait]
+impl StatusList2021Svc for StatusList2021Service {
+ #[tracing::instrument(
+ name = "create_status_list_credential",
+ skip_all,
+ fields(request = ?req.get_ref())
+ ret,
+ err,
+)]
+ async fn create(&self, req: Request) -> Result, Status> {
+ let CreateRequest {
+ purpose,
+ length,
+ id,
+ expiration_date,
+ contexts,
+ types,
+ issuer,
+ } = req.into_inner();
+ let status_list = length
+ .map(|entries| StatusList2021::new(entries as usize))
+ .unwrap_or(Ok(StatusList2021::default()))
+ .map_err(|_| Error::InvalidStatusListLength)?;
+
+ let mut builder = StatusList2021CredentialBuilder::new(status_list);
+ let contexts = contexts.into_iter().collect::>();
+ for ctx in contexts {
+ let url = Url::parse(&ctx).map_err(move |_| Error::InvalidContext(ctx))?;
+ builder = builder.context(Context::Url(url));
+ }
+
+ let types = types.into_iter().collect::>();
+ for t in types {
+ builder = builder.add_type(t);
+ }
+ let issuer = Url::parse(&issuer)
+ .map_err(move |_| Error::InvalidIssuer(issuer))
+ .map(Issuer::Url)?;
+ builder = builder.issuer(issuer);
+ builder = builder.purpose(StatusPurpose::from(Purpose::try_from(purpose).unwrap()));
+ if let Some(exp) = expiration_date {
+ let exp = Timestamp::parse(&exp).map_err(move |_| Error::InvalidTimestamp(exp))?;
+ builder = builder.expiration_date(exp);
+ }
+ if let Some(id) = id {
+ let id = Url::parse(&id).map_err(move |_| Error::InvalidId(id))?;
+ builder = builder.subject_id(id);
+ }
+ let status_list_credential = builder.build().map_err(Error::CredentialError)?;
+ let res = StatusListCredential {
+ credential_json: status_list_credential.to_json().unwrap(),
+ };
+
+ Ok(Response::new(res))
+ }
+
+ #[tracing::instrument(
+ name = "update_status_list_credential",
+ skip_all,
+ fields(request = ?req.get_ref())
+ ret,
+ err,
+ )]
+ async fn update(&self, req: Request) -> Result, Status> {
+ let UpdateRequest {
+ credential_json,
+ entries,
+ } = req.into_inner();
+ let mut status_list_credential =
+ StatusList2021Credential::from_json(&credential_json).map_err(Error::CredentialDeserializationError)?;
+
+ status_list_credential
+ .update(move |status_list| {
+ for (idx, value) in entries {
+ status_list.set_entry(idx as usize, value)?
+ }
+
+ Ok(())
+ })
+ .map_err(Error::StatusListError)?;
+
+ Ok(Response::new(StatusListCredential {
+ credential_json: status_list_credential.to_json().unwrap(),
+ }))
+ }
+}
+
+pub fn service() -> StatusList2021SvcServer {
+ StatusList2021SvcServer::new(StatusList2021Service)
+}
diff --git a/bindings/grpc/src/services/utils.rs b/bindings/grpc/src/services/utils.rs
new file mode 100644
index 0000000000..0e7d2fc570
--- /dev/null
+++ b/bindings/grpc/src/services/utils.rs
@@ -0,0 +1,67 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+use _utils::signing_server::Signing as SigningSvc;
+use _utils::signing_server::SigningServer;
+use _utils::DataSigningRequest;
+use _utils::DataSigningResponse;
+use identity_iota::storage::JwkStorage;
+use identity_iota::storage::KeyId;
+use identity_iota::storage::KeyStorageError;
+use identity_stronghold::StrongholdStorage;
+use tonic::Request;
+use tonic::Response;
+use tonic::Status;
+
+mod _utils {
+ tonic::include_proto!("utils");
+}
+
+#[derive(Debug, thiserror::Error)]
+#[error("Key storage error: {0}")]
+pub struct Error(#[from] KeyStorageError);
+
+impl From for Status {
+ fn from(value: Error) -> Self {
+ Status::internal(value.to_string())
+ }
+}
+
+pub struct SigningService {
+ storage: StrongholdStorage,
+}
+
+impl SigningService {
+ pub fn new(stronghold: &StrongholdStorage) -> Self {
+ Self {
+ storage: stronghold.clone(),
+ }
+ }
+}
+
+#[tonic::async_trait]
+impl SigningSvc for SigningService {
+ #[tracing::instrument(
+ name = "utils/sign",
+ skip_all,
+ fields(request = ?req.get_ref())
+ ret,
+ err,
+ )]
+ async fn sign(&self, req: Request) -> Result, Status> {
+ let DataSigningRequest { data, key_id } = req.into_inner();
+ let key_id = KeyId::new(key_id);
+ let public_key_jwk = self.storage.get_public_key(&key_id).await.map_err(Error)?;
+ let signature = self
+ .storage
+ .sign(&key_id, &data, &public_key_jwk)
+ .await
+ .map_err(Error)?;
+
+ Ok(Response::new(DataSigningResponse { signature }))
+ }
+}
+
+pub fn service(stronghold: &StrongholdStorage) -> SigningServer {
+ SigningServer::new(SigningService::new(stronghold))
+}
diff --git a/bindings/grpc/tests/api/credential_revocation_check.rs b/bindings/grpc/tests/api/credential_revocation_check.rs
new file mode 100644
index 0000000000..9e92197c72
--- /dev/null
+++ b/bindings/grpc/tests/api/credential_revocation_check.rs
@@ -0,0 +1,99 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+use credentials::credential_revocation_client::CredentialRevocationClient;
+use credentials::RevocationStatus;
+use identity_iota::credential::RevocationBitmap;
+use identity_iota::credential::RevocationBitmapStatus;
+use identity_iota::credential::{self};
+use identity_iota::did::DID;
+use serde_json::json;
+
+use crate::credential_revocation_check::credentials::RevocationCheckRequest;
+use crate::helpers::Entity;
+use crate::helpers::TestServer;
+
+mod credentials {
+ tonic::include_proto!("credentials");
+}
+
+#[tokio::test]
+async fn checking_status_of_credential_works() -> anyhow::Result<()> {
+ let server = TestServer::new().await;
+ let client = server.client();
+ let mut issuer = Entity::new();
+ issuer.create_did(client).await?;
+
+ let mut subject = Entity::new();
+ subject.create_did(client).await?;
+
+ let service_id = issuer
+ .document()
+ .unwrap() // Safety: `create_did` didn't fail
+ .id()
+ .to_url()
+ .join("#my-revocation-service")?;
+
+ // Add a revocation service to the issuer's DID document
+ issuer
+ .update_document(client, |mut doc| {
+ let service = RevocationBitmap::new().to_service(service_id.clone()).unwrap();
+
+ doc.insert_service(service).ok().map(|_| doc)
+ })
+ .await?;
+
+ let credential_status: credential::Status = RevocationBitmapStatus::new(service_id, 3).into();
+
+ let mut grpc_client = CredentialRevocationClient::connect(server.endpoint()).await?;
+ let req = RevocationCheckRequest {
+ r#type: credential_status.type_,
+ url: credential_status.id.into_string(),
+ properties: credential_status
+ .properties
+ .into_iter()
+ .map(|(k, v)| (k, v.to_string().trim_matches('"').to_owned()))
+ .collect(),
+ };
+ let res = grpc_client.check(tonic::Request::new(req.clone())).await?.into_inner();
+
+ assert_eq!(res.status(), RevocationStatus::Valid);
+
+ // Revoke credential
+ issuer
+ .update_document(&client, |mut doc| {
+ doc.revoke_credentials("my-revocation-service", &[3]).ok().map(|_| doc)
+ })
+ .await?;
+
+ let res = grpc_client.check(tonic::Request::new(req)).await?.into_inner();
+ assert_eq!(res.status(), RevocationStatus::Revoked);
+
+ Ok(())
+}
+
+#[tokio::test]
+async fn checking_status_of_valid_but_unresolvable_url_fails() -> anyhow::Result<()> {
+ use identity_grpc::services::credential::revocation::RevocationCheckError;
+ let server = TestServer::new().await;
+
+ let mut grpc_client = CredentialRevocationClient::connect(server.endpoint()).await?;
+ let properties = json!({
+ "revocationBitmapIndex": "3"
+ });
+ let req = RevocationCheckRequest {
+ r#type: RevocationBitmap::TYPE.to_owned(),
+ url: "did:example:1234567890#my-revocation-service".to_owned(),
+ properties: properties
+ .as_object()
+ .unwrap()
+ .into_iter()
+ .map(|(k, v)| (k.clone(), v.to_string().trim_matches('"').to_owned()))
+ .collect(),
+ };
+ let res_error = grpc_client.check(tonic::Request::new(req.clone())).await;
+
+ assert!(res_error.is_err_and(|e| matches!(e.try_into().unwrap(), RevocationCheckError::ResolutionError(_))));
+
+ Ok(())
+}
diff --git a/bindings/grpc/tests/api/credential_validation.rs b/bindings/grpc/tests/api/credential_validation.rs
new file mode 100644
index 0000000000..f1bfedf100
--- /dev/null
+++ b/bindings/grpc/tests/api/credential_validation.rs
@@ -0,0 +1,151 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+use _credentials::vc_validation_client::VcValidationClient;
+use _credentials::VcValidationRequest;
+use identity_iota::core::FromJson;
+use identity_iota::core::ToJson;
+use identity_iota::core::Url;
+use identity_iota::credential::status_list_2021::StatusList2021;
+use identity_iota::credential::status_list_2021::StatusList2021CredentialBuilder;
+use identity_iota::credential::status_list_2021::StatusPurpose;
+use identity_iota::credential::Credential;
+use identity_iota::credential::CredentialBuilder;
+use identity_iota::credential::Issuer;
+use identity_iota::credential::Subject;
+use identity_iota::did::DID;
+use identity_storage::JwkDocumentExt;
+use identity_storage::JwsSignatureOptions;
+use identity_stronghold::StrongholdStorage;
+use serde_json::json;
+
+use crate::helpers::make_stronghold;
+use crate::helpers::Entity;
+use crate::helpers::TestServer;
+
+mod _credentials {
+ tonic::include_proto!("credentials");
+}
+
+#[tokio::test]
+async fn credential_validation() -> anyhow::Result<()> {
+ let stronghold = StrongholdStorage::new(make_stronghold());
+ let server = TestServer::new_with_stronghold(stronghold.clone()).await;
+ let api_client = server.client();
+
+ let mut issuer = Entity::new_with_stronghold(stronghold);
+ issuer.create_did(api_client).await?;
+
+ let mut holder = Entity::new();
+ holder.create_did(api_client).await?;
+
+ let subject = Subject::from_json_value(json!({
+ "id": holder.document().unwrap().id().as_str(),
+ "name": "Alice",
+ "degree": {
+ "type": "BachelorDegree",
+ "name": "Bachelor of Science and Arts",
+ },
+ "GPA": "4.0",
+ }))?;
+
+ // Build credential using subject above and issuer.
+ let credential: Credential = CredentialBuilder::default()
+ .id(Url::parse("https://example.edu/credentials/3732")?)
+ .issuer(Url::parse(issuer.document().unwrap().id().as_str())?)
+ .type_("UniversityDegreeCredential")
+ .subject(subject)
+ .build()?;
+
+ let credential_jwt = issuer
+ .document()
+ .unwrap()
+ .create_credential_jwt(
+ &credential,
+ &issuer.storage(),
+ &issuer.fragment().unwrap(),
+ &JwsSignatureOptions::default(),
+ None,
+ )
+ .await?
+ .into();
+
+ let mut grpc_client = VcValidationClient::connect(server.endpoint()).await?;
+ let decoded_cred = grpc_client
+ .validate(VcValidationRequest {
+ credential_jwt,
+ status_list_credential_json: None,
+ })
+ .await?
+ .into_inner()
+ .credential_json;
+
+ let decoded_cred = serde_json::from_str::(&decoded_cred)?;
+ assert_eq!(decoded_cred, credential);
+
+ Ok(())
+}
+
+#[tokio::test]
+async fn revoked_credential_validation() -> anyhow::Result<()> {
+ let stronghold = StrongholdStorage::new(make_stronghold());
+ let server = TestServer::new_with_stronghold(stronghold.clone()).await;
+ let api_client = server.client();
+
+ let mut issuer = Entity::new_with_stronghold(stronghold);
+ issuer.create_did(api_client).await?;
+
+ let mut holder = Entity::new();
+ holder.create_did(api_client).await?;
+
+ let subject = Subject::from_json_value(json!({
+ "id": holder.document().unwrap().id().as_str(),
+ "name": "Alice",
+ "degree": {
+ "type": "BachelorDegree",
+ "name": "Bachelor of Science and Arts",
+ },
+ "GPA": "4.0",
+ }))?;
+
+ let mut status_list_credential = StatusList2021CredentialBuilder::new(StatusList2021::default())
+ .issuer(Issuer::Url(Url::parse(issuer.document().unwrap().id().as_str())?))
+ .purpose(StatusPurpose::Revocation)
+ .subject_id(Url::parse("https://example.edu/credentials/status/1")?)
+ .build()?;
+
+ // Build credential using subject above and issuer.
+ let mut credential: Credential = CredentialBuilder::default()
+ .id(Url::parse("https://example.edu/credentials/3732")?)
+ .issuer(Url::parse(issuer.document().unwrap().id().as_str())?)
+ .type_("UniversityDegreeCredential")
+ .subject(subject)
+ .build()?;
+ status_list_credential.set_credential_status(&mut credential, 0, true)?;
+
+ let credential_jwt = issuer
+ .document()
+ .unwrap()
+ .create_credential_jwt(
+ &credential,
+ &issuer.storage(),
+ &issuer.fragment().unwrap(),
+ &JwsSignatureOptions::default(),
+ None,
+ )
+ .await?
+ .into();
+
+ let mut grpc_client = VcValidationClient::connect(server.endpoint()).await?;
+ let error = grpc_client
+ .validate(VcValidationRequest {
+ credential_jwt,
+ status_list_credential_json: Some(status_list_credential.to_json()?),
+ })
+ .await
+ .unwrap_err();
+
+ assert!(error.message().contains("revoked"));
+
+ Ok(())
+}
diff --git a/bindings/grpc/tests/api/did_document_creation.rs b/bindings/grpc/tests/api/did_document_creation.rs
new file mode 100644
index 0000000000..394217e7a3
--- /dev/null
+++ b/bindings/grpc/tests/api/did_document_creation.rs
@@ -0,0 +1,43 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+use identity_stronghold::StrongholdStorage;
+use iota_sdk::types::block::address::ToBech32Ext;
+use tonic::Request;
+
+use crate::helpers::get_address_with_funds;
+use crate::helpers::make_stronghold;
+use crate::helpers::Entity;
+use crate::helpers::TestServer;
+use crate::helpers::FAUCET_ENDPOINT;
+use _document::document_service_client::DocumentServiceClient;
+use _document::CreateDidRequest;
+
+mod _document {
+ tonic::include_proto!("document");
+}
+
+#[tokio::test]
+async fn did_document_creation() -> anyhow::Result<()> {
+ let stronghold = StrongholdStorage::new(make_stronghold());
+ let server = TestServer::new_with_stronghold(stronghold.clone()).await;
+ let api_client = server.client();
+ let hrp = api_client.get_bech32_hrp().await?;
+
+ let user = Entity::new_with_stronghold(stronghold);
+ let user_address = get_address_with_funds(
+ api_client,
+ user.storage().key_storage().as_secret_manager(),
+ FAUCET_ENDPOINT,
+ )
+ .await?;
+
+ let mut grpc_client = DocumentServiceClient::connect(server.endpoint()).await?;
+ grpc_client
+ .create(Request::new(CreateDidRequest {
+ bech32_address: user_address.to_bech32(hrp).to_string(),
+ }))
+ .await?;
+
+ Ok(())
+}
diff --git a/bindings/grpc/tests/api/domain_linkage.rs b/bindings/grpc/tests/api/domain_linkage.rs
new file mode 100644
index 0000000000..a79b732d58
--- /dev/null
+++ b/bindings/grpc/tests/api/domain_linkage.rs
@@ -0,0 +1,174 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+use identity_iota::core::Duration;
+use identity_iota::core::Object;
+use identity_iota::core::OrderedSet;
+use identity_iota::core::Timestamp;
+use identity_iota::core::Url;
+use identity_iota::credential::Credential;
+use identity_iota::credential::DomainLinkageConfiguration;
+use identity_iota::credential::DomainLinkageCredentialBuilder;
+use identity_iota::credential::Jwt;
+use identity_iota::credential::LinkedDomainService;
+use identity_iota::did::DIDUrl;
+use identity_iota::did::DID;
+use identity_storage::JwkDocumentExt;
+use identity_storage::JwsSignatureOptions;
+use identity_stronghold::StrongholdStorage;
+
+use crate::domain_linkage::_credentials::domain_linkage_client::DomainLinkageClient;
+use crate::domain_linkage::_credentials::LinkedDidEndpointValidationStatus;
+use crate::domain_linkage::_credentials::LinkedDidValidationStatus;
+use crate::domain_linkage::_credentials::ValidateDidAgainstDidConfigurationsRequest;
+use crate::domain_linkage::_credentials::ValidateDidResponse;
+use crate::domain_linkage::_credentials::ValidateDomainAgainstDidConfigurationRequest;
+use crate::domain_linkage::_credentials::ValidateDomainResponse;
+use crate::helpers::make_stronghold;
+use crate::helpers::Entity;
+use crate::helpers::TestServer;
+
+mod _credentials {
+ tonic::include_proto!("domain_linkage");
+}
+
+/// Prepares basically the same test setup as in test `examples/1_advanced/6_domain_linkage.rs`.
+async fn prepare_test() -> anyhow::Result<(TestServer, Url, String, Jwt)> {
+ let stronghold = StrongholdStorage::new(make_stronghold());
+ let server = TestServer::new_with_stronghold(stronghold.clone()).await;
+ let api_client = server.client();
+
+ let mut issuer = Entity::new_with_stronghold(stronghold);
+ issuer.create_did(api_client).await?;
+ let did = issuer
+ .document()
+ .ok_or_else(|| anyhow::anyhow!("no DID document for issuer"))?
+ .id();
+ let did_string = did.to_string();
+ // =====================================================
+ // Create Linked Domain service
+ // =====================================================
+
+ // The DID should be linked to the following domains.
+ let domain_1: Url = Url::parse("https://foo.example.com")?;
+ let domain_2: Url = Url::parse("https://bar.example.com")?;
+
+ let mut domains: OrderedSet = OrderedSet::new();
+ domains.append(domain_1.clone());
+ domains.append(domain_2.clone());
+
+ // Create a Linked Domain Service to enable the discovery of the linked domains through the DID Document.
+ // This is optional since it is not a hard requirement by the specs.
+ let service_url: DIDUrl = did.clone().join("#domain-linkage")?;
+ let linked_domain_service: LinkedDomainService = LinkedDomainService::new(service_url, domains, Object::new())?;
+ issuer
+ .update_document(&api_client, |mut doc| {
+ doc.insert_service(linked_domain_service.into()).ok().map(|_| doc)
+ })
+ .await?;
+ let updated_did_document = issuer
+ .document()
+ .ok_or_else(|| anyhow::anyhow!("no DID document for issuer"))?;
+
+ println!("DID document with linked domain service: {updated_did_document:#}");
+
+ // =====================================================
+ // Create DID Configuration resource
+ // =====================================================
+
+ // Create the Domain Linkage Credential.
+ let domain_linkage_credential: Credential = DomainLinkageCredentialBuilder::new()
+ .issuer(updated_did_document.id().clone().into())
+ .origin(domain_1.clone())
+ .issuance_date(Timestamp::now_utc())
+ // Expires after a year.
+ .expiration_date(
+ Timestamp::now_utc()
+ .checked_add(Duration::days(365))
+ .ok_or_else(|| anyhow::anyhow!("calculation should not overflow"))?,
+ )
+ .build()?;
+
+ let jwt: Jwt = updated_did_document
+ .create_credential_jwt(
+ &domain_linkage_credential,
+ &issuer.storage(),
+ &issuer
+ .fragment()
+ .ok_or_else(|| anyhow::anyhow!("no fragment for issuer"))?,
+ &JwsSignatureOptions::default(),
+ None,
+ )
+ .await?;
+
+ Ok((server, domain_1, did_string, jwt))
+}
+
+#[tokio::test]
+async fn can_validate_domain() -> anyhow::Result<()> {
+ let (server, linked_domain, _, jwt) = prepare_test().await?;
+ let configuration_resource: DomainLinkageConfiguration = DomainLinkageConfiguration::new(vec![jwt.clone()]);
+ let mut grpc_client = DomainLinkageClient::connect(server.endpoint()).await?;
+
+ let response = grpc_client
+ .validate_domain_against_did_configuration(ValidateDomainAgainstDidConfigurationRequest {
+ domain: linked_domain.to_string(),
+ did_configuration: configuration_resource.to_string(),
+ })
+ .await?;
+
+ assert_eq!(
+ response.into_inner(),
+ ValidateDomainResponse {
+ linked_dids: vec![LinkedDidValidationStatus {
+ valid: true,
+ document: Some(jwt.as_str().to_string()),
+ error: None,
+ }],
+ }
+ );
+
+ Ok(())
+}
+
+#[tokio::test]
+async fn can_validate_did() -> anyhow::Result<()> {
+ let (server, linked_domain, issuer_did, jwt) = prepare_test().await?;
+ let configuration_resource: DomainLinkageConfiguration = DomainLinkageConfiguration::new(vec![jwt.clone()]);
+ let mut grpc_client = DomainLinkageClient::connect(server.endpoint()).await?;
+
+ let response = grpc_client
+ .validate_did_against_did_configurations(ValidateDidAgainstDidConfigurationsRequest {
+ did: issuer_did.clone(),
+ did_configurations: vec![ValidateDomainAgainstDidConfigurationRequest {
+ domain: linked_domain.to_string(),
+ did_configuration: configuration_resource.to_string(),
+ }],
+ })
+ .await?;
+
+ assert_eq!(
+ response.into_inner(),
+ ValidateDidResponse {
+ service: vec![
+ LinkedDidEndpointValidationStatus {
+ id: issuer_did,
+ service_endpoint: vec![
+ LinkedDidValidationStatus {
+ valid: true,
+ document: Some(jwt.as_str().to_string()),
+ error: None,
+ },
+ LinkedDidValidationStatus {
+ valid: false,
+ document: None,
+ error: Some("could not get domain linkage config; domain linkage error: error sending request for url (https://bar.example.com/.well-known/did-configuration.json): error trying to connect: dns error: failed to lookup address information: nodename nor servname provided, or not known".to_string()),
+ }
+ ],
+ }
+ ]
+ }
+ );
+
+ Ok(())
+}
diff --git a/bindings/grpc/tests/api/health_check.rs b/bindings/grpc/tests/api/health_check.rs
new file mode 100644
index 0000000000..d8ea486269
--- /dev/null
+++ b/bindings/grpc/tests/api/health_check.rs
@@ -0,0 +1,24 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+use health_check::health_check_client::HealthCheckClient;
+use health_check::HealthCheckRequest;
+use health_check::HealthCheckResponse;
+
+use crate::helpers::TestServer;
+
+mod health_check {
+ tonic::include_proto!("health_check");
+}
+
+#[tokio::test]
+async fn health_check() -> anyhow::Result<()> {
+ let server = TestServer::new().await;
+ let mut grpc_client = HealthCheckClient::connect(server.endpoint()).await?;
+ let request = tonic::Request::new(HealthCheckRequest {});
+
+ let response = grpc_client.check(request).await?;
+ assert_eq!(response.into_inner(), HealthCheckResponse { status: "OK".into() });
+
+ Ok(())
+}
diff --git a/bindings/grpc/tests/api/helpers.rs b/bindings/grpc/tests/api/helpers.rs
new file mode 100644
index 0000000000..c307213db7
--- /dev/null
+++ b/bindings/grpc/tests/api/helpers.rs
@@ -0,0 +1,336 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+use anyhow::Context;
+use identity_iota::iota::IotaClientExt;
+use identity_iota::iota::IotaDocument;
+use identity_iota::iota::IotaIdentityClientExt;
+use identity_iota::iota::NetworkName;
+use identity_iota::verification::jws::JwsAlgorithm;
+use identity_iota::verification::MethodScope;
+use identity_storage::key_id_storage::KeyIdMemstore;
+use identity_storage::key_storage::JwkMemStore;
+use identity_storage::JwkDocumentExt;
+use identity_storage::JwkStorage;
+use identity_storage::KeyIdStorage;
+use identity_storage::Storage;
+use identity_stronghold::StrongholdStorage;
+use iota_sdk::client::api::GetAddressesOptions;
+use iota_sdk::client::node_api::indexer::query_parameters::QueryParameter;
+use iota_sdk::client::secret::stronghold::StrongholdSecretManager;
+use iota_sdk::client::secret::SecretManager;
+use iota_sdk::client::stronghold::StrongholdAdapter;
+use iota_sdk::client::Client;
+use iota_sdk::client::Password;
+use iota_sdk::crypto::keys::bip39;
+use iota_sdk::types::block::address::Address;
+use iota_sdk::types::block::address::Bech32Address;
+use iota_sdk::types::block::address::Hrp;
+use iota_sdk::types::block::output::AliasOutputBuilder;
+use rand::distributions::Alphanumeric;
+use rand::distributions::DistString;
+use rand::thread_rng;
+use std::net::SocketAddr;
+use std::path::PathBuf;
+use tokio::net::TcpListener;
+use tokio::task::JoinHandle;
+use tonic::transport::Uri;
+
+pub type MemStorage = Storage;
+
+pub const API_ENDPOINT: &str = "http://localhost";
+pub const FAUCET_ENDPOINT: &str = "http://localhost/faucet/api/enqueue";
+
+#[derive(Debug)]
+pub struct TestServer {
+ client: Client,
+ addr: SocketAddr,
+ _handle: JoinHandle>,
+}
+
+impl TestServer {
+ pub async fn new() -> Self {
+ let stronghold = StrongholdSecretManager::builder()
+ .password(random_password(18))
+ .build(random_stronghold_path())
+ .map(StrongholdStorage::new)
+ .expect("Failed to create temporary stronghold");
+
+ Self::new_with_stronghold(stronghold).await
+ }
+
+ pub async fn new_with_stronghold(stronghold: StrongholdStorage) -> Self {
+ let _ = tracing::subscriber::set_global_default(tracing_subscriber::fmt().compact().finish());
+
+ let listener = TcpListener::bind("127.0.0.1:0")
+ .await
+ .expect("Failed to bind to random OS's port");
+ let addr = listener.local_addr().unwrap();
+
+ let client: Client = Client::builder()
+ .with_primary_node(API_ENDPOINT, None)
+ .unwrap()
+ .finish()
+ .await
+ .expect("Failed to connect to API's endpoint");
+
+ let server = identity_grpc::server::GRpcServer::new(client.clone(), stronghold)
+ .into_router()
+ .serve_with_incoming(tokio_stream::wrappers::TcpListenerStream::new(listener));
+ TestServer {
+ _handle: tokio::spawn(server),
+ addr,
+ client,
+ }
+ }
+
+ pub fn endpoint(&self) -> Uri {
+ format!("https://{}", self.addr)
+ .parse()
+ .expect("Failed to parse server's URI")
+ }
+
+ pub fn client(&self) -> &Client {
+ &self.client
+ }
+}
+
+pub async fn create_did(
+ client: &Client,
+ secret_manager: &mut SecretManager,
+ storage: &Storage,
+) -> anyhow::Result<(Address, IotaDocument, String)>
+where
+ K: JwkStorage,
+ I: KeyIdStorage,
+{
+ let address: Address = get_address_with_funds(client, secret_manager, FAUCET_ENDPOINT)
+ .await
+ .context("failed to get address with funds")?;
+
+ let network_name = client.network_name().await?;
+ let (document, fragment): (IotaDocument, String) = create_did_document(&network_name, storage).await?;
+ let alias_output = client.new_did_output(address, document, None).await?;
+
+ let document: IotaDocument = client.publish_did_output(secret_manager, alias_output).await?;
+
+ Ok((address, document, fragment))
+}
+
+/// Creates an example DID document with the given `network_name`.
+///
+/// Its functionality is equivalent to the "create DID" example
+/// and exists for convenient calling from the other examples.
+pub async fn create_did_document(
+ network_name: &NetworkName,
+ storage: &Storage,
+) -> anyhow::Result<(IotaDocument, String)>
+where
+ I: KeyIdStorage,
+ K: JwkStorage,
+{
+ let mut document: IotaDocument = IotaDocument::new(network_name);
+
+ let fragment: String = document
+ .generate_method(
+ storage,
+ JwkMemStore::ED25519_KEY_TYPE,
+ JwsAlgorithm::EdDSA,
+ None,
+ MethodScope::VerificationMethod,
+ )
+ .await?;
+
+ Ok((document, fragment))
+}
+
+/// Generates an address from the given [`SecretManager`] and adds funds from the faucet.
+pub async fn get_address_with_funds(
+ client: &Client,
+ stronghold: &SecretManager,
+ faucet_endpoint: &str,
+) -> anyhow::Result {
+ let address = get_address(client, stronghold).await?;
+
+ request_faucet_funds(client, address, faucet_endpoint)
+ .await
+ .context("failed to request faucet funds")?;
+
+ Ok(*address)
+}
+
+/// Initializes the [`SecretManager`] with a new mnemonic, if necessary,
+/// and generates an address from the given [`SecretManager`].
+pub async fn get_address(client: &Client, secret_manager: &SecretManager) -> anyhow::Result {
+ let random: [u8; 32] = rand::random();
+ let mnemonic = bip39::wordlist::encode(random.as_ref(), &bip39::wordlist::ENGLISH)
+ .map_err(|err| anyhow::anyhow!(format!("{err:?}")))?;
+
+ if let SecretManager::Stronghold(ref stronghold) = secret_manager {
+ match stronghold.store_mnemonic(mnemonic).await {
+ Ok(()) => (),
+ Err(iota_sdk::client::stronghold::Error::MnemonicAlreadyStored) => (),
+ Err(err) => anyhow::bail!(err),
+ }
+ } else {
+ anyhow::bail!("expected a `StrongholdSecretManager`");
+ }
+
+ let bech32_hrp: Hrp = client.get_bech32_hrp().await?;
+ let address: Bech32Address = secret_manager
+ .generate_ed25519_addresses(
+ GetAddressesOptions::default()
+ .with_range(0..1)
+ .with_bech32_hrp(bech32_hrp),
+ )
+ .await?[0];
+
+ Ok(address)
+}
+
+/// Requests funds from the faucet for the given `address`.
+async fn request_faucet_funds(client: &Client, address: Bech32Address, faucet_endpoint: &str) -> anyhow::Result<()> {
+ iota_sdk::client::request_funds_from_faucet(faucet_endpoint, &address).await?;
+
+ tokio::time::timeout(std::time::Duration::from_secs(45), async {
+ loop {
+ tokio::time::sleep(std::time::Duration::from_secs(5)).await;
+
+ let balance = get_address_balance(client, &address)
+ .await
+ .context("failed to get address balance")?;
+ if balance > 0 {
+ break;
+ }
+ }
+ Ok::<(), anyhow::Error>(())
+ })
+ .await
+ .context("maximum timeout exceeded")??;
+
+ Ok(())
+}
+
+pub struct Entity {
+ secret_manager: SecretManager,
+ storage: Storage,
+ did: Option<(Address, IotaDocument, String)>,
+}
+
+pub fn random_password(len: usize) -> Password {
+ let mut rng = thread_rng();
+ Alphanumeric.sample_string(&mut rng, len).into()
+}
+
+pub fn random_stronghold_path() -> PathBuf {
+ let mut file = std::env::temp_dir();
+ file.push("test_strongholds");
+ file.push(rand::distributions::Alphanumeric.sample_string(&mut rand::thread_rng(), 32));
+ file.set_extension("stronghold");
+ file.to_owned()
+}
+
+impl Default for Entity {
+ fn default() -> Self {
+ let secret_manager = SecretManager::Stronghold(make_stronghold());
+ let storage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new());
+
+ Self {
+ secret_manager,
+ storage,
+ did: None,
+ }
+ }
+}
+
+impl Entity {
+ pub fn new() -> Self {
+ Self::default()
+ }
+}
+
+impl Entity {
+ pub fn new_with_stronghold(s: StrongholdStorage) -> Self {
+ let secret_manager = SecretManager::Stronghold(make_stronghold());
+ let storage = Storage::new(s.clone(), s);
+
+ Self {
+ secret_manager,
+ storage,
+ did: None,
+ }
+ }
+}
+
+impl Entity {
+ pub async fn create_did(&mut self, client: &Client) -> anyhow::Result<()> {
+ let Entity {
+ secret_manager,
+ storage,
+ did,
+ } = self;
+ *did = Some(create_did(client, secret_manager, storage).await?);
+
+ Ok(())
+ }
+
+ pub fn storage(&self) -> &Storage {
+ &self.storage
+ }
+
+ pub fn document(&self) -> Option<&IotaDocument> {
+ self.did.as_ref().map(|(_, doc, _)| doc)
+ }
+
+ pub fn fragment(&self) -> Option<&str> {
+ self.did.as_ref().map(|(_, _, frag)| frag.as_ref())
+ }
+
+ pub async fn update_document(&mut self, client: &Client, f: F) -> anyhow::Result<()>
+ where
+ F: FnOnce(IotaDocument) -> Option,
+ {
+ let (address, doc, fragment) = self.did.take().context("Missing doc")?;
+ let mut new_doc = f(doc.clone());
+ if let Some(doc) = new_doc.take() {
+ let alias_output = client.update_did_output(doc.clone()).await?;
+ let rent_structure = client.get_rent_structure().await?;
+ let alias_output = AliasOutputBuilder::from(&alias_output)
+ .with_minimum_storage_deposit(rent_structure)
+ .finish()?;
+
+ new_doc = Some(client.publish_did_output(&self.secret_manager, alias_output).await?);
+ }
+
+ self.did = Some((address, new_doc.unwrap_or(doc), fragment));
+
+ Ok(())
+ }
+}
+/// Returns the balance of the given Bech32-encoded `address`.
+async fn get_address_balance(client: &Client, address: &Bech32Address) -> anyhow::Result {
+ let output_ids = client
+ .basic_output_ids(vec![
+ QueryParameter::Address(address.to_owned()),
+ QueryParameter::HasExpiration(false),
+ QueryParameter::HasTimelock(false),
+ QueryParameter::HasStorageDepositReturn(false),
+ ])
+ .await?;
+
+ let outputs = client.get_outputs(&output_ids).await?;
+
+ let mut total_amount = 0;
+ for output_response in outputs {
+ total_amount += output_response.output().amount();
+ }
+
+ Ok(total_amount)
+}
+
+pub fn make_stronghold() -> StrongholdAdapter {
+ StrongholdAdapter::builder()
+ .password(random_password(18))
+ .build(random_stronghold_path())
+ .expect("Failed to create temporary stronghold")
+}
diff --git a/bindings/grpc/tests/api/jwt.rs b/bindings/grpc/tests/api/jwt.rs
new file mode 100644
index 0000000000..927027b300
--- /dev/null
+++ b/bindings/grpc/tests/api/jwt.rs
@@ -0,0 +1,54 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+use _credentials::jwt_client::JwtClient;
+use _credentials::JwtCreationRequest;
+use identity_iota::core::Object;
+use identity_iota::core::Timestamp;
+use identity_iota::core::ToJson;
+use identity_iota::credential::CredentialBuilder;
+use identity_iota::did::DID;
+use identity_stronghold::StrongholdStorage;
+use iota_sdk::Url;
+use serde_json::json;
+
+use crate::helpers::make_stronghold;
+use crate::helpers::Entity;
+use crate::helpers::TestServer;
+
+mod _credentials {
+ tonic::include_proto!("credentials");
+}
+
+#[tokio::test]
+async fn jwt_creation() -> anyhow::Result<()> {
+ let stronghold = StrongholdStorage::new(make_stronghold());
+ let server = TestServer::new_with_stronghold(stronghold.clone()).await;
+ let api_client = server.client();
+
+ let mut issuer = Entity::new_with_stronghold(stronghold);
+ issuer.create_did(api_client).await?;
+
+ let mut holder = Entity::new();
+ holder.create_did(api_client).await?;
+
+ let credential = CredentialBuilder::::default()
+ .issuance_date(Timestamp::now_utc())
+ .issuer(Url::parse(issuer.document().unwrap().id().as_str())?)
+ .subject(serde_json::from_value(json!({
+ "id": holder.document().unwrap().id().as_str(),
+ "type": "UniversityDegree",
+ "gpa": "4.0",
+ }))?)
+ .build()?;
+
+ let mut grpc_client = JwtClient::connect(server.endpoint()).await?;
+ let _ = grpc_client
+ .create(JwtCreationRequest {
+ credential_json: credential.to_json()?,
+ issuer_fragment: issuer.fragment().unwrap().to_owned(),
+ })
+ .await?;
+
+ Ok(())
+}
diff --git a/bindings/grpc/tests/api/main.rs b/bindings/grpc/tests/api/main.rs
new file mode 100644
index 0000000000..af4929bfae
--- /dev/null
+++ b/bindings/grpc/tests/api/main.rs
@@ -0,0 +1,13 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+mod credential_revocation_check;
+mod credential_validation;
+mod did_document_creation;
+mod domain_linkage;
+mod health_check;
+mod helpers;
+mod jwt;
+mod sd_jwt_validation;
+mod status_list_2021;
+mod utils;
diff --git a/bindings/grpc/tests/api/sd_jwt_validation.rs b/bindings/grpc/tests/api/sd_jwt_validation.rs
new file mode 100644
index 0000000000..e746e930c3
--- /dev/null
+++ b/bindings/grpc/tests/api/sd_jwt_validation.rs
@@ -0,0 +1,165 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+use _sd_jwt::verification_client::VerificationClient;
+use _sd_jwt::KeyBindingOptions;
+use _sd_jwt::VerificationRequest;
+use identity_iota::core::FromJson;
+use identity_iota::core::Timestamp;
+use identity_iota::core::ToJson;
+use identity_iota::core::Url;
+use identity_iota::credential::Credential;
+use identity_iota::credential::CredentialBuilder;
+use identity_iota::credential::Jws;
+use identity_iota::credential::Subject;
+use identity_iota::did::DID;
+use identity_iota::sd_jwt_payload::KeyBindingJwtClaims;
+use identity_iota::sd_jwt_payload::SdJwt;
+use identity_iota::sd_jwt_payload::SdObjectEncoder;
+use identity_iota::sd_jwt_payload::Sha256Hasher;
+use identity_storage::JwkDocumentExt;
+use identity_storage::JwsSignatureOptions;
+
+use crate::helpers::Entity;
+use crate::helpers::TestServer;
+
+mod _sd_jwt {
+ tonic::include_proto!("sd_jwt");
+}
+
+#[tokio::test]
+async fn sd_jwt_validation_works() -> anyhow::Result<()> {
+ let server = TestServer::new().await;
+ let client = server.client();
+ let mut issuer = Entity::new();
+ issuer.create_did(client).await?;
+
+ let mut holder = Entity::new();
+ holder.create_did(client).await?;
+
+ // Create an address credential subject.
+ let subject = Subject::from_json_value(serde_json::json!({
+ "id": holder.document().unwrap().id().as_str(),
+ "name": "Alice",
+ "address": {
+ "locality": "Maxstadt",
+ "postal_code": "12344",
+ "country": "DE",
+ "street_address": "Weidenstraße 22"
+ }
+ }))?;
+ // Build credential using subject above and issuer.
+ let credential: Credential = CredentialBuilder::default()
+ .id(Url::parse("https://example.com/credentials/3732")?)
+ .issuer(Url::parse(issuer.document().unwrap().id().as_str())?)
+ .type_("AddressCredential")
+ .subject(subject)
+ .build()?;
+
+ // In Order to create an selective disclosure JWT, the plain text JWT
+ // claims set must be created first.
+ let payload = credential.serialize_jwt(None)?;
+
+ // Using the crate `sd-jwt` properties of the claims can be made selectively disclosable.
+ // The default sha-256 hasher will be used to create the digests.
+ // Read more in https://github.com/iotaledger/sd-jwt-payload .
+ let mut encoder = SdObjectEncoder::new(&payload)?;
+
+ // Make "locality", "postal_code" and "street_address" selectively disclosable while keeping
+ // other properties in plain text.
+ let disclosures = vec![
+ encoder.conceal("/vc/credentialSubject/address/locality", None)?,
+ encoder.conceal("/vc/credentialSubject/address/postal_code", None)?,
+ encoder.conceal("/vc/credentialSubject/address/street_address", None)?,
+ ];
+
+ // Add the `_sd_alg` property.
+ encoder.add_sd_alg_property();
+ let encoded_payload = encoder.try_to_string()?;
+
+ // Create the signed JWT.
+ let jwt: Jws = issuer
+ .document()
+ .unwrap()
+ .create_jws(
+ issuer.storage(),
+ issuer.fragment().unwrap(),
+ encoded_payload.as_bytes(),
+ &JwsSignatureOptions::default(),
+ )
+ .await?;
+
+ // One way to send the JWT and the disclosures, is by creating an SD-JWT with all the
+ // disclosures.
+ let disclosures: Vec = disclosures
+ .into_iter()
+ .map(|disclosure| disclosure.to_string())
+ .collect();
+ let sd_jwt_str = SdJwt::new(jwt.into(), disclosures, None).presentation();
+
+ const VERIFIER_DID: &str = "did:example:verifier";
+ // A unique random challenge generated by the requester per presentation can mitigate replay attacks.
+ let nonce: &str = "475a7984-1bb5-4c4c-a56f-822bccd46440";
+
+ // ===========================================================================
+ // Step 5: Holder creates an SD-JWT to be presented to a verifier.
+ // ===========================================================================
+
+ let sd_jwt = SdJwt::parse(&sd_jwt_str)?;
+
+ // The holder only wants to present "locality" and "postal_code" but not "street_address".
+ let disclosures = vec![
+ sd_jwt.disclosures.first().unwrap().clone(),
+ sd_jwt.disclosures.get(1).unwrap().clone(),
+ ];
+
+ // Optionally, the holder can add a Key Binding JWT (KB-JWT). This is dependent on the verifier's policy.
+ // Issuing the KB-JWT is done by creating the claims set and setting the header `typ` value
+ // with the help of `KeyBindingJwtClaims`.
+ let binding_claims = KeyBindingJwtClaims::new(
+ &Sha256Hasher::new(),
+ sd_jwt.jwt.as_str().to_string(),
+ disclosures.clone(),
+ nonce.to_string(),
+ VERIFIER_DID.to_string(),
+ Timestamp::now_utc().to_unix(),
+ )
+ .to_json()?;
+
+ // Setting the `typ` in the header is required.
+ let options = JwsSignatureOptions::new().typ(KeyBindingJwtClaims::KB_JWT_HEADER_TYP);
+
+ // Create the KB-JWT.
+ let kb_jwt: Jws = holder
+ .document()
+ .unwrap()
+ .create_jws(
+ holder.storage(),
+ holder.fragment().unwrap(),
+ binding_claims.as_bytes(),
+ &options,
+ )
+ .await?;
+
+ // Create the final SD-JWT.
+ let sd_jwt_obj = SdJwt::new(sd_jwt.jwt, disclosures, Some(kb_jwt.into()));
+
+ // Holder presents the SD-JWT to the verifier.
+ let sd_jwt_presentation: String = sd_jwt_obj.presentation();
+
+ // Verify the JWT.
+ let mut sd_jwt_verification_client = VerificationClient::connect(server.endpoint()).await?;
+ let _ = sd_jwt_verification_client
+ .verify(VerificationRequest {
+ jwt: sd_jwt_presentation,
+ kb_options: Some(KeyBindingOptions {
+ nonce: Some(nonce.to_owned()),
+ aud: Some(VERIFIER_DID.to_owned()),
+ holder_did: holder.document().unwrap().id().to_string(),
+ ..Default::default()
+ }),
+ })
+ .await?;
+
+ Ok(())
+}
diff --git a/bindings/grpc/tests/api/status_list_2021.rs b/bindings/grpc/tests/api/status_list_2021.rs
new file mode 100644
index 0000000000..67ad31b34d
--- /dev/null
+++ b/bindings/grpc/tests/api/status_list_2021.rs
@@ -0,0 +1,94 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+use crate::helpers::TestServer;
+use _status_list_2021::status_list2021_svc_client::StatusList2021SvcClient;
+use _status_list_2021::CreateRequest;
+use _status_list_2021::Purpose;
+use _status_list_2021::UpdateRequest;
+use identity_iota::core::FromJson;
+use identity_iota::core::ToJson;
+use identity_iota::core::Url;
+use identity_iota::credential::status_list_2021::StatusList2021;
+use identity_iota::credential::status_list_2021::StatusList2021Credential;
+use identity_iota::credential::status_list_2021::StatusList2021CredentialBuilder;
+use identity_iota::credential::status_list_2021::StatusPurpose;
+use identity_iota::credential::Issuer;
+use tonic::Request;
+
+mod _status_list_2021 {
+ tonic::include_proto!("status_list_2021");
+}
+
+#[tokio::test]
+async fn status_list_2021_credential_creation() -> anyhow::Result<()> {
+ let server = TestServer::new().await;
+
+ let id = Url::parse("http://example.com/credentials/status/1").unwrap();
+ let issuer = Issuer::Url(Url::parse("http://example.com/issuers/1").unwrap());
+ let status_list_credential = StatusList2021CredentialBuilder::new(StatusList2021::default())
+ .purpose(StatusPurpose::Revocation)
+ .subject_id(id.clone())
+ .issuer(issuer.clone())
+ .build()
+ .unwrap();
+
+ let mut grpc_client = StatusList2021SvcClient::connect(server.endpoint()).await?;
+ let res = grpc_client
+ .create(Request::new(CreateRequest {
+ id: Some(id.into_string()),
+ issuer: issuer.url().to_string(),
+ purpose: Purpose::Revocation as i32,
+ length: None,
+ expiration_date: None,
+ contexts: vec![],
+ types: vec![],
+ }))
+ .await?
+ .into_inner()
+ .credential_json;
+ let grpc_credential = StatusList2021Credential::from_json(&res)?;
+
+ assert_eq!(status_list_credential, grpc_credential);
+ Ok(())
+}
+
+#[tokio::test]
+async fn status_list_2021_credential_update() -> anyhow::Result<()> {
+ let server = TestServer::new().await;
+
+ let id = Url::parse("http://example.com/credentials/status/1").unwrap();
+ let issuer = Issuer::Url(Url::parse("http://example.com/issuers/1").unwrap());
+ let mut status_list_credential = StatusList2021CredentialBuilder::new(StatusList2021::default())
+ .purpose(StatusPurpose::Revocation)
+ .subject_id(id)
+ .issuer(issuer)
+ .build()
+ .unwrap();
+
+ let entries_to_set = [0_u64, 42, 420, 4200];
+ let entries = entries_to_set.iter().map(|i| (*i, true)).collect();
+
+ let mut grpc_client = StatusList2021SvcClient::connect(server.endpoint()).await?;
+ let grpc_credential = grpc_client
+ .update(Request::new(UpdateRequest {
+ credential_json: status_list_credential.to_json().unwrap(),
+ entries,
+ }))
+ .await
+ .map(|res| res.into_inner().credential_json)
+ .map(|credential_json| StatusList2021Credential::from_json(&credential_json).unwrap())
+ .unwrap();
+
+ status_list_credential.update(|status_list| {
+ for idx in entries_to_set {
+ if let Err(e) = status_list.set_entry(idx as usize, true) {
+ return Err(e);
+ }
+ }
+ Ok(())
+ })?;
+
+ assert_eq!(status_list_credential, grpc_credential);
+ Ok(())
+}
diff --git a/bindings/grpc/tests/api/utils.rs b/bindings/grpc/tests/api/utils.rs
new file mode 100644
index 0000000000..9c863bf3de
--- /dev/null
+++ b/bindings/grpc/tests/api/utils.rs
@@ -0,0 +1,48 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+use _utils::signing_client::SigningClient;
+use _utils::DataSigningRequest;
+use identity_iota::verification::jws::JwsAlgorithm;
+use identity_storage::JwkStorage;
+use identity_storage::KeyType;
+use identity_stronghold::StrongholdStorage;
+
+use crate::helpers::make_stronghold;
+use crate::helpers::TestServer;
+
+mod _utils {
+ tonic::include_proto!("utils");
+}
+
+const SAMPLE_SIGNING_DATA: &'static [u8] = b"I'm just some random data to be signed :)";
+
+#[tokio::test]
+async fn raw_data_signing_works() -> anyhow::Result<()> {
+ let stronghold = StrongholdStorage::new(make_stronghold());
+ let server = TestServer::new_with_stronghold(stronghold.clone()).await;
+
+ let key_id = stronghold
+ .generate(KeyType::from_static_str("Ed25519"), JwsAlgorithm::EdDSA)
+ .await?
+ .key_id;
+
+ let expected_signature = {
+ let public_key_jwk = stronghold.get_public_key(&key_id).await?;
+ stronghold.sign(&key_id, SAMPLE_SIGNING_DATA, &public_key_jwk).await?
+ };
+
+ let mut grpc_client = SigningClient::connect(server.endpoint()).await?;
+ let signature = grpc_client
+ .sign(DataSigningRequest {
+ data: SAMPLE_SIGNING_DATA.to_owned(),
+ key_id: key_id.to_string(),
+ })
+ .await?
+ .into_inner()
+ .signature;
+
+ assert_eq!(signature, expected_signature);
+
+ Ok(())
+}
diff --git a/bindings/grpc/tooling/domain-linkage-test-server/.well-known/did-configuration.json b/bindings/grpc/tooling/domain-linkage-test-server/.well-known/did-configuration.json
new file mode 100644
index 0000000000..802f453e3e
--- /dev/null
+++ b/bindings/grpc/tooling/domain-linkage-test-server/.well-known/did-configuration.json
@@ -0,0 +1,6 @@
+{
+ "@context": "https://identity.foundation/.well-known/did-configuration/v1",
+ "linked_dids": [
+ "add your domain linkage credential here"
+ ]
+}
\ No newline at end of file
diff --git a/bindings/grpc/tooling/start-http-server.sh b/bindings/grpc/tooling/start-http-server.sh
new file mode 100644
index 0000000000..4cebbf82d2
--- /dev/null
+++ b/bindings/grpc/tooling/start-http-server.sh
@@ -0,0 +1,4 @@
+#!/bin/sh
+http-server ./domain-linkage-test-server &
+# replace or omint the --domain parameter if you don't have a static domain or don't want to use it
+ngrok http --domain=example-static-domain.ngrok-free.app 8080
\ No newline at end of file
diff --git a/bindings/grpc/tooling/start-rpc-server.sh b/bindings/grpc/tooling/start-rpc-server.sh
new file mode 100755
index 0000000000..69c207f6cf
--- /dev/null
+++ b/bindings/grpc/tooling/start-rpc-server.sh
@@ -0,0 +1,7 @@
+#!/bin/sh
+cd ..
+
+API_ENDPOINT=replace_me \
+STRONGHOLD_PWD=replace_me \
+SNAPSHOT_PATH=replace_me \
+cargo +nightly run --release
diff --git a/bindings/wasm/CHANGELOG.md b/bindings/wasm/CHANGELOG.md
index 14c8168040..67378276e3 100644
--- a/bindings/wasm/CHANGELOG.md
+++ b/bindings/wasm/CHANGELOG.md
@@ -1,285 +1,75 @@
# Changelog
-## [wasm-v1.0.0](https://github.com/iotaledger/identity.rs/tree/wasm-v1.0.0) (2023-11-02)
+## [wasm-v1.3.0](https://github.com/iotaledger/identity.rs/tree/wasm-v1.3.0) (2024-05-28)
-[Full Changelog](https://github.com/iotaledger/identity.rs/compare/wasm-v0.6.0...wasm-v1.0.0)
-
-This version introduces a new DID method targeting the IOTA UTXO ledger. This method works fundamentally differently from the previous method and introduces new capabilities to interact with Layer 1 assets like Native Tokens, NFTs and various Output types.
-
-This version changes the credential and presentation format to JWT, as specified by the [VC Data Model v1.1](https://www.w3.org/TR/vc-data-model/#json-web-token).
-
-Note: Identities and credentials created with the earlier versions cannot be resolved with this version of the library.
-
-### Changed
-
-- Add dedicated EdDSA verifier crate [#1238](https://github.com/iotaledger/identity.rs/pull/1238)
-- Use `VC Data Model v1.1` JWT encoding instead of `VC-JWT` [#1234](https://github.com/iotaledger/identity.rs/pull/1234)
-- Change `verifiable_credential` to type `Vec` in `Presentation` [#1231](https://github.com/iotaledger/identity.rs/pull/1231)
-- Polish Wasm bindings [#1206](https://github.com/iotaledger/identity.rs/pull/1206)
-- Polish `identity_credential` [#1205](https://github.com/iotaledger/identity.rs/pull/1205)
-- Polish `identity_iota_core` [#1203](https://github.com/iotaledger/identity.rs/pull/1203)
-- Upgrade `client-wasm` to `sdk-wasm` [#1202](https://github.com/iotaledger/identity.rs/pull/1202)
-- Rename `JwtPresentation` to `Presentation` [#1200](https://github.com/iotaledger/identity.rs/pull/1200)
-- Remove legacy signing and verification APIs [#1194](https://github.com/iotaledger/identity.rs/pull/1194)
-- Remove old `Presentation` type [#1190](https://github.com/iotaledger/identity.rs/pull/1190)
-- Remove reexported `Resolver` validation APIs [#1183](https://github.com/iotaledger/identity.rs/pull/1183)
-- Use JWT credentials for Domain Linkage [#1180](https://github.com/iotaledger/identity.rs/pull/1180)
-- Remove stronghold nodejs bindings [#1178](https://github.com/iotaledger/identity.rs/pull/1178)
-- JwkStorageDocument & JwtCredential validation [#1152](https://github.com/iotaledger/identity.rs/pull/1152)
-- Add initial PublicKeyJwk support [#1143](https://github.com/iotaledger/identity.rs/pull/1143)
-- Refactor `MethodType` to make it extensible [#1112](https://github.com/iotaledger/identity.rs/pull/1112)
-- Remove generics in `CoreDocument`, `VerificationMethod`, `Service`, `DIDUrl` and `LinkedDomainService` [#1110](https://github.com/iotaledger/identity.rs/pull/1110)
-- Use official client-wasm dependency in examples [#1097](https://github.com/iotaledger/identity.rs/pull/1097)
-- More identifier checks in `CoreDocument` [#1067](https://github.com/iotaledger/identity.rs/pull/1067)
-- Update to `iota-client` 2.0.1-rc.4 and `iota-client-wasm` 0.5.0-alpha.6 [#1088](https://github.com/iotaledger/identity.rs/pull/1088)
-- Use Bech32-encoded state controller and governor addresses [\#1044](https://github.com/iotaledger/identity.rs/pull/1044)
-- Expose iteration over verification relationship fields [\#1024](https://github.com/iotaledger/identity.rs/pull/1024)
-- Chore/rename mixed resolver [\#1026](https://github.com/iotaledger/identity.rs/pull/1026)
-- Add length prefix to DID Document payloads [\#1010](https://github.com/iotaledger/identity.rs/pull/1010)
-- Update Wasm credential, presentation validators for Stardust [\#1004](https://github.com/iotaledger/identity.rs/pull/1004)
-- Rename `Stardust` types to `Iota` [\#1000](https://github.com/iotaledger/identity.rs/pull/1000)
-- Change Stardust DID method to IOTA [\#982](https://github.com/iotaledger/identity.rs/pull/982)
-- Add Wasm Stardust Client [\#975](https://github.com/iotaledger/identity.rs/pull/975)
-- Generalized Resolver [\#970](https://github.com/iotaledger/identity.rs/pull/970)
-- Change `Storage` to handle `CoreDID` [\#968](https://github.com/iotaledger/identity.rs/pull/968)
-- Change `Storage` to store arbitrary blobs [\#953](https://github.com/iotaledger/identity.rs/pull/953)
-- Change `Service` `type` field to allow sets [\#944](https://github.com/iotaledger/identity.rs/pull/944)
-- Generalise `CredentialValidator`, `PresentationValidator` to support arbitrary DID Documents [\#935](https://github.com/iotaledger/identity.rs/pull/935)
+[Full Changelog](https://github.com/iotaledger/identity.rs/compare/wasm-v1.2.0...wasm-v1.3.0)
### Added
-
-- Allow arbitrary JWS header parameters [#1245](https://github.com/iotaledger/identity.rs/pull/1245)
-- Allow custom JWT claims for presentations [#1244](https://github.com/iotaledger/identity.rs/pull/1244)
-- Allow custom `kid` to be set in JWS [#1239](https://github.com/iotaledger/identity.rs/pull/1239)
-- Allow custom JWT claims for credentials [#1237](https://github.com/iotaledger/identity.rs/pull/1237)
-- Improve `Proof` [#1209](https://github.com/iotaledger/identity.rs/pull/1209)
-- Add `resolve_multiple` to Resolver [#1189](https://github.com/iotaledger/identity.rs/pull/1189)
-- Move jwk_storage and key_id_storage to Wasm lib [#1181](https://github.com/iotaledger/identity.rs/pull/1181)
-- Wasm Bindings for JWT Presentations [#1179](https://github.com/iotaledger/identity.rs/pull/1179)
-- Polish JWK thumbprint and document extension API [#1173](https://github.com/iotaledger/identity.rs/pull/1173)
-- Wasm bindings for `KeyIdStorage` [#1147](https://github.com/iotaledger/identity.rs/pull/1147)
-- Introduce `IToCoreDocument` and document locks in the bindings [#1120](https://github.com/iotaledger/identity.rs/pull/1120)
-- Add Wasm Bindings for Domain Linkage [#1115](https://github.com/iotaledger/identity.rs/pull/1115)
-- Add wasm credentials and presentations examples [#1075](https://github.com/iotaledger/identity.rs/pull/1075)
-- Add revocation examples [#1076](https://github.com/iotaledger/identity.rs/pull/1076)
-- Add `IotaDID.fromAliasId` to the Wasm bindings [\#1048](https://github.com/iotaledger/identity.rs/pull/1048)
-- Expose Controller and Governor Addresses in metadata [\#1023](https://github.com/iotaledger/identity.rs/pull/1023)
-- Add Wasm bindings for `CoreDocument` [\#994](https://github.com/iotaledger/identity.rs/pull/994)
-- Add initial Wasm Stardust bindings [\#967](https://github.com/iotaledger/identity.rs/pull/967)
+- Add ZK BBS+-based selectively disclosable credentials (JPT) [\#1355](https://github.com/iotaledger/identity.rs/pull/1355)
+- Add EcDSA verifier [\#1353](https://github.com/iotaledger/identity.rs/pull/1353)
### Patch
+- Support for specification-compliant verification method type `JsonWebKey2020` [\#1367](https://github.com/iotaledger/identity.rs/pull/1367)
-- Fix wasm panic caused by a race condition in `IotaDocument` and `CoreDocument` [#1258](https://github.com/iotaledger/identity.rs/pull/1258)
-- Fix holder claim check in VP [#1236](https://github.com/iotaledger/identity.rs/pull/1236)
-- Fix issuer claim check in VC [#1235](https://github.com/iotaledger/identity.rs/pull/1235)
-- Fix clippy's issue `uninlined-format-args` [#1109](https://github.com/iotaledger/identity.rs/pull/1109)
-- Update iota.js peer dependency [#1107](https://github.com/iotaledger/identity.rs/pull/1107)
-- Fix unresolved import in TS artifacts [\#1066](https://github.com/iotaledger/identity.rs/pull/1066)
-- Recommend unique `credentialStatus.id` in `RevocationBitmap2022` [\#1039](https://github.com/iotaledger/identity.rs/pull/1039)
-- Support case insensitive serialization of `RentStructure` [\#1012](https://github.com/iotaledger/identity.rs/pull/1012)
-- Fix broken wasm bindings compilation [\#995](https://github.com/iotaledger/identity.rs/pull/995)
-- Fix DID TypeScript references [\#977](https://github.com/iotaledger/identity.rs/pull/977)
+## [wasm-v1.2.0](https://github.com/iotaledger/identity.rs/tree/wasm-v1.2.0) (2024-03-27)
-## [wasm-v1.0.0-rc.1](https://github.com/iotaledger/identity.rs/tree/wasm-v1.0.0-rc.1) (2023-09-29)
-
-[Full Changelog](https://github.com/iotaledger/identity.rs/compare/wasm-v0.6.0...wasm-v1.0.0-rc.1)
-
-This version introduces a new DID method targeting the IOTA UTXO ledger. This method works fundamentally differently from the previous method and introduces new capabilities to interact with Layer 1 assets like Native Tokens, NFTs and various Output types.
-
-This version changes the credential and presentation format to JWT, as specified by the [VC Data Model v1.1](https://www.w3.org/TR/vc-data-model/#json-web-token).
-
-Note: Identities and credentials created with the earlier versions cannot be resolved with this version of the library.
-
-### Changed
-
-- Add dedicated EdDSA verifier crate [#1238](https://github.com/iotaledger/identity.rs/pull/1238)
-- Use `VC Data Model v1.1` JWT encoding instead of `VC-JWT` [#1234](https://github.com/iotaledger/identity.rs/pull/1234)
-- Change `verifiable_credential` to type `Vec` in `Presentation` [#1231](https://github.com/iotaledger/identity.rs/pull/1231)
-- Polish Wasm bindings [#1206](https://github.com/iotaledger/identity.rs/pull/1206)
-- Polish `identity_credential` [#1205](https://github.com/iotaledger/identity.rs/pull/1205)
-- Polish `identity_iota_core` [#1203](https://github.com/iotaledger/identity.rs/pull/1203)
-- Upgrade `client-wasm` to `sdk-wasm` [#1202](https://github.com/iotaledger/identity.rs/pull/1202)
-- Rename `JwtPresentation` to `Presentation` [#1200](https://github.com/iotaledger/identity.rs/pull/1200)
-- Remove legacy signing and verification APIs [#1194](https://github.com/iotaledger/identity.rs/pull/1194)
-- Remove old `Presentation` type [#1190](https://github.com/iotaledger/identity.rs/pull/1190)
-- Remove reexported `Resolver` validation APIs [#1183](https://github.com/iotaledger/identity.rs/pull/1183)
-- Use JWT credentials for Domain Linkage [#1180](https://github.com/iotaledger/identity.rs/pull/1180)
-- Remove stronghold nodejs bindings [#1178](https://github.com/iotaledger/identity.rs/pull/1178)
-- JwkStorageDocument & JwtCredential validation [#1152](https://github.com/iotaledger/identity.rs/pull/1152)
-- Add initial PublicKeyJwk support [#1143](https://github.com/iotaledger/identity.rs/pull/1143)
-- Refactor `MethodType` to make it extensible [#1112](https://github.com/iotaledger/identity.rs/pull/1112)
-- Remove generics in `CoreDocument`, `VerificationMethod`, `Service`, `DIDUrl` and `LinkedDomainService` [#1110](https://github.com/iotaledger/identity.rs/pull/1110)
-- Use official client-wasm dependency in examples [#1097](https://github.com/iotaledger/identity.rs/pull/1097)
-- More identifier checks in `CoreDocument` [#1067](https://github.com/iotaledger/identity.rs/pull/1067)
-- Update to `iota-client` 2.0.1-rc.4 and `iota-client-wasm` 0.5.0-alpha.6 [#1088](https://github.com/iotaledger/identity.rs/pull/1088)
-- Use Bech32-encoded state controller and governor addresses [\#1044](https://github.com/iotaledger/identity.rs/pull/1044)
-- Expose iteration over verification relationship fields [\#1024](https://github.com/iotaledger/identity.rs/pull/1024)
-- Chore/rename mixed resolver [\#1026](https://github.com/iotaledger/identity.rs/pull/1026)
-- Add length prefix to DID Document payloads [\#1010](https://github.com/iotaledger/identity.rs/pull/1010)
-- Update Wasm credential, presentation validators for Stardust [\#1004](https://github.com/iotaledger/identity.rs/pull/1004)
-- Rename `Stardust` types to `Iota` [\#1000](https://github.com/iotaledger/identity.rs/pull/1000)
-- Change Stardust DID method to IOTA [\#982](https://github.com/iotaledger/identity.rs/pull/982)
-- Add Wasm Stardust Client [\#975](https://github.com/iotaledger/identity.rs/pull/975)
-- Generalized Resolver [\#970](https://github.com/iotaledger/identity.rs/pull/970)
-- Change `Storage` to handle `CoreDID` [\#968](https://github.com/iotaledger/identity.rs/pull/968)
-- Change `Storage` to store arbitrary blobs [\#953](https://github.com/iotaledger/identity.rs/pull/953)
-- Change `Service` `type` field to allow sets [\#944](https://github.com/iotaledger/identity.rs/pull/944)
-- Generalise `CredentialValidator`, `PresentationValidator` to support arbitrary DID Documents [\#935](https://github.com/iotaledger/identity.rs/pull/935)
+[Full Changelog](https://github.com/iotaledger/identity.rs/compare/wasm-v1.1.0...wasm-v1.2.0)
### Added
-- Allow arbitrary JWS header parameters [#1245](https://github.com/iotaledger/identity.rs/pull/1245)
-- Allow custom JWT claims for presentations [#1244](https://github.com/iotaledger/identity.rs/pull/1244)
-- Allow custom `kid` to be set in JWS [#1239](https://github.com/iotaledger/identity.rs/pull/1239)
-- Allow custom JWT claims for credentials [#1237](https://github.com/iotaledger/identity.rs/pull/1237)
-- Improve `Proof` [#1209](https://github.com/iotaledger/identity.rs/pull/1209)
-- Add `resolve_multiple` to Resolver [#1189](https://github.com/iotaledger/identity.rs/pull/1189)
-- Move jwk_storage and key_id_storage to Wasm lib [#1181](https://github.com/iotaledger/identity.rs/pull/1181)
-- Wasm Bindings for JWT Presentations [#1179](https://github.com/iotaledger/identity.rs/pull/1179)
-- Polish JWK thumbprint and document extension API [#1173](https://github.com/iotaledger/identity.rs/pull/1173)
-- Wasm bindings for `KeyIdStorage` [#1147](https://github.com/iotaledger/identity.rs/pull/1147)
-- Introduce `IToCoreDocument` and document locks in the bindings [#1120](https://github.com/iotaledger/identity.rs/pull/1120)
-- Add Wasm Bindings for Domain Linkage [#1115](https://github.com/iotaledger/identity.rs/pull/1115)
-- Add wasm credentials and presentations examples [#1075](https://github.com/iotaledger/identity.rs/pull/1075)
-- Add revocation examples [#1076](https://github.com/iotaledger/identity.rs/pull/1076)
-- Add `IotaDID.fromAliasId` to the Wasm bindings [\#1048](https://github.com/iotaledger/identity.rs/pull/1048)
-- Expose Controller and Governor Addresses in metadata [\#1023](https://github.com/iotaledger/identity.rs/pull/1023)
-- Add Wasm bindings for `CoreDocument` [\#994](https://github.com/iotaledger/identity.rs/pull/994)
-- Add initial Wasm Stardust bindings [\#967](https://github.com/iotaledger/identity.rs/pull/967)
+- Allow arbitrary verification methods [\#1334](https://github.com/iotaledger/identity.rs/pull/1334)
+- use latest release of sd-jwt-payload [\#1333](https://github.com/iotaledger/identity.rs/pull/1333)
+- Add constructor for `VerificationMethod` in TS [\#1321](https://github.com/iotaledger/identity.rs/pull/1321)
+- Allow setting additional controllers for `IotaDocument` [\#1314](https://github.com/iotaledger/identity.rs/pull/1314)
### Patch
-- Fix holder claim check in VP [#1236](https://github.com/iotaledger/identity.rs/pull/1236)
-- Fix issuer claim check in VC [#1235](https://github.com/iotaledger/identity.rs/pull/1235)
-- Fix clippy's issue `uninlined-format-args` [#1109](https://github.com/iotaledger/identity.rs/pull/1109)
-- Update iota.js peer dependency [#1107](https://github.com/iotaledger/identity.rs/pull/1107)
-- Fix unresolved import in TS artifacts [\#1066](https://github.com/iotaledger/identity.rs/pull/1066)
-- Recommend unique `credentialStatus.id` in `RevocationBitmap2022` [\#1039](https://github.com/iotaledger/identity.rs/pull/1039)
-- Support case insensitive serialization of `RentStructure` [\#1012](https://github.com/iotaledger/identity.rs/pull/1012)
-- Fix broken wasm bindings compilation [\#995](https://github.com/iotaledger/identity.rs/pull/995)
-- Fix DID TypeScript references [\#977](https://github.com/iotaledger/identity.rs/pull/977)
-
-## [wasm-v0.7.0-alpha.7](https://github.com/iotaledger/identity.rs/tree/wasm-v0.7.0-alpha.7) (2023-09-28)
+- Support %-encoded characters in DID method id [\#1303](https://github.com/iotaledger/identity.rs/pull/1303)
-[Full Changelog](https://github.com/iotaledger/identity.rs/compare/wasm-v0.7.0-alpha.6...wasm-v0.7.0-alpha.7)
-
-### Changed
-- Use `VC Data Model v1.1` JWT encoding instead of `VC-JWT` [#1234](https://github.com/iotaledger/identity.rs/pull/1234)
-- Change `verifiable_credential` to type `Vec` in `Presentation` [#1231](https://github.com/iotaledger/identity.rs/pull/1231)
+## [wasm-v1.1.0](https://github.com/iotaledger/identity.rs/tree/wasm-v1.1.0) (2024-02-07)
+[Full Changelog](https://github.com/iotaledger/identity.rs/compare/wasm-v1.0.0...wasm-v1.1.0)
### Added
-- Allow arbitrary JWS header parameters [#1245](https://github.com/iotaledger/identity.rs/pull/1245)
-- Allow custom JWT claims for presentations [#1244](https://github.com/iotaledger/identity.rs/pull/1244)
-- Allow custom `kid` to be set in JWS [#1239](https://github.com/iotaledger/identity.rs/pull/1239)
-- Allow custom JWT claims for credentials [#1237](https://github.com/iotaledger/identity.rs/pull/1237)
+- Update `sd-jwt-payload` dependency [\#1296](https://github.com/iotaledger/identity.rs/pull/1296)
+- Add support for StatusList2021 [\#1273](https://github.com/iotaledger/identity.rs/pull/1273)
+- Support Selective Disclosure SD-JWT [\#1268](https://github.com/iotaledger/identity.rs/pull/1268)
### Patch
-- Fix holder claim check in VP [#1236](https://github.com/iotaledger/identity.rs/pull/1236)
-- Fix issuer claim check in VC [#1235](https://github.com/iotaledger/identity.rs/pull/1235)
-
-## [wasm-v0.7.0-alpha.6](https://github.com/iotaledger/identity.rs/tree/wasm-v0.7.0-alpha.6) (2023-08-15)
-[Full Changelog](https://github.com/iotaledger/identity.rs/compare/wasm-v0.7.0-alpha.5...wasm-v0.7.0-alpha.6)
-
-### Added
-
-- Improve `Proof` [#1209](https://github.com/iotaledger/identity.rs/pull/1209)
-- Add `resolve_multiple` to Resolver [#1189](https://github.com/iotaledger/identity.rs/pull/1189)
-- Move jwk_storage and key_id_storage to Wasm lib [#1181](https://github.com/iotaledger/identity.rs/pull/1181)
-- Wasm Bindings for JWT Presentations [#1179](https://github.com/iotaledger/identity.rs/pull/1179)
-- Polish JWK thumbprint and document extension API [#1173](https://github.com/iotaledger/identity.rs/pull/1173)
-- Wasm bindings for `KeyIdStorage` [#1147](https://github.com/iotaledger/identity.rs/pull/1147)
-- Introduce `IToCoreDocument` and document locks in the bindings [#1120](https://github.com/iotaledger/identity.rs/pull/1120)
-- Add Wasm Bindings for Domain Linkage [#1115](https://github.com/iotaledger/identity.rs/pull/1115)
-
-### Changed
-
-- Polish Wasm bindings [#1206](https://github.com/iotaledger/identity.rs/pull/1206)
-- Polish `identity_credential` [#1205](https://github.com/iotaledger/identity.rs/pull/1205)
-- Polish `identity_iota_core` [#1203](https://github.com/iotaledger/identity.rs/pull/1203)
-- Upgrade `client-wasm` to `sdk-wasm` [#1202](https://github.com/iotaledger/identity.rs/pull/1202)
-- Rename `JwtPresentation` to `Presentation` [#1200](https://github.com/iotaledger/identity.rs/pull/1200)
-- Remove legacy signing and verification APIs [#1194](https://github.com/iotaledger/identity.rs/pull/1194)
-- Remove old `Presentation` type [#1190](https://github.com/iotaledger/identity.rs/pull/1190)
-- Remove reexported `Resolver` validation APIs [#1183](https://github.com/iotaledger/identity.rs/pull/1183)
-- Use JWT credentials for Domain Linkage [#1180](https://github.com/iotaledger/identity.rs/pull/1180)
-- Remove stronghold nodejs bindings [#1178](https://github.com/iotaledger/identity.rs/pull/1178)
-- JwkStorageDocument & JwtCredential validation [#1152](https://github.com/iotaledger/identity.rs/pull/1152)
-- Add initial PublicKeyJwk support [#1143](https://github.com/iotaledger/identity.rs/pull/1143)
-- Refactor `MethodType` to make it extensible [#1112](https://github.com/iotaledger/identity.rs/pull/1112)
-- Remove generics in `CoreDocument`, `VerificationMethod`, `Service`, `DIDUrl` and `LinkedDomainService` [#1110](https://github.com/iotaledger/identity.rs/pull/1110)
-
-### Patch
-
-- Fix clippy's issue `uninlined-format-args` [#1109](https://github.com/iotaledger/identity.rs/pull/1109)
-- Update iota.js peer dependency [#1107](https://github.com/iotaledger/identity.rs/pull/1107)
-
-## [wasm-v0.7.0-alpha.5](https://github.com/iotaledger/identity.rs/tree/wasm-v0.7.0-alpha.5) (2023-01-24)
-
-[Full Changelog](https://github.com/iotaledger/identity.rs/compare/wasm-v0.7.0-alpha.4...wasm-v0.7.0-alpha.5)
-
-### Changed
-- Use official client-wasm dependency in examples [#1097](https://github.com/iotaledger/identity.rs/pull/1097)
-
-## [wasm-v0.7.0-alpha.4](https://github.com/iotaledger/identity.rs/tree/wasm-v0.7.0-alpha.4) (2022-11-24)
-
-[Full Changelog](https://github.com/iotaledger/identity.rs/compare/wasm-v0.7.0-alpha.3...wasm-v0.7.0-alpha.4)
-
-### Added
-- Add wasm credentials and presentations examples [#1075](https://github.com/iotaledger/identity.rs/pull/1075)
-- Add revocation examples [#1076](https://github.com/iotaledger/identity.rs/pull/1076)
-
-### Changed
-- More identifier checks in `CoreDocument` [#1067](https://github.com/iotaledger/identity.rs/pull/1067)
-- Update to `iota-client` 2.0.1-rc.4 and `iota-client-wasm` 0.5.0-alpha.6 [#1088](https://github.com/iotaledger/identity.rs/pull/1088)
-
-## [wasm-v0.7.0-alpha.3](https://github.com/iotaledger/identity.rs/tree/wasm-v0.7.0-alpha.3) (2022-11-01)
-
-[Full Changelog](https://github.com/iotaledger/identity.rs/compare/wasm-v0.7.0-alpha.2...wasm-v0.7.0-alpha.3)
-
-### Added
-
-- Add `IotaDID.fromAliasId` to the Wasm bindings [\#1048](https://github.com/iotaledger/identity.rs/pull/1048)
-
-### Patch
-
-- Fix unresolved import in TS artifacts [\#1066](https://github.com/iotaledger/identity.rs/pull/1066)
+- Fix RevocationBitmap2022 encoding bug [\#1292](https://github.com/iotaledger/identity.rs/pull/1292)
+- Credentials cannot be unrevoked with StatusList2021 [\#1284](https://github.com/iotaledger/identity.rs/pull/1284)
+- Validate domain-linkage URL making sure they only include an origin [\#1267](https://github.com/iotaledger/identity.rs/pull/1267)
+## [wasm-v1.0.0](https://github.com/iotaledger/identity.rs/tree/wasm-v1.0.0) (2023-11-02)
-## [wasm-v0.7.0-alpha.2](https://github.com/iotaledger/identity.rs/tree/wasm-v0.7.0-alpha.2) (2022-09-30)
+[Full Changelog](https://github.com/iotaledger/identity.rs/compare/wasm-v0.6.0...wasm-v1.0.0)
-[Full Changelog](https://github.com/iotaledger/identity.rs/compare/wasm-v0.7.0-alpha.1...wasm-v0.7.0-alpha.2)
-
### Changed
+- Allow custom `kid` to be set in JWS [\#1239](https://github.com/iotaledger/identity.rs/pull/1239)
+- Add dedicated EdDSA verifier crate [\#1238](https://github.com/iotaledger/identity.rs/pull/1238)
+- Change `verifiable_credential` to type `Vec` in `Presentation` [\#1231](https://github.com/iotaledger/identity.rs/pull/1231)
+- Polish Wasm bindings [\#1206](https://github.com/iotaledger/identity.rs/pull/1206)
+- Polish `identity_credential` [\#1205](https://github.com/iotaledger/identity.rs/pull/1205)
+- Polish `identity_iota_core` [\#1203](https://github.com/iotaledger/identity.rs/pull/1203)
+- Upgrade `client-wasm` to `sdk-wasm` [\#1202](https://github.com/iotaledger/identity.rs/pull/1202)
+- Rename `JwtPresentation` to `Presentation` [\#1200](https://github.com/iotaledger/identity.rs/pull/1200)
+- Remove legacy signing and verification APIs [\#1194](https://github.com/iotaledger/identity.rs/pull/1194)
+- Remove old `Presentation` type [\#1190](https://github.com/iotaledger/identity.rs/pull/1190)
+- Remove reexported `Resolver` validation APIs [\#1183](https://github.com/iotaledger/identity.rs/pull/1183)
+- Use JWT credentials for Domain Linkage [\#1180](https://github.com/iotaledger/identity.rs/pull/1180)
+- Remove stronghold nodejs bindings [\#1178](https://github.com/iotaledger/identity.rs/pull/1178)
+- JwkStorageDocument & JwtCredential validation [\#1152](https://github.com/iotaledger/identity.rs/pull/1152)
+- Add initial PublicKeyJwk support [\#1143](https://github.com/iotaledger/identity.rs/pull/1143)
+- Refactor `MethodType` to make it extensible [\#1112](https://github.com/iotaledger/identity.rs/pull/1112)
+- Remove generics in `CoreDocument`, `VerificationMethod`, `Service`, `DIDUrl` and `LinkedDomainService` [\#1110](https://github.com/iotaledger/identity.rs/pull/1110)
+- Update to `iota-client` 2.0.1-rc.4 and `iota-client-wasm` 0.5.0-alpha.6 [\#1088](https://github.com/iotaledger/identity.rs/pull/1088)
+- More identifier checks in `CoreDocument` [\#1067](https://github.com/iotaledger/identity.rs/pull/1067)
- Use Bech32-encoded state controller and governor addresses [\#1044](https://github.com/iotaledger/identity.rs/pull/1044)
+- Rename `MixedResolver` to `Resolver` in Wasm [\#1026](https://github.com/iotaledger/identity.rs/pull/1026)
- Expose iteration over verification relationship fields [\#1024](https://github.com/iotaledger/identity.rs/pull/1024)
-
-### Added
-
-- Expose Controller and Governor Addresses in metadata [\#1023](https://github.com/iotaledger/identity.rs/pull/1023)
-
-### Patch
-
-- Recommend unique `credentialStatus.id` in `RevocationBitmap2022` [\#1039](https://github.com/iotaledger/identity.rs/pull/1039)
-
-## [wasm-v0.7.0-alpha.1](https://github.com/iotaledger/identity.rs/tree/wasm-v0.7.0-alpha.1) (2022-09-16)
-
-[Full Changelog](https://github.com/iotaledger/identity.rs/compare/wasm-v0.6.0...wasm-v0.7.0-alpha.1)
-
-This version introduces a new DID method targeting the IOTA UTXO ledger. This method works fundamentally differently from the previous method and introduces new capabilities to interact with Layer 1 entities like native tokens, NFTs and smart contracts.
-
- This is an early alpha release, so there may be breaking changes in upcoming versions that invalidate DIDs created with this version. The method at this point is only intended for experimentation.
-
- Note: Identities created with the earlier versions cannot be resolved with this version of the library.
-
-
-
-### Changed
-
-- Chore/rename mixed resolver [\#1026](https://github.com/iotaledger/identity.rs/pull/1026)
- Add length prefix to DID Document payloads [\#1010](https://github.com/iotaledger/identity.rs/pull/1010)
- Update Wasm credential, presentation validators for Stardust [\#1004](https://github.com/iotaledger/identity.rs/pull/1004)
- Rename `Stardust` types to `Iota` [\#1000](https://github.com/iotaledger/identity.rs/pull/1000)
@@ -293,11 +83,33 @@ This version introduces a new DID method targeting the IOTA UTXO ledger. This me
### Added
+- Allow arbitrary JWS header parameters [\#1245](https://github.com/iotaledger/identity.rs/pull/1245)
+- Allow custom JWT claims for presentations [\#1244](https://github.com/iotaledger/identity.rs/pull/1244)
+- Allow custom JWT claims for credentials [\#1237](https://github.com/iotaledger/identity.rs/pull/1237)
+- Use `VC Data Model v1.1` JWT encoding instead of `VC-JWT` [\#1234](https://github.com/iotaledger/identity.rs/pull/1234)
+- Improve `Proof` [\#1209](https://github.com/iotaledger/identity.rs/pull/1209)
+- Add `resolve_multiple` to Resolver [\#1189](https://github.com/iotaledger/identity.rs/pull/1189)
+- Move jwk\_storage and key\_id\_storage to Wasm lib [\#1181](https://github.com/iotaledger/identity.rs/pull/1181)
+- Wasm Bindings for JWT Presentations [\#1179](https://github.com/iotaledger/identity.rs/pull/1179)
+- Polish JWK thumbprint and document extension API [\#1173](https://github.com/iotaledger/identity.rs/pull/1173)
+- Wasm bindings for `KeyIdStorage` [\#1147](https://github.com/iotaledger/identity.rs/pull/1147)
+- Introduce `IToCoreDocument` and document locks in the bindings [\#1120](https://github.com/iotaledger/identity.rs/pull/1120)
+- Add Wasm Bindings for Domain Linkage [\#1115](https://github.com/iotaledger/identity.rs/pull/1115)
+- Add revocation examples [\#1076](https://github.com/iotaledger/identity.rs/pull/1076)
+- Add wasm credentials and presentations examples [\#1075](https://github.com/iotaledger/identity.rs/pull/1075)
+- Add `IotaDID.fromAliasId` to the Wasm bindings [\#1048](https://github.com/iotaledger/identity.rs/pull/1048)
+- Expose Controller and Governor Addresses in metadata [\#1023](https://github.com/iotaledger/identity.rs/pull/1023)
- Add Wasm bindings for `CoreDocument` [\#994](https://github.com/iotaledger/identity.rs/pull/994)
- Add initial Wasm Stardust bindings [\#967](https://github.com/iotaledger/identity.rs/pull/967)
### Patch
+- Fix wasm panic caused by a race condition in `IotaDocument` and `CoreDocument` [\#1258](https://github.com/iotaledger/identity.rs/pull/1258)
+- Fix issuer claim check in VC [\#1235](https://github.com/iotaledger/identity.rs/pull/1235)
+- Update iota.js peer dependency [\#1107](https://github.com/iotaledger/identity.rs/pull/1107)
+- Fix unresolved import in TS artifacts [\#1066](https://github.com/iotaledger/identity.rs/pull/1066)
+- Fix `IotaDocument.unpackFromOutput` parameter type [\#1041](https://github.com/iotaledger/identity.rs/pull/1041)
+- Recommend unique `credentialStatus.id` in `RevocationBitmap2022` [\#1039](https://github.com/iotaledger/identity.rs/pull/1039)
- Support case insensitive serialization of `RentStructure` [\#1012](https://github.com/iotaledger/identity.rs/pull/1012)
- Fix broken wasm bindings compilation [\#995](https://github.com/iotaledger/identity.rs/pull/995)
- Fix DID TypeScript references [\#977](https://github.com/iotaledger/identity.rs/pull/977)
diff --git a/bindings/wasm/Cargo.toml b/bindings/wasm/Cargo.toml
index 0e67337d41..27d693e23b 100644
--- a/bindings/wasm/Cargo.toml
+++ b/bindings/wasm/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "identity_wasm"
-version = "1.0.0"
+version = "1.3.0"
authors = ["IOTA Stiftung"]
edition = "2021"
homepage = "https://www.iota.org"
@@ -21,6 +21,7 @@ console_error_panic_hook = { version = "0.1" }
futures = { version = "0.3" }
identity_eddsa_verifier = { path = "../../identity_eddsa_verifier", default-features = false, features = ["ed25519"] }
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_json = { version = "1.0", default-features = false }
@@ -29,11 +30,12 @@ serde_repr = { version = "0.1", default-features = false }
tokio = { version = "1.29", default-features = false, features = ["sync"] }
wasm-bindgen = { version = "0.2.85", features = ["serde-serialize"] }
wasm-bindgen-futures = { version = "0.4", default-features = false }
+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"]
+features = ["client", "revocation-bitmap", "resolver", "domain-linkage", "sd-jwt", "status-list-2021", "jpt-bbs-plus"]
[dev-dependencies]
rand = "0.8.5"
@@ -45,3 +47,8 @@ instant = { version = "0.1", default-features = false, features = ["wasm-bindgen
[profile.release]
opt-level = 's'
lto = true
+
+[lints.clippy]
+# can be removed as soon as fix has been added to clippy
+# see https://github.com/rust-lang/rust-clippy/issues/12377
+empty_docs = "allow"
diff --git a/bindings/wasm/README.md b/bindings/wasm/README.md
index f3b854fa37..b7cee7a287 100644
--- a/bindings/wasm/README.md
+++ b/bindings/wasm/README.md
@@ -81,7 +81,7 @@ const EXAMPLE_JWK = new Jwk({
});
// The endpoint of the IOTA node to use.
-const API_ENDPOINT = "http://127.0.0.1:14265";
+const API_ENDPOINT = "http://localhost";
/** Demonstrate how to create a DID Document. */
async function main() {
@@ -230,7 +230,7 @@ import init, { Client } from "@iota/sdk-wasm/web";
import * as identity from "@iota/identity-wasm/web";
// The endpoint of the IOTA node to use.
-const API_ENDPOINT = "http://127.0.0.1:14265";
+const API_ENDPOINT = "http://localhost";
const EXAMPLE_JWK = new identity.Jwk({
kty: identity.JwkType.Okp,
diff --git a/bindings/wasm/docs/api-reference.md b/bindings/wasm/docs/api-reference.md
index e17fcf0c2c..db03dc07ec 100644
--- a/bindings/wasm/docs/api-reference.md
+++ b/bindings/wasm/docs/api-reference.md
@@ -11,9 +11,16 @@ if the object is being concurrently modified.
Credential
+CustomMethodData
+A custom verification method data format.
+
DIDUrl
A method agnostic DID Url.
+DecodedJptCredential
+
+DecodedJptPresentation
+
DecodedJws
A cryptographically verified decoded token from a JWS.
Contains the decoded headers and the raw claims.
@@ -64,11 +71,41 @@ if the object is being concurrently modified.
An extension interface that provides helper functions for publication
and resolution of DID documents in Alias Outputs.
+IssuerProtectedHeader
+
+Jpt
+A JSON Proof Token (JPT).
+
+JptCredentialValidationOptions
+Options to declare validation criteria for Jpt .
+
+JptCredentialValidator
+
+JptCredentialValidatorUtils
+Utility functions for validating JPT credentials.
+
+JptPresentationValidationOptions
+Options to declare validation criteria for a Jpt presentation.
+
+JptPresentationValidator
+
+JptPresentationValidatorUtils
+Utility functions for verifying JPT presentations.
+
Jwk
JwkGenOutput
The result of a key generation in JwkStorage
.
+JwpCredentialOptions
+
+JwpIssued
+
+JwpPresentationOptions
+Options to be set in the JWT claims of a verifiable presentation.
+
+JwpVerificationOptions
+
Jws
A wrapper around a JSON Web Signature (JWS).
@@ -120,8 +157,14 @@ use the methods pack
and unpack
instead.
MethodType
Supported verification method types.
+PayloadEntry
+
+Payloads
+
Presentation
+PresentationProtectedHeader
+
Proof
Represents a cryptographic proof that can be used to validate verifiable credentials and
presentations.
@@ -131,6 +174,8 @@ can be utilized to implement standards or user-defined proofs. The presence of t
Note that this proof is not related to JWT and can be used in combination or as an alternative
to it.
+ProofUpdateCtx
+
Resolver
Convenience type for resolving DID documents from different DID methods.
Also provides methods for resolving DID Documents associated with
@@ -141,6 +186,9 @@ verifiable Credential s and Pre
RevocationBitmap
A compressed bitmap for managing credential revocation.
+RevocationTimeframeStatus
+Information used to determine the current status of a Credential .
+
SdJwt
Representation of an SD-JWT of the format
<Issuer-signed JWT>~<Disclosure 1>~<Disclosure 2>~...~<Disclosure N>~<optional KB-JWT>
.
@@ -156,6 +204,23 @@ verifiable Credential s and Pre
with their corresponding disclosure digests.
Note: digests are created using the sha-256 algorithm.
+SelectiveDisclosurePresentation
+Used to construct a JwpPresentedBuilder and handle the selective disclosure of attributes
+
+@context MUST NOT be blinded
+id MUST be blinded
+type MUST NOT be blinded
+issuer MUST NOT be blinded
+issuanceDate MUST be blinded (if Timeframe Revocation mechanism is used)
+expirationDate MUST be blinded (if Timeframe Revocation mechanism is used)
+credentialSubject (User have to choose which attribute must be blinded)
+credentialSchema MUST NOT be blinded
+credentialStatus MUST NOT be blinded
+refreshService MUST NOT be blinded (probably will be used for Timeslot Revocation mechanism)
+termsOfUse NO reason to use it in ZK VC (will be in any case blinded)
+evidence (User have to choose which attribute must be blinded)
+
+
Service
A DID Document Service used to enable trusted interactions associated with a DID subject.
@@ -187,11 +252,31 @@ working with storage backed DID documents.
## Members
-StateMetadataEncoding
+PresentationProofAlgorithm
-StatusPurpose
-Purpose of a StatusList2021 .
+ProofAlgorithm
+
+StatusCheck
+Controls validation behaviour when checking whether or not a credential has been revoked by its
+credentialStatus
.
+
+Strict
+Validate the status if supported, reject any unsupported
+credentialStatus
types.
+Only RevocationBitmap2022
is currently supported.
+This is the default.
+
+SkipUnsupported
+Validate the status if supported, skip any unsupported
+credentialStatus
types.
+SkipAll
+Skip all status checks.
+
+SerializationType
+
+MethodRelationship
+
SubjectHolderRelationship
Declares how credential subjects must relate to the presentation holder.
See also the Subject-Holder Relationship section of the specification.
@@ -206,6 +291,13 @@ This variant is the default.
Any
The holder is not required to have any kind of relationship to any credential subject.
+CredentialStatus
+
+StatusPurpose
+Purpose of a StatusList2021 .
+
+StateMetadataEncoding
+
FailFast
Declares when validation should return if an error occurs.
@@ -215,6 +307,12 @@ This variant is the default.
FirstError
Return after the first error occurs.
+PayloadType
+
+MethodRelationship
+
+CredentialStatus
+
StatusCheck
Controls validation behaviour when checking whether or not a credential has been revoked by its
credentialStatus
.
@@ -232,10 +330,6 @@ This variant is the default.
SkipAll
Skip all status checks.
-CredentialStatus
-
-MethodRelationship
-
## Functions
@@ -249,6 +343,9 @@ This variant is the default.
This function does not check whether alg = EdDSA
in the protected header. Callers are expected to assert this
prior to calling the function.
+start()
+Initializes the console error panic hook for better error messages
+
encodeB64(data) ⇒ string
Encode the given bytes in url-safe base64.
@@ -1138,6 +1235,53 @@ Deserializes an instance from a JSON object.
| --- | --- |
| json | any
|
+
+
+## CustomMethodData
+A custom verification method data format.
+
+**Kind**: global class
+
+* [CustomMethodData](#CustomMethodData)
+ * [new CustomMethodData(name, data)](#new_CustomMethodData_new)
+ * _instance_
+ * [.clone()](#CustomMethodData+clone) ⇒ [CustomMethodData
](#CustomMethodData)
+ * [.toJSON()](#CustomMethodData+toJSON) ⇒ any
+ * _static_
+ * [.fromJSON(json)](#CustomMethodData.fromJSON) ⇒ [CustomMethodData
](#CustomMethodData)
+
+
+
+### new CustomMethodData(name, data)
+
+| Param | Type |
+| --- | --- |
+| name | string
|
+| data | any
|
+
+
+
+### customMethodData.clone() ⇒ [CustomMethodData
](#CustomMethodData)
+Deep clones the object.
+
+**Kind**: instance method of [CustomMethodData
](#CustomMethodData)
+
+
+### customMethodData.toJSON() ⇒ any
+Serializes this to a JSON object.
+
+**Kind**: instance method of [CustomMethodData
](#CustomMethodData)
+
+
+### CustomMethodData.fromJSON(json) ⇒ [CustomMethodData
](#CustomMethodData)
+Deserializes an instance from a JSON object.
+
+**Kind**: static method of [CustomMethodData
](#CustomMethodData)
+
+| Param | Type |
+| --- | --- |
+| json | any
|
+
## DIDUrl
@@ -1285,6 +1429,74 @@ Deserializes an instance from a JSON object.
| --- | --- |
| json | any
|
+
+
+## DecodedJptCredential
+**Kind**: global class
+
+* [DecodedJptCredential](#DecodedJptCredential)
+ * [.clone()](#DecodedJptCredential+clone) ⇒ [DecodedJptCredential
](#DecodedJptCredential)
+ * [.credential()](#DecodedJptCredential+credential) ⇒ [Credential
](#Credential)
+ * [.customClaims()](#DecodedJptCredential+customClaims) ⇒ Map.<string, any>
+ * [.decodedJwp()](#DecodedJptCredential+decodedJwp) ⇒ [JwpIssued
](#JwpIssued)
+
+
+
+### decodedJptCredential.clone() ⇒ [DecodedJptCredential
](#DecodedJptCredential)
+Deep clones the object.
+
+**Kind**: instance method of [DecodedJptCredential
](#DecodedJptCredential)
+
+
+### decodedJptCredential.credential() ⇒ [Credential
](#Credential)
+Returns the [Credential](#Credential) embedded into this JPT.
+
+**Kind**: instance method of [DecodedJptCredential
](#DecodedJptCredential)
+
+
+### decodedJptCredential.customClaims() ⇒ Map.<string, any>
+Returns the custom claims parsed from the JPT.
+
+**Kind**: instance method of [DecodedJptCredential
](#DecodedJptCredential)
+
+
+### decodedJptCredential.decodedJwp() ⇒ [JwpIssued
](#JwpIssued)
+**Kind**: instance method of [DecodedJptCredential
](#DecodedJptCredential)
+
+
+## DecodedJptPresentation
+**Kind**: global class
+
+* [DecodedJptPresentation](#DecodedJptPresentation)
+ * [.clone()](#DecodedJptPresentation+clone) ⇒ [DecodedJptPresentation
](#DecodedJptPresentation)
+ * [.credential()](#DecodedJptPresentation+credential) ⇒ [Credential
](#Credential)
+ * [.customClaims()](#DecodedJptPresentation+customClaims) ⇒ Map.<string, any>
+ * [.aud()](#DecodedJptPresentation+aud) ⇒ string
\| undefined
+
+
+
+### decodedJptPresentation.clone() ⇒ [DecodedJptPresentation
](#DecodedJptPresentation)
+Deep clones the object.
+
+**Kind**: instance method of [DecodedJptPresentation
](#DecodedJptPresentation)
+
+
+### decodedJptPresentation.credential() ⇒ [Credential
](#Credential)
+Returns the [Credential](#Credential) embedded into this JPT.
+
+**Kind**: instance method of [DecodedJptPresentation
](#DecodedJptPresentation)
+
+
+### decodedJptPresentation.customClaims() ⇒ Map.<string, any>
+Returns the custom claims parsed from the JPT.
+
+**Kind**: instance method of [DecodedJptPresentation
](#DecodedJptPresentation)
+
+
+### decodedJptPresentation.aud() ⇒ string
\| undefined
+Returns the `aud` property parsed from the JWT claims.
+
+**Kind**: instance method of [DecodedJptPresentation
](#DecodedJptPresentation)
## DecodedJws
@@ -1967,6 +2179,7 @@ if the object is being concurrently modified.
* _instance_
* [.id()](#IotaDocument+id) ⇒ [IotaDID
](#IotaDID)
* [.controller()](#IotaDocument+controller) ⇒ [Array.<IotaDID>
](#IotaDID)
+ * [.setController(controller)](#IotaDocument+setController)
* [.alsoKnownAs()](#IotaDocument+alsoKnownAs) ⇒ Array.<string>
* [.setAlsoKnownAs(urls)](#IotaDocument+setAlsoKnownAs)
* [.properties()](#IotaDocument+properties) ⇒ Map.<string, any>
@@ -2007,6 +2220,11 @@ if the object is being concurrently modified.
* [.createJws(storage, fragment, payload, options)](#IotaDocument+createJws) ⇒ [Promise.<Jws>
](#Jws)
* [.createCredentialJwt(storage, fragment, credential, options, [custom_claims])](#IotaDocument+createCredentialJwt) ⇒ [Promise.<Jwt>
](#Jwt)
* [.createPresentationJwt(storage, fragment, presentation, signature_options, presentation_options)](#IotaDocument+createPresentationJwt) ⇒ [Promise.<Jwt>
](#Jwt)
+ * [.generateMethodJwp(storage, alg, fragment, scope)](#IotaDocument+generateMethodJwp) ⇒ Promise.<string>
+ * [.createIssuedJwp(storage, fragment, jpt_claims, options)](#IotaDocument+createIssuedJwp) ⇒ Promise.<string>
+ * [.createPresentedJwp(presentation, method_id, options)](#IotaDocument+createPresentedJwp) ⇒ Promise.<string>
+ * [.createCredentialJpt(credential, storage, fragment, options, [custom_claims])](#IotaDocument+createCredentialJpt) ⇒ [Promise.<Jpt>
](#Jpt)
+ * [.createPresentationJpt(presentation, method_id, options)](#IotaDocument+createPresentationJpt) ⇒ [Promise.<Jpt>
](#Jpt)
* _static_
* [.newWithId(id)](#IotaDocument.newWithId) ⇒ [IotaDocument
](#IotaDocument)
* [.unpackFromOutput(did, aliasOutput, allowEmpty)](#IotaDocument.unpackFromOutput) ⇒ [IotaDocument
](#IotaDocument)
@@ -2039,6 +2257,20 @@ NOTE: controllers are determined by the `state_controller` unlock condition of t
during resolution and are omitted when publishing.
**Kind**: instance method of [IotaDocument
](#IotaDocument)
+
+
+### iotaDocument.setController(controller)
+Sets the controllers of the document.
+
+Note: Duplicates will be ignored.
+Use `null` to remove all controllers.
+
+**Kind**: instance method of [IotaDocument
](#IotaDocument)
+
+| Param | Type |
+| --- | --- |
+| controller | [Array.<IotaDID>
](#IotaDID) \| null
|
+
### iotaDocument.alsoKnownAs() ⇒ Array.<string>
@@ -2498,6 +2730,65 @@ private key backed by the `storage` in accordance with the passed `options`.
| signature_options | [JwsSignatureOptions
](#JwsSignatureOptions) |
| presentation_options | [JwtPresentationOptions
](#JwtPresentationOptions) |
+
+
+### iotaDocument.generateMethodJwp(storage, alg, fragment, scope) ⇒ Promise.<string>
+**Kind**: instance method of [IotaDocument
](#IotaDocument)
+
+| Param | Type |
+| --- | --- |
+| storage | [Storage
](#Storage) |
+| alg | [ProofAlgorithm
](#ProofAlgorithm) |
+| fragment | string
\| undefined
|
+| scope | [MethodScope
](#MethodScope) |
+
+
+
+### iotaDocument.createIssuedJwp(storage, fragment, jpt_claims, options) ⇒ Promise.<string>
+**Kind**: instance method of [IotaDocument
](#IotaDocument)
+
+| Param | Type |
+| --- | --- |
+| storage | [Storage
](#Storage) |
+| fragment | string
|
+| jpt_claims | JptClaims
|
+| options | [JwpCredentialOptions
](#JwpCredentialOptions) |
+
+
+
+### iotaDocument.createPresentedJwp(presentation, method_id, options) ⇒ Promise.<string>
+**Kind**: instance method of [IotaDocument
](#IotaDocument)
+
+| Param | Type |
+| --- | --- |
+| presentation | [SelectiveDisclosurePresentation
](#SelectiveDisclosurePresentation) |
+| method_id | string
|
+| options | [JwpPresentationOptions
](#JwpPresentationOptions) |
+
+
+
+### iotaDocument.createCredentialJpt(credential, storage, fragment, options, [custom_claims]) ⇒ [Promise.<Jpt>
](#Jpt)
+**Kind**: instance method of [IotaDocument
](#IotaDocument)
+
+| Param | Type |
+| --- | --- |
+| credential | [Credential
](#Credential) |
+| storage | [Storage
](#Storage) |
+| fragment | string
|
+| options | [JwpCredentialOptions
](#JwpCredentialOptions) |
+| [custom_claims] | Map.<string, any>
\| undefined
|
+
+
+
+### iotaDocument.createPresentationJpt(presentation, method_id, options) ⇒ [Promise.<Jpt>
](#Jpt)
+**Kind**: instance method of [IotaDocument
](#IotaDocument)
+
+| Param | Type |
+| --- | --- |
+| presentation | [SelectiveDisclosurePresentation
](#SelectiveDisclosurePresentation) |
+| method_id | string
|
+| options | [JwpPresentationOptions
](#JwpPresentationOptions) |
+
### IotaDocument.newWithId(id) ⇒ [IotaDocument
](#IotaDocument)
@@ -2732,123 +3023,497 @@ Fetches the `IAliasOutput` associated with the given DID.
| client | IIotaIdentityClient
|
| did | [IotaDID
](#IotaDID) |
-
+
-## Jwk
+## IssuerProtectedHeader
**Kind**: global class
-* [Jwk](#Jwk)
- * [new Jwk(jwk)](#new_Jwk_new)
- * _instance_
- * [.kty()](#Jwk+kty) ⇒ JwkType
- * [.use()](#Jwk+use) ⇒ JwkUse
\| undefined
- * [.keyOps()](#Jwk+keyOps) ⇒ Array.<JwkOperation>
- * [.alg()](#Jwk+alg) ⇒ JwsAlgorithm
\| undefined
- * [.kid()](#Jwk+kid) ⇒ string
\| undefined
- * [.x5u()](#Jwk+x5u) ⇒ string
\| undefined
- * [.x5c()](#Jwk+x5c) ⇒ Array.<string>
- * [.x5t()](#Jwk+x5t) ⇒ string
\| undefined
- * [.x5t256()](#Jwk+x5t256) ⇒ string
\| undefined
- * [.paramsEc()](#Jwk+paramsEc) ⇒ JwkParamsEc
\| undefined
- * [.paramsOkp()](#Jwk+paramsOkp) ⇒ JwkParamsOkp
\| undefined
- * [.paramsOct()](#Jwk+paramsOct) ⇒ JwkParamsOct
\| undefined
- * [.paramsRsa()](#Jwk+paramsRsa) ⇒ JwkParamsRsa
\| undefined
- * [.toPublic()](#Jwk+toPublic) ⇒ [Jwk
](#Jwk) \| undefined
- * [.isPublic()](#Jwk+isPublic) ⇒ boolean
- * [.isPrivate()](#Jwk+isPrivate) ⇒ boolean
- * [.toJSON()](#Jwk+toJSON) ⇒ any
- * [.clone()](#Jwk+clone) ⇒ [Jwk
](#Jwk)
- * _static_
- * [.fromJSON(json)](#Jwk.fromJSON) ⇒ [Jwk
](#Jwk)
+* [IssuerProtectedHeader](#IssuerProtectedHeader)
+ * [.typ](#IssuerProtectedHeader+typ) ⇒ string
\| undefined
+ * [.typ](#IssuerProtectedHeader+typ)
+ * [.alg](#IssuerProtectedHeader+alg) ⇒ [ProofAlgorithm
](#ProofAlgorithm)
+ * [.alg](#IssuerProtectedHeader+alg)
+ * [.kid](#IssuerProtectedHeader+kid) ⇒ string
\| undefined
+ * [.kid](#IssuerProtectedHeader+kid)
+ * [.cid](#IssuerProtectedHeader+cid) ⇒ string
\| undefined
+ * [.cid](#IssuerProtectedHeader+cid)
+ * [.claims()](#IssuerProtectedHeader+claims) ⇒ Array.<string>
-
+
-### new Jwk(jwk)
+### issuerProtectedHeader.typ ⇒ string
\| undefined
+JWP type (JPT).
+
+**Kind**: instance property of [IssuerProtectedHeader
](#IssuerProtectedHeader)
+
+
+### issuerProtectedHeader.typ
+JWP type (JPT).
+
+**Kind**: instance property of [IssuerProtectedHeader
](#IssuerProtectedHeader)
| Param | Type |
| --- | --- |
-| jwk | IJwkParams
|
+| [arg0] | string
\| undefined
|
-
+
-### jwk.kty() ⇒ JwkType
-Returns the value for the key type parameter (kty).
+### issuerProtectedHeader.alg ⇒ [ProofAlgorithm
](#ProofAlgorithm)
+Algorithm used for the JWP.
-**Kind**: instance method of [Jwk
](#Jwk)
-
+**Kind**: instance property of [IssuerProtectedHeader
](#IssuerProtectedHeader)
+
-### jwk.use() ⇒ JwkUse
\| undefined
-Returns the value for the use property (use).
+### issuerProtectedHeader.alg
+Algorithm used for the JWP.
-**Kind**: instance method of [Jwk
](#Jwk)
-
+**Kind**: instance property of [IssuerProtectedHeader
](#IssuerProtectedHeader)
-### jwk.keyOps() ⇒ Array.<JwkOperation>
-**Kind**: instance method of [Jwk
](#Jwk)
-
+| Param | Type |
+| --- | --- |
+| arg0 | [ProofAlgorithm
](#ProofAlgorithm) |
-### jwk.alg() ⇒ JwsAlgorithm
\| undefined
-Returns the value for the algorithm property (alg).
+
-**Kind**: instance method of [Jwk
](#Jwk)
-
+### issuerProtectedHeader.kid ⇒ string
\| undefined
+ID for the key used for the JWP.
-### jwk.kid() ⇒ string
\| undefined
-Returns the value of the key ID property (kid).
+**Kind**: instance property of [IssuerProtectedHeader
](#IssuerProtectedHeader)
+
-**Kind**: instance method of [Jwk
](#Jwk)
-
+### issuerProtectedHeader.kid
+ID for the key used for the JWP.
-### jwk.x5u() ⇒ string
\| undefined
-Returns the value of the X.509 URL property (x5u).
+**Kind**: instance property of [IssuerProtectedHeader
](#IssuerProtectedHeader)
-**Kind**: instance method of [Jwk
](#Jwk)
-
+| Param | Type |
+| --- | --- |
+| [arg0] | string
\| undefined
|
-### jwk.x5c() ⇒ Array.<string>
-Returns the value of the X.509 certificate chain property (x5c).
+
-**Kind**: instance method of [Jwk
](#Jwk)
-
+### issuerProtectedHeader.cid ⇒ string
\| undefined
+Not handled for now. Will be used in the future to resolve external claims
-### jwk.x5t() ⇒ string
\| undefined
-Returns the value of the X.509 certificate SHA-1 thumbprint property (x5t).
+**Kind**: instance property of [IssuerProtectedHeader
](#IssuerProtectedHeader)
+
-**Kind**: instance method of [Jwk
](#Jwk)
-
+### issuerProtectedHeader.cid
+Not handled for now. Will be used in the future to resolve external claims
-### jwk.x5t256() ⇒ string
\| undefined
-Returns the value of the X.509 certificate SHA-256 thumbprint property (x5t#S256).
+**Kind**: instance property of [IssuerProtectedHeader
](#IssuerProtectedHeader)
-**Kind**: instance method of [Jwk
](#Jwk)
-
+| Param | Type |
+| --- | --- |
+| [arg0] | string
\| undefined
|
-### jwk.paramsEc() ⇒ JwkParamsEc
\| undefined
-If this JWK is of kty EC, returns those parameters.
+
-**Kind**: instance method of [Jwk
](#Jwk)
-
+### issuerProtectedHeader.claims() ⇒ Array.<string>
+**Kind**: instance method of [IssuerProtectedHeader
](#IssuerProtectedHeader)
+
-### jwk.paramsOkp() ⇒ JwkParamsOkp
\| undefined
-If this JWK is of kty OKP, returns those parameters.
+## Jpt
+A JSON Proof Token (JPT).
-**Kind**: instance method of [Jwk
](#Jwk)
-
+**Kind**: global class
-### jwk.paramsOct() ⇒ JwkParamsOct
\| undefined
-If this JWK is of kty OCT, returns those parameters.
+* [Jpt](#Jpt)
+ * [new Jpt(jpt_string)](#new_Jpt_new)
+ * [.toString()](#Jpt+toString) ⇒ string
+ * [.clone()](#Jpt+clone) ⇒ [Jpt
](#Jpt)
-**Kind**: instance method of [Jwk
](#Jwk)
-
+
-### jwk.paramsRsa() ⇒ JwkParamsRsa
\| undefined
-If this JWK is of kty RSA, returns those parameters.
+### new Jpt(jpt_string)
+Creates a new [Jpt](#Jpt).
-**Kind**: instance method of [Jwk
](#Jwk)
-
-### jwk.toPublic() ⇒ [Jwk
](#Jwk) \| undefined
-Returns a clone of the [Jwk](#Jwk) with _all_ private key components unset.
+| Param | Type |
+| --- | --- |
+| jpt_string | string
|
+
+
+
+### jpt.toString() ⇒ string
+**Kind**: instance method of [Jpt
](#Jpt)
+
+
+### jpt.clone() ⇒ [Jpt
](#Jpt)
+Deep clones the object.
+
+**Kind**: instance method of [Jpt
](#Jpt)
+
+
+## JptCredentialValidationOptions
+Options to declare validation criteria for [Jpt](#Jpt).
+
+**Kind**: global class
+
+* [JptCredentialValidationOptions](#JptCredentialValidationOptions)
+ * [new JptCredentialValidationOptions([opts])](#new_JptCredentialValidationOptions_new)
+ * _instance_
+ * [.clone()](#JptCredentialValidationOptions+clone) ⇒ [JptCredentialValidationOptions
](#JptCredentialValidationOptions)
+ * [.toJSON()](#JptCredentialValidationOptions+toJSON) ⇒ any
+ * _static_
+ * [.fromJSON(json)](#JptCredentialValidationOptions.fromJSON) ⇒ [JptCredentialValidationOptions
](#JptCredentialValidationOptions)
+
+
+
+### new JptCredentialValidationOptions([opts])
+Creates a new default istance.
+
+
+| Param | Type |
+| --- | --- |
+| [opts] | IJptCredentialValidationOptions
\| undefined
|
+
+
+
+### jptCredentialValidationOptions.clone() ⇒ [JptCredentialValidationOptions
](#JptCredentialValidationOptions)
+Deep clones the object.
+
+**Kind**: instance method of [JptCredentialValidationOptions
](#JptCredentialValidationOptions)
+
+
+### jptCredentialValidationOptions.toJSON() ⇒ any
+Serializes this to a JSON object.
+
+**Kind**: instance method of [JptCredentialValidationOptions
](#JptCredentialValidationOptions)
+
+
+### JptCredentialValidationOptions.fromJSON(json) ⇒ [JptCredentialValidationOptions
](#JptCredentialValidationOptions)
+Deserializes an instance from a JSON object.
+
+**Kind**: static method of [JptCredentialValidationOptions
](#JptCredentialValidationOptions)
+
+| Param | Type |
+| --- | --- |
+| json | any
|
+
+
+
+## JptCredentialValidator
+**Kind**: global class
+
+
+### JptCredentialValidator.validate(credential_jpt, issuer, options, fail_fast) ⇒ [DecodedJptCredential
](#DecodedJptCredential)
+**Kind**: static method of [JptCredentialValidator
](#JptCredentialValidator)
+
+| Param | Type |
+| --- | --- |
+| credential_jpt | [Jpt
](#Jpt) |
+| issuer | [CoreDocument
](#CoreDocument) \| IToCoreDocument
|
+| options | [JptCredentialValidationOptions
](#JptCredentialValidationOptions) |
+| fail_fast | [FailFast
](#FailFast) |
+
+
+
+## JptCredentialValidatorUtils
+Utility functions for validating JPT credentials.
+
+**Kind**: global class
+
+* [JptCredentialValidatorUtils](#JptCredentialValidatorUtils)
+ * [.extractIssuer(credential)](#JptCredentialValidatorUtils.extractIssuer) ⇒ [CoreDID
](#CoreDID)
+ * [.extractIssuerFromIssuedJpt(credential)](#JptCredentialValidatorUtils.extractIssuerFromIssuedJpt) ⇒ [CoreDID
](#CoreDID)
+ * [.checkTimeframesWithValidityTimeframe2024(credential, validity_timeframe, status_check)](#JptCredentialValidatorUtils.checkTimeframesWithValidityTimeframe2024)
+ * [.checkRevocationWithValidityTimeframe2024(credential, issuer, status_check)](#JptCredentialValidatorUtils.checkRevocationWithValidityTimeframe2024)
+ * [.checkTimeframesAndRevocationWithValidityTimeframe2024(credential, issuer, validity_timeframe, status_check)](#JptCredentialValidatorUtils.checkTimeframesAndRevocationWithValidityTimeframe2024)
+
+
+
+### JptCredentialValidatorUtils.extractIssuer(credential) ⇒ [CoreDID
](#CoreDID)
+Utility for extracting the issuer field of a [`Credential`](`Credential`) as a DID.
+# Errors
+Fails if the issuer field is not a valid DID.
+
+**Kind**: static method of [JptCredentialValidatorUtils
](#JptCredentialValidatorUtils)
+
+| Param | Type |
+| --- | --- |
+| credential | [Credential
](#Credential) |
+
+
+
+### JptCredentialValidatorUtils.extractIssuerFromIssuedJpt(credential) ⇒ [CoreDID
](#CoreDID)
+Utility for extracting the issuer field of a credential in JPT representation as DID.
+# Errors
+If the JPT decoding fails or the issuer field is not a valid DID.
+
+**Kind**: static method of [JptCredentialValidatorUtils
](#JptCredentialValidatorUtils)
+
+| Param | Type |
+| --- | --- |
+| credential | [Jpt
](#Jpt) |
+
+
+
+### JptCredentialValidatorUtils.checkTimeframesWithValidityTimeframe2024(credential, validity_timeframe, status_check)
+**Kind**: static method of [JptCredentialValidatorUtils
](#JptCredentialValidatorUtils)
+
+| Param | Type |
+| --- | --- |
+| credential | [Credential
](#Credential) |
+| validity_timeframe | [Timestamp
](#Timestamp) \| undefined
|
+| status_check | [StatusCheck
](#StatusCheck) |
+
+
+
+### JptCredentialValidatorUtils.checkRevocationWithValidityTimeframe2024(credential, issuer, status_check)
+Checks whether the credential status has been revoked.
+
+Only supports `RevocationTimeframe2024`.
+
+**Kind**: static method of [JptCredentialValidatorUtils
](#JptCredentialValidatorUtils)
+
+| Param | Type |
+| --- | --- |
+| credential | [Credential
](#Credential) |
+| issuer | [CoreDocument
](#CoreDocument) \| IToCoreDocument
|
+| status_check | [StatusCheck
](#StatusCheck) |
+
+
+
+### JptCredentialValidatorUtils.checkTimeframesAndRevocationWithValidityTimeframe2024(credential, issuer, validity_timeframe, status_check)
+Checks whether the credential status has been revoked or the timeframe interval is INVALID
+
+Only supports `RevocationTimeframe2024`.
+
+**Kind**: static method of [JptCredentialValidatorUtils
](#JptCredentialValidatorUtils)
+
+| Param | Type |
+| --- | --- |
+| credential | [Credential
](#Credential) |
+| issuer | [CoreDocument
](#CoreDocument) \| IToCoreDocument
|
+| validity_timeframe | [Timestamp
](#Timestamp) \| undefined
|
+| status_check | [StatusCheck
](#StatusCheck) |
+
+
+
+## JptPresentationValidationOptions
+Options to declare validation criteria for a [Jpt](#Jpt) presentation.
+
+**Kind**: global class
+
+* [JptPresentationValidationOptions](#JptPresentationValidationOptions)
+ * [new JptPresentationValidationOptions([opts])](#new_JptPresentationValidationOptions_new)
+ * _instance_
+ * [.clone()](#JptPresentationValidationOptions+clone) ⇒ [JptPresentationValidationOptions
](#JptPresentationValidationOptions)
+ * [.toJSON()](#JptPresentationValidationOptions+toJSON) ⇒ any
+ * _static_
+ * [.fromJSON(json)](#JptPresentationValidationOptions.fromJSON) ⇒ [JptPresentationValidationOptions
](#JptPresentationValidationOptions)
+
+
+
+### new JptPresentationValidationOptions([opts])
+
+| Param | Type |
+| --- | --- |
+| [opts] | IJptPresentationValidationOptions
\| undefined
|
+
+
+
+### jptPresentationValidationOptions.clone() ⇒ [JptPresentationValidationOptions
](#JptPresentationValidationOptions)
+Deep clones the object.
+
+**Kind**: instance method of [JptPresentationValidationOptions
](#JptPresentationValidationOptions)
+
+
+### jptPresentationValidationOptions.toJSON() ⇒ any
+Serializes this to a JSON object.
+
+**Kind**: instance method of [JptPresentationValidationOptions
](#JptPresentationValidationOptions)
+
+
+### JptPresentationValidationOptions.fromJSON(json) ⇒ [JptPresentationValidationOptions
](#JptPresentationValidationOptions)
+Deserializes an instance from a JSON object.
+
+**Kind**: static method of [JptPresentationValidationOptions
](#JptPresentationValidationOptions)
+
+| Param | Type |
+| --- | --- |
+| json | any
|
+
+
+
+## JptPresentationValidator
+**Kind**: global class
+
+
+### JptPresentationValidator.validate(presentation_jpt, issuer, options, fail_fast) ⇒ [DecodedJptPresentation
](#DecodedJptPresentation)
+Decodes and validates a Presented [Credential](#Credential) issued as a JPT (JWP Presented Form). A
+[DecodedJptPresentation](#DecodedJptPresentation) is returned upon success.
+
+The following properties are validated according to `options`:
+- the holder's proof on the JWP,
+- the expiration date,
+- the issuance date,
+- the semantic structure.
+
+**Kind**: static method of [JptPresentationValidator
](#JptPresentationValidator)
+
+| Param | Type |
+| --- | --- |
+| presentation_jpt | [Jpt
](#Jpt) |
+| issuer | [CoreDocument
](#CoreDocument) \| IToCoreDocument
|
+| options | [JptPresentationValidationOptions
](#JptPresentationValidationOptions) |
+| fail_fast | [FailFast
](#FailFast) |
+
+
+
+## JptPresentationValidatorUtils
+Utility functions for verifying JPT presentations.
+
+**Kind**: global class
+
+* [JptPresentationValidatorUtils](#JptPresentationValidatorUtils)
+ * [.extractIssuerFromPresentedJpt(presentation)](#JptPresentationValidatorUtils.extractIssuerFromPresentedJpt) ⇒ [CoreDID
](#CoreDID)
+ * [.checkTimeframesWithValidityTimeframe2024(credential, validity_timeframe, status_check)](#JptPresentationValidatorUtils.checkTimeframesWithValidityTimeframe2024)
+
+
+
+### JptPresentationValidatorUtils.extractIssuerFromPresentedJpt(presentation) ⇒ [CoreDID
](#CoreDID)
+Utility for extracting the issuer field of a credential in JPT representation as DID.
+# Errors
+If the JPT decoding fails or the issuer field is not a valid DID.
+
+**Kind**: static method of [JptPresentationValidatorUtils
](#JptPresentationValidatorUtils)
+
+| Param | Type |
+| --- | --- |
+| presentation | [Jpt
](#Jpt) |
+
+
+
+### JptPresentationValidatorUtils.checkTimeframesWithValidityTimeframe2024(credential, validity_timeframe, status_check)
+Check timeframe interval in credentialStatus with `RevocationTimeframeStatus`.
+
+**Kind**: static method of [JptPresentationValidatorUtils
](#JptPresentationValidatorUtils)
+
+| Param | Type |
+| --- | --- |
+| credential | [Credential
](#Credential) |
+| validity_timeframe | [Timestamp
](#Timestamp) \| undefined
|
+| status_check | [StatusCheck
](#StatusCheck) |
+
+
+
+## Jwk
+**Kind**: global class
+
+* [Jwk](#Jwk)
+ * [new Jwk(jwk)](#new_Jwk_new)
+ * _instance_
+ * [.kty()](#Jwk+kty) ⇒ JwkType
+ * [.use()](#Jwk+use) ⇒ JwkUse
\| undefined
+ * [.keyOps()](#Jwk+keyOps) ⇒ Array.<JwkOperation>
+ * [.alg()](#Jwk+alg) ⇒ JwsAlgorithm
\| undefined
+ * [.kid()](#Jwk+kid) ⇒ string
\| undefined
+ * [.x5u()](#Jwk+x5u) ⇒ string
\| undefined
+ * [.x5c()](#Jwk+x5c) ⇒ Array.<string>
+ * [.x5t()](#Jwk+x5t) ⇒ string
\| undefined
+ * [.x5t256()](#Jwk+x5t256) ⇒ string
\| undefined
+ * [.paramsEc()](#Jwk+paramsEc) ⇒ JwkParamsEc
\| undefined
+ * [.paramsOkp()](#Jwk+paramsOkp) ⇒ JwkParamsOkp
\| undefined
+ * [.paramsOct()](#Jwk+paramsOct) ⇒ JwkParamsOct
\| undefined
+ * [.paramsRsa()](#Jwk+paramsRsa) ⇒ JwkParamsRsa
\| undefined
+ * [.toPublic()](#Jwk+toPublic) ⇒ [Jwk
](#Jwk) \| undefined
+ * [.isPublic()](#Jwk+isPublic) ⇒ boolean
+ * [.isPrivate()](#Jwk+isPrivate) ⇒ boolean
+ * [.toJSON()](#Jwk+toJSON) ⇒ any
+ * [.clone()](#Jwk+clone) ⇒ [Jwk
](#Jwk)
+ * _static_
+ * [.fromJSON(json)](#Jwk.fromJSON) ⇒ [Jwk
](#Jwk)
+
+
+
+### new Jwk(jwk)
+
+| Param | Type |
+| --- | --- |
+| jwk | IJwkParams
|
+
+
+
+### jwk.kty() ⇒ JwkType
+Returns the value for the key type parameter (kty).
+
+**Kind**: instance method of [Jwk
](#Jwk)
+
+
+### jwk.use() ⇒ JwkUse
\| undefined
+Returns the value for the use property (use).
+
+**Kind**: instance method of [Jwk
](#Jwk)
+
+
+### jwk.keyOps() ⇒ Array.<JwkOperation>
+**Kind**: instance method of [Jwk
](#Jwk)
+
+
+### jwk.alg() ⇒ JwsAlgorithm
\| undefined
+Returns the value for the algorithm property (alg).
+
+**Kind**: instance method of [Jwk
](#Jwk)
+
+
+### jwk.kid() ⇒ string
\| undefined
+Returns the value of the key ID property (kid).
+
+**Kind**: instance method of [Jwk
](#Jwk)
+
+
+### jwk.x5u() ⇒ string
\| undefined
+Returns the value of the X.509 URL property (x5u).
+
+**Kind**: instance method of [Jwk
](#Jwk)
+
+
+### jwk.x5c() ⇒ Array.<string>
+Returns the value of the X.509 certificate chain property (x5c).
+
+**Kind**: instance method of [Jwk
](#Jwk)
+
+
+### jwk.x5t() ⇒ string
\| undefined
+Returns the value of the X.509 certificate SHA-1 thumbprint property (x5t).
+
+**Kind**: instance method of [Jwk
](#Jwk)
+
+
+### jwk.x5t256() ⇒ string
\| undefined
+Returns the value of the X.509 certificate SHA-256 thumbprint property (x5t#S256).
+
+**Kind**: instance method of [Jwk
](#Jwk)
+
+
+### jwk.paramsEc() ⇒ JwkParamsEc
\| undefined
+If this JWK is of kty EC, returns those parameters.
+
+**Kind**: instance method of [Jwk
](#Jwk)
+
+
+### jwk.paramsOkp() ⇒ JwkParamsOkp
\| undefined
+If this JWK is of kty OKP, returns those parameters.
+
+**Kind**: instance method of [Jwk
](#Jwk)
+
+
+### jwk.paramsOct() ⇒ JwkParamsOct
\| undefined
+If this JWK is of kty OCT, returns those parameters.
+
+**Kind**: instance method of [Jwk
](#Jwk)
+
+
+### jwk.paramsRsa() ⇒ JwkParamsRsa
\| undefined
+If this JWK is of kty RSA, returns those parameters.
+
+**Kind**: instance method of [Jwk
](#Jwk)
+
+
+### jwk.toPublic() ⇒ [Jwk
](#Jwk) \| undefined
+Returns a clone of the [Jwk](#Jwk) with _all_ private key components unset.
Nothing is returned when `kty = oct` as this key type is not considered public by this library.
**Kind**: instance method of [Jwk
](#Jwk)
@@ -2948,6 +3613,217 @@ Deserializes an instance from a JSON object.
| --- | --- |
| json | any
|
+
+
+## JwpCredentialOptions
+**Kind**: global class
+
+* [JwpCredentialOptions](#JwpCredentialOptions)
+ * _instance_
+ * [.kid](#JwpCredentialOptions+kid) ⇒ string
\| undefined
+ * [.kid](#JwpCredentialOptions+kid)
+ * [.toJSON()](#JwpCredentialOptions+toJSON) ⇒ any
+ * _static_
+ * [.fromJSON(value)](#JwpCredentialOptions.fromJSON) ⇒ [JwpCredentialOptions
](#JwpCredentialOptions)
+
+
+
+### jwpCredentialOptions.kid ⇒ string
\| undefined
+**Kind**: instance property of [JwpCredentialOptions
](#JwpCredentialOptions)
+
+
+### jwpCredentialOptions.kid
+**Kind**: instance property of [JwpCredentialOptions
](#JwpCredentialOptions)
+
+| Param | Type |
+| --- | --- |
+| [arg0] | string
\| undefined
|
+
+
+
+### jwpCredentialOptions.toJSON() ⇒ any
+**Kind**: instance method of [JwpCredentialOptions
](#JwpCredentialOptions)
+
+
+### JwpCredentialOptions.fromJSON(value) ⇒ [JwpCredentialOptions
](#JwpCredentialOptions)
+**Kind**: static method of [JwpCredentialOptions
](#JwpCredentialOptions)
+
+| Param | Type |
+| --- | --- |
+| value | any
|
+
+
+
+## JwpIssued
+**Kind**: global class
+
+* [JwpIssued](#JwpIssued)
+ * _instance_
+ * [.toJSON()](#JwpIssued+toJSON) ⇒ any
+ * [.clone()](#JwpIssued+clone) ⇒ [JwpIssued
](#JwpIssued)
+ * [.encode(serialization)](#JwpIssued+encode) ⇒ string
+ * [.setProof(proof)](#JwpIssued+setProof)
+ * [.getProof()](#JwpIssued+getProof) ⇒ Uint8Array
+ * [.getPayloads()](#JwpIssued+getPayloads) ⇒ [Payloads
](#Payloads)
+ * [.setPayloads(payloads)](#JwpIssued+setPayloads)
+ * [.getIssuerProtectedHeader()](#JwpIssued+getIssuerProtectedHeader) ⇒ [IssuerProtectedHeader
](#IssuerProtectedHeader)
+ * _static_
+ * [.fromJSON(json)](#JwpIssued.fromJSON) ⇒ [JwpIssued
](#JwpIssued)
+
+
+
+### jwpIssued.toJSON() ⇒ any
+Serializes this to a JSON object.
+
+**Kind**: instance method of [JwpIssued
](#JwpIssued)
+
+
+### jwpIssued.clone() ⇒ [JwpIssued
](#JwpIssued)
+Deep clones the object.
+
+**Kind**: instance method of [JwpIssued
](#JwpIssued)
+
+
+### jwpIssued.encode(serialization) ⇒ string
+**Kind**: instance method of [JwpIssued
](#JwpIssued)
+
+| Param | Type |
+| --- | --- |
+| serialization | [SerializationType
](#SerializationType) |
+
+
+
+### jwpIssued.setProof(proof)
+**Kind**: instance method of [JwpIssued
](#JwpIssued)
+
+| Param | Type |
+| --- | --- |
+| proof | Uint8Array
|
+
+
+
+### jwpIssued.getProof() ⇒ Uint8Array
+**Kind**: instance method of [JwpIssued
](#JwpIssued)
+
+
+### jwpIssued.getPayloads() ⇒ [Payloads
](#Payloads)
+**Kind**: instance method of [JwpIssued
](#JwpIssued)
+
+
+### jwpIssued.setPayloads(payloads)
+**Kind**: instance method of [JwpIssued
](#JwpIssued)
+
+| Param | Type |
+| --- | --- |
+| payloads | [Payloads
](#Payloads) |
+
+
+
+### jwpIssued.getIssuerProtectedHeader() ⇒ [IssuerProtectedHeader
](#IssuerProtectedHeader)
+**Kind**: instance method of [JwpIssued
](#JwpIssued)
+
+
+### JwpIssued.fromJSON(json) ⇒ [JwpIssued
](#JwpIssued)
+Deserializes an instance from a JSON object.
+
+**Kind**: static method of [JwpIssued
](#JwpIssued)
+
+| Param | Type |
+| --- | --- |
+| json | any
|
+
+
+
+## JwpPresentationOptions
+Options to be set in the JWT claims of a verifiable presentation.
+
+**Kind**: global class
+
+* [JwpPresentationOptions](#JwpPresentationOptions)
+ * [.audience](#JwpPresentationOptions+audience) ⇒ string
\| undefined
+ * [.audience](#JwpPresentationOptions+audience)
+ * [.nonce](#JwpPresentationOptions+nonce) ⇒ string
\| undefined
+ * [.nonce](#JwpPresentationOptions+nonce)
+
+
+
+### jwpPresentationOptions.audience ⇒ string
\| undefined
+Sets the audience for presentation (`aud` property in JWP Presentation Header).
+
+**Kind**: instance property of [JwpPresentationOptions
](#JwpPresentationOptions)
+
+
+### jwpPresentationOptions.audience
+Sets the audience for presentation (`aud` property in JWP Presentation Header).
+
+**Kind**: instance property of [JwpPresentationOptions
](#JwpPresentationOptions)
+
+| Param | Type |
+| --- | --- |
+| [arg0] | string
\| undefined
|
+
+
+
+### jwpPresentationOptions.nonce ⇒ string
\| undefined
+The nonce to be placed in the Presentation Protected Header.
+
+**Kind**: instance property of [JwpPresentationOptions
](#JwpPresentationOptions)
+
+
+### jwpPresentationOptions.nonce
+The nonce to be placed in the Presentation Protected Header.
+
+**Kind**: instance property of [JwpPresentationOptions
](#JwpPresentationOptions)
+
+| Param | Type |
+| --- | --- |
+| [arg0] | string
\| undefined
|
+
+
+
+## JwpVerificationOptions
+**Kind**: global class
+
+* [JwpVerificationOptions](#JwpVerificationOptions)
+ * _instance_
+ * [.clone()](#JwpVerificationOptions+clone) ⇒ [JwpVerificationOptions
](#JwpVerificationOptions)
+ * [.toJSON()](#JwpVerificationOptions+toJSON) ⇒ any
+ * _static_
+ * [.fromJSON(json)](#JwpVerificationOptions.fromJSON) ⇒ [JwpVerificationOptions
](#JwpVerificationOptions)
+ * [.new([opts])](#JwpVerificationOptions.new) ⇒ [JwpVerificationOptions
](#JwpVerificationOptions)
+
+
+
+### jwpVerificationOptions.clone() ⇒ [JwpVerificationOptions
](#JwpVerificationOptions)
+Deep clones the object.
+
+**Kind**: instance method of [JwpVerificationOptions
](#JwpVerificationOptions)
+
+
+### jwpVerificationOptions.toJSON() ⇒ any
+Serializes this to a JSON object.
+
+**Kind**: instance method of [JwpVerificationOptions
](#JwpVerificationOptions)
+
+
+### JwpVerificationOptions.fromJSON(json) ⇒ [JwpVerificationOptions
](#JwpVerificationOptions)
+Deserializes an instance from a JSON object.
+
+**Kind**: static method of [JwpVerificationOptions
](#JwpVerificationOptions)
+
+| Param | Type |
+| --- | --- |
+| json | any
|
+
+
+
+### JwpVerificationOptions.new([opts]) ⇒ [JwpVerificationOptions
](#JwpVerificationOptions)
+**Kind**: static method of [JwpVerificationOptions
](#JwpVerificationOptions)
+
+| Param | Type |
+| --- | --- |
+| [opts] | IJwpVerificationOptions
\| undefined
|
+
## Jws
@@ -4328,6 +5204,7 @@ Supported verification method data formats.
* [MethodData](#MethodData)
* _instance_
+ * [.tryCustom()](#MethodData+tryCustom) ⇒ [CustomMethodData
](#CustomMethodData)
* [.tryDecode()](#MethodData+tryDecode) ⇒ Uint8Array
* [.tryPublicKeyJwk()](#MethodData+tryPublicKeyJwk) ⇒ [Jwk
](#Jwk)
* [.toJSON()](#MethodData+toJSON) ⇒ any
@@ -4336,8 +5213,15 @@ Supported verification method data formats.
* [.newBase58(data)](#MethodData.newBase58) ⇒ [MethodData
](#MethodData)
* [.newMultibase(data)](#MethodData.newMultibase) ⇒ [MethodData
](#MethodData)
* [.newJwk(key)](#MethodData.newJwk) ⇒ [MethodData
](#MethodData)
+ * [.newCustom(name, data)](#MethodData.newCustom) ⇒ [MethodData
](#MethodData)
* [.fromJSON(json)](#MethodData.fromJSON) ⇒ [MethodData
](#MethodData)
+
+
+### methodData.tryCustom() ⇒ [CustomMethodData
](#CustomMethodData)
+Returns the wrapped custom method data format is `Custom`.
+
+**Kind**: instance method of [MethodData
](#MethodData)
### methodData.tryDecode() ⇒ Uint8Array
@@ -4404,6 +5288,18 @@ An error is thrown if the given `key` contains any private components.
| --- | --- |
| key | [Jwk
](#Jwk) |
+
+
+### MethodData.newCustom(name, data) ⇒ [MethodData
](#MethodData)
+Creates a new custom [MethodData](#MethodData).
+
+**Kind**: static method of [MethodData
](#MethodData)
+
+| Param | Type |
+| --- | --- |
+| name | string
|
+| data | any
|
+
### MethodData.fromJSON(json) ⇒ [MethodData
](#MethodData)
@@ -4555,6 +5451,7 @@ Supported verification method types.
* [.Ed25519VerificationKey2018()](#MethodType.Ed25519VerificationKey2018) ⇒ [MethodType
](#MethodType)
* [.X25519KeyAgreementKey2019()](#MethodType.X25519KeyAgreementKey2019) ⇒ [MethodType
](#MethodType)
* [.JsonWebKey()](#MethodType.JsonWebKey) ⇒ [MethodType
](#MethodType)
+ * [.custom(type_)](#MethodType.custom) ⇒ [MethodType
](#MethodType)
* [.fromJSON(json)](#MethodType.fromJSON) ⇒ [MethodType
](#MethodType)
@@ -4590,6 +5487,17 @@ A verification method for use with JWT verification as prescribed by the [Jwk](#
in the `publicKeyJwk` entry.
**Kind**: static method of [MethodType
](#MethodType)
+
+
+### MethodType.custom(type_) ⇒ [MethodType
](#MethodType)
+A custom method.
+
+**Kind**: static method of [MethodType
](#MethodType)
+
+| Param | Type |
+| --- | --- |
+| type_ | string
|
+
### MethodType.fromJSON(json) ⇒ [MethodType
](#MethodType)
@@ -4601,6 +5509,143 @@ Deserializes an instance from a JSON object.
| --- | --- |
| json | any
|
+
+
+## PayloadEntry
+**Kind**: global class
+
+* [PayloadEntry](#PayloadEntry)
+ * [.1](#PayloadEntry+1) ⇒ [PayloadType
](#PayloadType)
+ * [.1](#PayloadEntry+1)
+ * [.value](#PayloadEntry+value)
+ * [.value](#PayloadEntry+value) ⇒ any
+
+
+
+### payloadEntry.1 ⇒ [PayloadType
](#PayloadType)
+**Kind**: instance property of [PayloadEntry
](#PayloadEntry)
+
+
+### payloadEntry.1
+**Kind**: instance property of [PayloadEntry
](#PayloadEntry)
+
+| Param | Type |
+| --- | --- |
+| arg0 | [PayloadType
](#PayloadType) |
+
+
+
+### payloadEntry.value
+**Kind**: instance property of [PayloadEntry
](#PayloadEntry)
+
+| Param | Type |
+| --- | --- |
+| value | any
|
+
+
+
+### payloadEntry.value ⇒ any
+**Kind**: instance property of [PayloadEntry
](#PayloadEntry)
+
+
+## Payloads
+**Kind**: global class
+
+* [Payloads](#Payloads)
+ * [new Payloads(entries)](#new_Payloads_new)
+ * _instance_
+ * [.toJSON()](#Payloads+toJSON) ⇒ any
+ * [.clone()](#Payloads+clone) ⇒ [Payloads
](#Payloads)
+ * [.getValues()](#Payloads+getValues) ⇒ Array.<any>
+ * [.getUndisclosedIndexes()](#Payloads+getUndisclosedIndexes) ⇒ Uint32Array
+ * [.getDisclosedIndexes()](#Payloads+getDisclosedIndexes) ⇒ Uint32Array
+ * [.getUndisclosedPayloads()](#Payloads+getUndisclosedPayloads) ⇒ Array.<any>
+ * [.getDisclosedPayloads()](#Payloads+getDisclosedPayloads) ⇒ [Payloads
](#Payloads)
+ * [.setUndisclosed(index)](#Payloads+setUndisclosed)
+ * [.replacePayloadAtIndex(index, value)](#Payloads+replacePayloadAtIndex) ⇒ any
+ * _static_
+ * [.fromJSON(json)](#Payloads.fromJSON) ⇒ [Payloads
](#Payloads)
+ * [.newFromValues(values)](#Payloads.newFromValues) ⇒ [Payloads
](#Payloads)
+
+
+
+### new Payloads(entries)
+
+| Param | Type |
+| --- | --- |
+| entries | [Array.<PayloadEntry>
](#PayloadEntry) |
+
+
+
+### payloads.toJSON() ⇒ any
+Serializes this to a JSON object.
+
+**Kind**: instance method of [Payloads
](#Payloads)
+
+
+### payloads.clone() ⇒ [Payloads
](#Payloads)
+Deep clones the object.
+
+**Kind**: instance method of [Payloads
](#Payloads)
+
+
+### payloads.getValues() ⇒ Array.<any>
+**Kind**: instance method of [Payloads
](#Payloads)
+
+
+### payloads.getUndisclosedIndexes() ⇒ Uint32Array
+**Kind**: instance method of [Payloads
](#Payloads)
+
+
+### payloads.getDisclosedIndexes() ⇒ Uint32Array
+**Kind**: instance method of [Payloads
](#Payloads)
+
+
+### payloads.getUndisclosedPayloads() ⇒ Array.<any>
+**Kind**: instance method of [Payloads
](#Payloads)
+
+
+### payloads.getDisclosedPayloads() ⇒ [Payloads
](#Payloads)
+**Kind**: instance method of [Payloads
](#Payloads)
+
+
+### payloads.setUndisclosed(index)
+**Kind**: instance method of [Payloads
](#Payloads)
+
+| Param | Type |
+| --- | --- |
+| index | number
|
+
+
+
+### payloads.replacePayloadAtIndex(index, value) ⇒ any
+**Kind**: instance method of [Payloads
](#Payloads)
+
+| Param | Type |
+| --- | --- |
+| index | number
|
+| value | any
|
+
+
+
+### Payloads.fromJSON(json) ⇒ [Payloads
](#Payloads)
+Deserializes an instance from a JSON object.
+
+**Kind**: static method of [Payloads
](#Payloads)
+
+| Param | Type |
+| --- | --- |
+| json | any
|
+
+
+
+### Payloads.newFromValues(values) ⇒ [Payloads
](#Payloads)
+**Kind**: static method of [Payloads
](#Payloads)
+
+| Param | Type |
+| --- | --- |
+| values | Array.<any>
|
+
## Presentation
@@ -4717,26 +5762,105 @@ Deep clones the object.
**Kind**: instance method of [Presentation
](#Presentation)
-### Presentation.BaseContext() ⇒ string
-Returns the base JSON-LD context.
+### Presentation.BaseContext() ⇒ string
+Returns the base JSON-LD context.
+
+**Kind**: static method of [Presentation
](#Presentation)
+
+
+### Presentation.BaseType() ⇒ string
+Returns the base type.
+
+**Kind**: static method of [Presentation
](#Presentation)
+
+
+### Presentation.fromJSON(json) ⇒ [Presentation
](#Presentation)
+Deserializes an instance from a JSON object.
+
+**Kind**: static method of [Presentation
](#Presentation)
+
+| Param | Type |
+| --- | --- |
+| json | any
|
+
+
+
+## PresentationProtectedHeader
+**Kind**: global class
+
+* [PresentationProtectedHeader](#PresentationProtectedHeader)
+ * [.alg](#PresentationProtectedHeader+alg) ⇒ [PresentationProofAlgorithm
](#PresentationProofAlgorithm)
+ * [.alg](#PresentationProtectedHeader+alg)
+ * [.kid](#PresentationProtectedHeader+kid) ⇒ string
\| undefined
+ * [.kid](#PresentationProtectedHeader+kid)
+ * [.aud](#PresentationProtectedHeader+aud) ⇒ string
\| undefined
+ * [.aud](#PresentationProtectedHeader+aud)
+ * [.nonce](#PresentationProtectedHeader+nonce) ⇒ string
\| undefined
+ * [.nonce](#PresentationProtectedHeader+nonce)
+
+
+
+### presentationProtectedHeader.alg ⇒ [PresentationProofAlgorithm
](#PresentationProofAlgorithm)
+**Kind**: instance property of [PresentationProtectedHeader
](#PresentationProtectedHeader)
+
+
+### presentationProtectedHeader.alg
+**Kind**: instance property of [PresentationProtectedHeader
](#PresentationProtectedHeader)
+
+| Param | Type |
+| --- | --- |
+| arg0 | [PresentationProofAlgorithm
](#PresentationProofAlgorithm) |
+
+
+
+### presentationProtectedHeader.kid ⇒ string
\| undefined
+ID for the key used for the JWP.
+
+**Kind**: instance property of [PresentationProtectedHeader
](#PresentationProtectedHeader)
+
+
+### presentationProtectedHeader.kid
+ID for the key used for the JWP.
+
+**Kind**: instance property of [PresentationProtectedHeader
](#PresentationProtectedHeader)
+
+| Param | Type |
+| --- | --- |
+| [arg0] | string
\| undefined
|
+
+
+
+### presentationProtectedHeader.aud ⇒ string
\| undefined
+Who have received the JPT.
+
+**Kind**: instance property of [PresentationProtectedHeader
](#PresentationProtectedHeader)
+
+
+### presentationProtectedHeader.aud
+Who have received the JPT.
+
+**Kind**: instance property of [PresentationProtectedHeader
](#PresentationProtectedHeader)
+
+| Param | Type |
+| --- | --- |
+| [arg0] | string
\| undefined
|
-**Kind**: static method of [Presentation
](#Presentation)
-
+
-### Presentation.BaseType() ⇒ string
-Returns the base type.
+### presentationProtectedHeader.nonce ⇒ string
\| undefined
+For replay attacks.
-**Kind**: static method of [Presentation
](#Presentation)
-
+**Kind**: instance property of [PresentationProtectedHeader
](#PresentationProtectedHeader)
+
-### Presentation.fromJSON(json) ⇒ [Presentation
](#Presentation)
-Deserializes an instance from a JSON object.
+### presentationProtectedHeader.nonce
+For replay attacks.
-**Kind**: static method of [Presentation
](#Presentation)
+**Kind**: instance property of [PresentationProtectedHeader
](#PresentationProtectedHeader)
| Param | Type |
| --- | --- |
-| json | any
|
+| [arg0] | string
\| undefined
|
@@ -4807,6 +5931,146 @@ Deserializes an instance from a JSON object.
| --- | --- |
| json | any
|
+
+
+## ProofUpdateCtx
+**Kind**: global class
+
+* [ProofUpdateCtx](#ProofUpdateCtx)
+ * [.old_start_validity_timeframe](#ProofUpdateCtx+old_start_validity_timeframe) ⇒ Uint8Array
+ * [.old_start_validity_timeframe](#ProofUpdateCtx+old_start_validity_timeframe)
+ * [.new_start_validity_timeframe](#ProofUpdateCtx+new_start_validity_timeframe) ⇒ Uint8Array
+ * [.new_start_validity_timeframe](#ProofUpdateCtx+new_start_validity_timeframe)
+ * [.old_end_validity_timeframe](#ProofUpdateCtx+old_end_validity_timeframe) ⇒ Uint8Array
+ * [.old_end_validity_timeframe](#ProofUpdateCtx+old_end_validity_timeframe)
+ * [.new_end_validity_timeframe](#ProofUpdateCtx+new_end_validity_timeframe) ⇒ Uint8Array
+ * [.new_end_validity_timeframe](#ProofUpdateCtx+new_end_validity_timeframe)
+ * [.index_start_validity_timeframe](#ProofUpdateCtx+index_start_validity_timeframe) ⇒ number
+ * [.index_start_validity_timeframe](#ProofUpdateCtx+index_start_validity_timeframe)
+ * [.index_end_validity_timeframe](#ProofUpdateCtx+index_end_validity_timeframe) ⇒ number
+ * [.index_end_validity_timeframe](#ProofUpdateCtx+index_end_validity_timeframe)
+ * [.number_of_signed_messages](#ProofUpdateCtx+number_of_signed_messages) ⇒ number
+ * [.number_of_signed_messages](#ProofUpdateCtx+number_of_signed_messages)
+
+
+
+### proofUpdateCtx.old\_start\_validity\_timeframe ⇒ Uint8Array
+Old `startValidityTimeframe` value
+
+**Kind**: instance property of [ProofUpdateCtx
](#ProofUpdateCtx)
+
+
+### proofUpdateCtx.old\_start\_validity\_timeframe
+Old `startValidityTimeframe` value
+
+**Kind**: instance property of [ProofUpdateCtx
](#ProofUpdateCtx)
+
+| Param | Type |
+| --- | --- |
+| arg0 | Uint8Array
|
+
+
+
+### proofUpdateCtx.new\_start\_validity\_timeframe ⇒ Uint8Array
+New `startValidityTimeframe` value to be signed
+
+**Kind**: instance property of [ProofUpdateCtx
](#ProofUpdateCtx)
+
+
+### proofUpdateCtx.new\_start\_validity\_timeframe
+New `startValidityTimeframe` value to be signed
+
+**Kind**: instance property of [ProofUpdateCtx
](#ProofUpdateCtx)
+
+| Param | Type |
+| --- | --- |
+| arg0 | Uint8Array
|
+
+
+
+### proofUpdateCtx.old\_end\_validity\_timeframe ⇒ Uint8Array
+Old `endValidityTimeframe` value
+
+**Kind**: instance property of [ProofUpdateCtx
](#ProofUpdateCtx)
+
+
+### proofUpdateCtx.old\_end\_validity\_timeframe
+Old `endValidityTimeframe` value
+
+**Kind**: instance property of [ProofUpdateCtx
](#ProofUpdateCtx)
+
+| Param | Type |
+| --- | --- |
+| arg0 | Uint8Array
|
+
+
+
+### proofUpdateCtx.new\_end\_validity\_timeframe ⇒ Uint8Array
+New `endValidityTimeframe` value to be signed
+
+**Kind**: instance property of [ProofUpdateCtx
](#ProofUpdateCtx)
+
+
+### proofUpdateCtx.new\_end\_validity\_timeframe
+New `endValidityTimeframe` value to be signed
+
+**Kind**: instance property of [ProofUpdateCtx
](#ProofUpdateCtx)
+
+| Param | Type |
+| --- | --- |
+| arg0 | Uint8Array
|
+
+
+
+### proofUpdateCtx.index\_start\_validity\_timeframe ⇒ number
+Index of `startValidityTimeframe` claim inside the array of Claims
+
+**Kind**: instance property of [ProofUpdateCtx
](#ProofUpdateCtx)
+
+
+### proofUpdateCtx.index\_start\_validity\_timeframe
+Index of `startValidityTimeframe` claim inside the array of Claims
+
+**Kind**: instance property of [ProofUpdateCtx
](#ProofUpdateCtx)
+
+| Param | Type |
+| --- | --- |
+| arg0 | number
|
+
+
+
+### proofUpdateCtx.index\_end\_validity\_timeframe ⇒ number
+Index of `endValidityTimeframe` claim inside the array of Claims
+
+**Kind**: instance property of [ProofUpdateCtx
](#ProofUpdateCtx)
+
+
+### proofUpdateCtx.index\_end\_validity\_timeframe
+Index of `endValidityTimeframe` claim inside the array of Claims
+
+**Kind**: instance property of [ProofUpdateCtx
](#ProofUpdateCtx)
+
+| Param | Type |
+| --- | --- |
+| arg0 | number
|
+
+
+
+### proofUpdateCtx.number\_of\_signed\_messages ⇒ number
+Number of signed messages, number of payloads in a JWP
+
+**Kind**: instance property of [ProofUpdateCtx
](#ProofUpdateCtx)
+
+
+### proofUpdateCtx.number\_of\_signed\_messages
+Number of signed messages, number of payloads in a JWP
+
+**Kind**: instance property of [ProofUpdateCtx
](#ProofUpdateCtx)
+
+| Param | Type |
+| --- | --- |
+| arg0 | number
|
+
## Resolver
@@ -4975,6 +6239,85 @@ if it is a valid Revocation Bitmap Service.
| --- | --- |
| service | [Service
](#Service) |
+
+
+## RevocationTimeframeStatus
+Information used to determine the current status of a [Credential](#Credential).
+
+**Kind**: global class
+
+* [RevocationTimeframeStatus](#RevocationTimeframeStatus)
+ * [new RevocationTimeframeStatus(id, index, duration, [start_validity])](#new_RevocationTimeframeStatus_new)
+ * _instance_
+ * [.clone()](#RevocationTimeframeStatus+clone) ⇒ [RevocationTimeframeStatus
](#RevocationTimeframeStatus)
+ * [.toJSON()](#RevocationTimeframeStatus+toJSON) ⇒ any
+ * [.startValidityTimeframe()](#RevocationTimeframeStatus+startValidityTimeframe) ⇒ [Timestamp
](#Timestamp)
+ * [.endValidityTimeframe()](#RevocationTimeframeStatus+endValidityTimeframe) ⇒ [Timestamp
](#Timestamp)
+ * [.id()](#RevocationTimeframeStatus+id) ⇒ string
+ * [.index()](#RevocationTimeframeStatus+index) ⇒ number
\| undefined
+ * _static_
+ * [.fromJSON(json)](#RevocationTimeframeStatus.fromJSON) ⇒ [RevocationTimeframeStatus
](#RevocationTimeframeStatus)
+
+
+
+### new RevocationTimeframeStatus(id, index, duration, [start_validity])
+Creates a new `RevocationTimeframeStatus`.
+
+
+| Param | Type |
+| --- | --- |
+| id | string
|
+| index | number
|
+| duration | [Duration
](#Duration) |
+| [start_validity] | [Timestamp
](#Timestamp) \| undefined
|
+
+
+
+### revocationTimeframeStatus.clone() ⇒ [RevocationTimeframeStatus
](#RevocationTimeframeStatus)
+Deep clones the object.
+
+**Kind**: instance method of [RevocationTimeframeStatus
](#RevocationTimeframeStatus)
+
+
+### revocationTimeframeStatus.toJSON() ⇒ any
+Serializes this to a JSON object.
+
+**Kind**: instance method of [RevocationTimeframeStatus
](#RevocationTimeframeStatus)
+
+
+### revocationTimeframeStatus.startValidityTimeframe() ⇒ [Timestamp
](#Timestamp)
+Get startValidityTimeframe value.
+
+**Kind**: instance method of [RevocationTimeframeStatus
](#RevocationTimeframeStatus)
+
+
+### revocationTimeframeStatus.endValidityTimeframe() ⇒ [Timestamp
](#Timestamp)
+Get endValidityTimeframe value.
+
+**Kind**: instance method of [RevocationTimeframeStatus
](#RevocationTimeframeStatus)
+
+
+### revocationTimeframeStatus.id() ⇒ string
+Return the URL fo the `RevocationBitmapStatus`.
+
+**Kind**: instance method of [RevocationTimeframeStatus
](#RevocationTimeframeStatus)
+
+
+### revocationTimeframeStatus.index() ⇒ number
\| undefined
+Return the index of the credential in the issuer's revocation bitmap
+
+**Kind**: instance method of [RevocationTimeframeStatus
](#RevocationTimeframeStatus)
+
+
+### RevocationTimeframeStatus.fromJSON(json) ⇒ [RevocationTimeframeStatus
](#RevocationTimeframeStatus)
+Deserializes an instance from a JSON object.
+
+**Kind**: static method of [RevocationTimeframeStatus
](#RevocationTimeframeStatus)
+
+| Param | Type |
+| --- | --- |
+| json | any
|
+
## SdJwt
@@ -4991,11 +6334,9 @@ Representation of an SD-JWT of the format
* [.jwt()](#SdJwt+jwt) ⇒ string
* [.disclosures()](#SdJwt+disclosures) ⇒ Array.<string>
* [.keyBindingJwt()](#SdJwt+keyBindingJwt) ⇒ string
\| undefined
- * [.toJSON()](#SdJwt+toJSON) ⇒ any
* [.clone()](#SdJwt+clone) ⇒ [SdJwt
](#SdJwt)
* _static_
* [.parse(sd_jwt)](#SdJwt.parse) ⇒ [SdJwt
](#SdJwt)
- * [.fromJSON(json)](#SdJwt.fromJSON) ⇒ [SdJwt
](#SdJwt)
@@ -5038,12 +6379,6 @@ The disclosures part.
### sdJwt.keyBindingJwt() ⇒ string
\| undefined
The optional key binding JWT.
-**Kind**: instance method of [SdJwt
](#SdJwt)
-
-
-### sdJwt.toJSON() ⇒ any
-Serializes this to a JSON object.
-
**Kind**: instance method of [SdJwt
](#SdJwt)
@@ -5065,17 +6400,6 @@ Returns `DeserializationError` if parsing fails.
| --- | --- |
| sd_jwt | string
|
-
-
-### SdJwt.fromJSON(json) ⇒ [SdJwt
](#SdJwt)
-Deserializes an instance from a JSON object.
-
-**Kind**: static method of [SdJwt
](#SdJwt)
-
-| Param | Type |
-| --- | --- |
-| json | any
|
-
## SdJwtCredentialValidator
@@ -5233,7 +6557,6 @@ Note: digests are created using the sha-256 algorithm.
* [SdObjectEncoder](#SdObjectEncoder)
* [new SdObjectEncoder(object)](#new_SdObjectEncoder_new)
* [.conceal(path, [salt])](#SdObjectEncoder+conceal) ⇒ [Disclosure
](#Disclosure)
- * [.concealArrayEntry(path, element_index, [salt])](#SdObjectEncoder+concealArrayEntry) ⇒ [Disclosure
](#Disclosure)
* [.addSdAlgProperty()](#SdObjectEncoder+addSdAlgProperty)
* [.encodeToString()](#SdObjectEncoder+encodeToString) ⇒ string
* [.toString()](#SdObjectEncoder+toString) ⇒ string
@@ -5257,43 +6580,35 @@ Creates a new `SdObjectEncoder` with `sha-256` hash function.
Substitutes a value with the digest of its disclosure.
If no salt is provided, the disclosure will be created with a random salt value.
-The value of the key specified in `path` will be concealed. E.g. for path
-`["claim", "subclaim"]` the value of `claim.subclaim` will be concealed.
-
-## Error
-`InvalidPath` if path is invalid or the path slice is empty.
-`DataTypeMismatch` if existing SD format is invalid.
-
-## Note
-Use `concealArrayEntry` for values in arrays.
-
-**Kind**: instance method of [SdObjectEncoder
](#SdObjectEncoder)
-
-| Param | Type |
-| --- | --- |
-| path | Array.<string>
|
-| [salt] | string
\| undefined
|
+`path` indicates the pointer to the value that will be concealed using the syntax of
+[JSON pointer](https://datatracker.ietf.org/doc/html/rfc6901).
-
+For the following object:
-### sdObjectEncoder.concealArrayEntry(path, element_index, [salt]) ⇒ [Disclosure
](#Disclosure)
-Substitutes a value within an array with the digest of its disclosure.
-If no salt is provided, the disclosure will be created with random salt value.
+ ```
+{
+ "id": "did:value",
+ "claim1": {
+ "abc": true
+ },
+ "claim2": ["val_1", "val_2"]
+}
+```
-`path` is used to specify the array in the object, while `element_index` specifies
-the index of the element to be concealed (index start at 0).
+Path "/id" conceals `"id": "did:value"`
+Path "/claim1/abc" conceals `"abc": true`
+Path "/claim2/0" conceals `val_1`
+```
-## Error
-`InvalidPath` if path is invalid or the path slice is empty.
-`DataTypeMismatch` if existing SD format is invalid.
-`IndexOutofBounds` if `element_index` is out of bounds.
+## Errors
+* `InvalidPath` if pointer is invalid.
+* `DataTypeMismatch` if existing SD format is invalid.
**Kind**: instance method of [SdObjectEncoder
](#SdObjectEncoder)
| Param | Type |
| --- | --- |
-| path | Array.<string>
|
-| element_index | number
|
+| path | string
|
| [salt] | string
\| undefined
|
@@ -5337,9 +6652,96 @@ If path is an empty slice, decoys will be added to the top level.
| Param | Type |
| --- | --- |
-| path | Array.<string>
|
+| path | string
|
| number_of_decoys | number
|
+
+
+## SelectiveDisclosurePresentation
+Used to construct a JwpPresentedBuilder and handle the selective disclosure of attributes
+- @context MUST NOT be blinded
+- id MUST be blinded
+- type MUST NOT be blinded
+- issuer MUST NOT be blinded
+- issuanceDate MUST be blinded (if Timeframe Revocation mechanism is used)
+- expirationDate MUST be blinded (if Timeframe Revocation mechanism is used)
+- credentialSubject (User have to choose which attribute must be blinded)
+- credentialSchema MUST NOT be blinded
+- credentialStatus MUST NOT be blinded
+- refreshService MUST NOT be blinded (probably will be used for Timeslot Revocation mechanism)
+- termsOfUse NO reason to use it in ZK VC (will be in any case blinded)
+- evidence (User have to choose which attribute must be blinded)
+
+**Kind**: global class
+
+* [SelectiveDisclosurePresentation](#SelectiveDisclosurePresentation)
+ * [new SelectiveDisclosurePresentation(issued_jwp)](#new_SelectiveDisclosurePresentation_new)
+ * [.concealInSubject(path)](#SelectiveDisclosurePresentation+concealInSubject)
+ * [.concealInEvidence(path)](#SelectiveDisclosurePresentation+concealInEvidence)
+ * [.setPresentationHeader(header)](#SelectiveDisclosurePresentation+setPresentationHeader)
+
+
+
+### new SelectiveDisclosurePresentation(issued_jwp)
+Initialize a presentation starting from an Issued JWP.
+The properties `jti`, `nbf`, `issuanceDate`, `expirationDate` and `termsOfUse` are concealed by default.
+
+
+| Param | Type |
+| --- | --- |
+| issued_jwp | [JwpIssued
](#JwpIssued) |
+
+
+
+### selectiveDisclosurePresentation.concealInSubject(path)
+Selectively disclose "credentialSubject" attributes.
+# Example
+```
+{
+ "id": 1234,
+ "name": "Alice",
+ "mainCourses": ["Object-oriented Programming", "Mathematics"],
+ "degree": {
+ "type": "BachelorDegree",
+ "name": "Bachelor of Science and Arts",
+ },
+ "GPA": "4.0",
+}
+```
+If you want to undisclose for example the Mathematics course and the name of the degree:
+```
+undisclose_subject("mainCourses[1]");
+undisclose_subject("degree.name");
+```
+
+**Kind**: instance method of [SelectiveDisclosurePresentation
](#SelectiveDisclosurePresentation)
+
+| Param | Type |
+| --- | --- |
+| path | string
|
+
+
+
+### selectiveDisclosurePresentation.concealInEvidence(path)
+Undiscloses "evidence" attributes.
+
+**Kind**: instance method of [SelectiveDisclosurePresentation
](#SelectiveDisclosurePresentation)
+
+| Param | Type |
+| --- | --- |
+| path | string
|
+
+
+
+### selectiveDisclosurePresentation.setPresentationHeader(header)
+Sets presentation protected header.
+
+**Kind**: instance method of [SelectiveDisclosurePresentation
](#SelectiveDisclosurePresentation)
+
+| Param | Type |
+| --- | --- |
+| header | [PresentationProtectedHeader
](#PresentationProtectedHeader) |
+
## Service
@@ -5506,7 +6908,7 @@ A parsed [StatusList2021Credential](https://www.w3.org/TR/2023/WD-vc-status-list
* [new StatusList2021Credential(credential)](#new_StatusList2021Credential_new)
* _instance_
* [.id()](#StatusList2021Credential+id) ⇒ string
- * [.setCredentialStatus(credential, index, value)](#StatusList2021Credential+setCredentialStatus) ⇒ [StatusList2021Entry
](#StatusList2021Entry)
+ * [.setCredentialStatus(credential, index, revoked_or_suspended)](#StatusList2021Credential+setCredentialStatus) ⇒ [StatusList2021Entry
](#StatusList2021Entry)
* [.purpose()](#StatusList2021Credential+purpose) ⇒ [StatusPurpose
](#StatusPurpose)
* [.entry(index)](#StatusList2021Credential+entry) ⇒ [CredentialStatus
](#CredentialStatus)
* [.clone()](#StatusList2021Credential+clone) ⇒ [StatusList2021Credential
](#StatusList2021Credential)
@@ -5530,7 +6932,7 @@ Creates a new [StatusList2021Credential](#StatusList2021Credential).
**Kind**: instance method of [StatusList2021Credential
](#StatusList2021Credential)
-### statusList2021Credential.setCredentialStatus(credential, index, value) ⇒ [StatusList2021Entry
](#StatusList2021Entry)
+### statusList2021Credential.setCredentialStatus(credential, index, revoked_or_suspended) ⇒ [StatusList2021Entry
](#StatusList2021Entry)
Sets the given credential's status using the `index`-th entry of this status list.
Returns the created `credentialStatus`.
@@ -5540,7 +6942,7 @@ Returns the created `credentialStatus`.
| --- | --- |
| credential | [Credential
](#Credential) |
| index | number
|
-| value | boolean
|
+| revoked_or_suspended | boolean
|
@@ -5700,7 +7102,7 @@ Attempts to build a valid [StatusList2021Credential](#StatusList2021Credential)
* [.id()](#StatusList2021Entry+id) ⇒ string
* [.purpose()](#StatusList2021Entry+purpose) ⇒ [StatusPurpose
](#StatusPurpose)
* [.index()](#StatusList2021Entry+index) ⇒ number
- * [.status_list_credential()](#StatusList2021Entry+status_list_credential) ⇒ string
+ * [.statusListCredential()](#StatusList2021Entry+statusListCredential) ⇒ string
* [.toStatus()](#StatusList2021Entry+toStatus) ⇒ Status
* [.clone()](#StatusList2021Entry+clone) ⇒ [StatusList2021Entry
](#StatusList2021Entry)
* [.toJSON()](#StatusList2021Entry+toJSON) ⇒ any
@@ -5738,9 +7140,9 @@ Returns the purpose of this entry.
Returns the index of this entry.
**Kind**: instance method of [StatusList2021Entry
](#StatusList2021Entry)
-
+
-### statusList2021Entry.status\_list\_credential() ⇒ string
+### statusList2021Entry.statusListCredential() ⇒ string
Returns the referenced [StatusList2021Credential](#StatusList2021Credential)'s url.
**Kind**: instance method of [StatusList2021Entry
](#StatusList2021Entry)
@@ -5961,6 +7363,7 @@ A DID Document Verification Method.
**Kind**: global class
* [VerificationMethod](#VerificationMethod)
+ * [new VerificationMethod(id, controller, type_, data)](#new_VerificationMethod_new)
* _instance_
* [.id()](#VerificationMethod+id) ⇒ [DIDUrl
](#DIDUrl)
* [.setId(id)](#VerificationMethod+setId)
@@ -5978,6 +7381,19 @@ A DID Document Verification Method.
* [.newFromJwk(did, key, [fragment])](#VerificationMethod.newFromJwk) ⇒ [VerificationMethod
](#VerificationMethod)
* [.fromJSON(json)](#VerificationMethod.fromJSON) ⇒ [VerificationMethod
](#VerificationMethod)
+
+
+### new VerificationMethod(id, controller, type_, data)
+Create a custom [VerificationMethod](#VerificationMethod).
+
+
+| Param | Type |
+| --- | --- |
+| id | [DIDUrl
](#DIDUrl) |
+| controller | [CoreDID
](#CoreDID) |
+| type_ | [MethodType
](#MethodType) |
+| data | [MethodData
](#MethodData) |
+
### verificationMethod.id() ⇒ [DIDUrl
](#DIDUrl)
@@ -6113,15 +7529,46 @@ Deserializes an instance from a JSON object.
| --- | --- |
| json | any
|
-
-## StateMetadataEncoding
**Kind**: global variable
-
+
+
+## StatusCheck
+Controls validation behaviour when checking whether or not a credential has been revoked by its
+[`credentialStatus`](https://www.w3.org/TR/vc-data-model/#status).
+
+**Kind**: global variable
+
+
+## Strict
+Validate the status if supported, reject any unsupported
+[`credentialStatus`](https://www.w3.org/TR/vc-data-model/#status) types.
+
+Only `RevocationBitmap2022` is currently supported.
+
+This is the default.
+
+**Kind**: global variable
+
+
+## SkipUnsupported
+Validate the status if supported, skip any unsupported
+[`credentialStatus`](https://www.w3.org/TR/vc-data-model/#status) types.
+
+**Kind**: global variable
+
+
+## SkipAll
+Skip all status checks.
+
+**Kind**: global variable
+
-## StatusPurpose
-Purpose of a [StatusList2021](#StatusList2021).
+## SerializationType
+**Kind**: global variable
+
+## MethodRelationship
**Kind**: global variable
@@ -6149,6 +7596,11 @@ The holder must match the subject only for credentials where the [`nonTransferab
## Any
The holder is not required to have any kind of relationship to any credential subject.
+## StateMetadataEncoding
+**Kind**: global variable
+
+
+## StateMetadataEncoding
**Kind**: global variable
@@ -6168,44 +7620,7 @@ Return all errors that occur during validation.
Return after the first error occurs.
**Kind**: global variable
-
-
-## StatusCheck
-Controls validation behaviour when checking whether or not a credential has been revoked by its
-[`credentialStatus`](https://www.w3.org/TR/vc-data-model/#status).
-
-**Kind**: global variable
-
-
-## Strict
-Validate the status if supported, reject any unsupported
-[`credentialStatus`](https://www.w3.org/TR/vc-data-model/#status) types.
-
-Only `RevocationBitmap2022` is currently supported.
-
-This is the default.
-
-**Kind**: global variable
-
-
-## SkipUnsupported
-Validate the status if supported, skip any unsupported
-[`credentialStatus`](https://www.w3.org/TR/vc-data-model/#status) types.
-
-**Kind**: global variable
-
-
-## SkipAll
-Skip all status checks.
-
-**Kind**: global variable
-
-
-## CredentialStatus
-**Kind**: global variable
-
-## MethodRelationship
**Kind**: global variable
@@ -6229,6 +7644,12 @@ prior to calling the function.
| decodedSignature | Uint8Array
|
| publicKey | [Jwk
](#Jwk) |
+
+
+## start()
+Initializes the console error panic hook for better error messages
+
+**Kind**: global function
## encodeB64(data) ⇒ string
diff --git a/bindings/wasm/examples/src/1_advanced/6_sd_jwt.ts b/bindings/wasm/examples/src/1_advanced/6_sd_jwt.ts
index 397f608ee3..e389118f8c 100644
--- a/bindings/wasm/examples/src/1_advanced/6_sd_jwt.ts
+++ b/bindings/wasm/examples/src/1_advanced/6_sd_jwt.ts
@@ -99,16 +99,16 @@ export async function sdJwt() {
// Make "locality", "postal_code", "street_address" and the first entry of "nationalities"
// selectively disclosable while keeping other properties in plain text.
let disclosures = [
- encoder.conceal(["vc", "credentialSubject", "address", "locality"]),
- encoder.conceal(["vc", "credentialSubject", "address", "postal_code"]),
- encoder.conceal(["vc", "credentialSubject", "address", "street_address"]),
- encoder.concealArrayEntry(["vc", "credentialSubject", "nationalities"], 1),
+ encoder.conceal("/vc/credentialSubject/address/locality"),
+ encoder.conceal("/vc/credentialSubject/address/postal_code"),
+ encoder.conceal("/vc/credentialSubject/address/street_address"),
+ encoder.conceal("/vc/credentialSubject/nationalities/1"),
];
// Add decoys in the credential top level, nationalities array and address object.
- encoder.addDecoys(["vc", "credentialSubject", "nationalities"], 3);
- encoder.addDecoys(["vc"], 4);
- encoder.addDecoys(["vc", "credentialSubject", "address"], 2);
+ encoder.addDecoys("/vc/credentialSubject/nationalities", 3);
+ encoder.addDecoys("/vc", 4);
+ encoder.addDecoys("/vc/credentialSubject/address", 2);
// Add the `_sd_alg` property.
encoder.addSdAlgProperty();
diff --git a/bindings/wasm/examples/src/1_advanced/8_zkp.ts b/bindings/wasm/examples/src/1_advanced/8_zkp.ts
new file mode 100644
index 0000000000..55d0c82fca
--- /dev/null
+++ b/bindings/wasm/examples/src/1_advanced/8_zkp.ts
@@ -0,0 +1,226 @@
+import {
+ Credential,
+ FailFast,
+ IotaDID,
+ IotaDocument,
+ IotaIdentityClient,
+ JptCredentialValidationOptions,
+ JptCredentialValidator,
+ JptCredentialValidatorUtils,
+ JptPresentationValidationOptions,
+ JptPresentationValidator,
+ JptPresentationValidatorUtils,
+ JwkMemStore,
+ JwpCredentialOptions,
+ JwpPresentationOptions,
+ KeyIdMemStore,
+ MethodScope,
+ ProofAlgorithm,
+ SelectiveDisclosurePresentation,
+ Storage,
+} from "@iota/identity-wasm/node";
+import {
+ type Address,
+ AliasOutput,
+ Client,
+ MnemonicSecretManager,
+ SecretManager,
+ SecretManagerType,
+ Utils,
+} from "@iota/sdk-wasm/node";
+import { API_ENDPOINT, ensureAddressHasFunds } 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.BLS12381_SHA256,
+ 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.
+ // ===========================================================================
+
+ // Create a new client to interact with the IOTA ledger.
+ const client = new Client({
+ primaryNode: API_ENDPOINT,
+ localPow: true,
+ });
+
+ // Creates a new wallet and identity (see "0_create_did" example).
+ const issuerSecretManager: MnemonicSecretManager = {
+ mnemonic: Utils.generateMnemonic(),
+ };
+ const issuerStorage: Storage = new Storage(
+ new JwkMemStore(),
+ new KeyIdMemStore(),
+ );
+ let { document: issuerDocument, fragment: issuerFragment } = await createDid(
+ client,
+ issuerSecretManager,
+ issuerStorage,
+ );
+
+ // ===========================================================================
+ // Step 2: Issuer creates and signs a Verifiable Credential with BBS algorithm.
+ // ===========================================================================
+
+ // Create a credential subject indicating the degree earned by Alice.
+ const subject = {
+ name: "Alice",
+ mainCourses: ["Object-oriented Programming", "Mathematics"],
+ degree: {
+ type: "BachelorDegree",
+ name: "Bachelor of Science and Arts",
+ },
+ GPA: 4.0,
+ };
+
+ // Build credential using the above subject and issuer.
+ const credential = new Credential({
+ id: "https:/example.edu/credentials/3732",
+ issuer: issuerDocument.id(),
+ type: "UniversityDegreeCredential",
+ credentialSubject: subject,
+ });
+ const credentialJpt = await issuerDocument
+ .createCredentialJpt(
+ credential,
+ issuerStorage,
+ issuerFragment,
+ new JwpCredentialOptions(),
+ );
+ // Validate the credential's proof using the issuer's DID Document, the credential's semantic structure,
+ // that the issuance date is not in the future and that the expiration date is not in the past:
+ const decodedJpt = JptCredentialValidator.validate(
+ credentialJpt,
+ issuerDocument,
+ new JptCredentialValidationOptions(),
+ FailFast.FirstError,
+ );
+
+ // ===========================================================================
+ // Step 3: Issuer sends the Verifiable Credential to the holder.
+ // ===========================================================================
+ console.log("Sending credential (as JPT) to the holder: " + credentialJpt.toString());
+
+ // ============================================================================================
+ // Step 4: Holder resolve Issuer's DID, retrieve Issuer's document and validate the Credential
+ // ============================================================================================
+ const identityClient = new IotaIdentityClient(client);
+
+ // Holder resolves issuer's DID.
+ let issuerDid = IotaDID.parse(JptCredentialValidatorUtils.extractIssuerFromIssuedJpt(credentialJpt).toString());
+ let issuerDoc = await identityClient.resolveDid(issuerDid);
+
+ // Holder validates the credential and retrieve the JwpIssued, needed to construct the JwpPresented
+ let decodedCredential = JptCredentialValidator.validate(
+ credentialJpt,
+ issuerDoc,
+ new JptCredentialValidationOptions(),
+ FailFast.FirstError,
+ );
+
+ // ===========================================================================
+ // Step 5: Verifier sends the holder a challenge and requests a Presentation.
+ //
+ // Please be aware that when we mention "Presentation," we are not alluding to the Verifiable Presentation standard as defined by W3C (https://www.w3.org/TR/vc-data-model/#presentations).
+ // Instead, our reference is to a JWP Presentation (https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-proof#name-presented-form), which differs from the W3C standard.
+ // ===========================================================================
+
+ // A unique random challenge generated by the requester per presentation can mitigate replay attacks.
+ const challenge = "475a7984-1bb5-4c4c-a56f-822bccd46440";
+
+ // =========================================================================================================
+ // Step 6: Holder engages in the Selective Disclosure of credential's attributes.
+ // =========================================================================================================
+ const methodId = decodedCredential
+ .decodedJwp()
+ .getIssuerProtectedHeader()
+ .kid!;
+ const selectiveDisclosurePresentation = new SelectiveDisclosurePresentation(decodedCredential.decodedJwp());
+ selectiveDisclosurePresentation.concealInSubject("mainCourses[1]");
+ selectiveDisclosurePresentation.concealInSubject("degree.name");
+
+ // =======================================================================================================================================
+ // Step 7: Holder needs Issuer's Public Key to compute the Signature Proof of Knowledge and construct the Presentation
+ // JPT.
+ // =======================================================================================================================================
+
+ // Construct a JPT(JWP in the Presentation form) representing the Selectively Disclosed Verifiable Credential
+ const presentationOptions = new JwpPresentationOptions();
+ presentationOptions.nonce = challenge;
+ const presentationJpt = await issuerDoc
+ .createPresentationJpt(
+ selectiveDisclosurePresentation,
+ methodId,
+ presentationOptions,
+ );
+
+ // ===========================================================================
+ // Step 8: Holder sends a Presentation JPT to the Verifier.
+ // ===========================================================================
+
+ console.log("Sending presentation (as JPT) to the verifier: " + presentationJpt.toString());
+
+ // ===========================================================================
+ // Step 9: Verifier receives the Presentation and verifies it.
+ // ===========================================================================
+
+ // Verifier resolve Issuer DID
+ const issuerDidV = IotaDID.parse(
+ JptPresentationValidatorUtils.extractIssuerFromPresentedJpt(presentationJpt).toString(),
+ );
+ const issuerDocV = await identityClient.resolveDid(issuerDidV);
+
+ const presentationValidationOptions = new JptPresentationValidationOptions({ nonce: challenge });
+ const decodedPresentedCredential = JptPresentationValidator.validate(
+ presentationJpt,
+ issuerDocV,
+ presentationValidationOptions,
+ FailFast.FirstError,
+ );
+
+ console.log("Presented credential successfully validated: " + decodedPresentedCredential.credential());
+}
diff --git a/bindings/wasm/examples/src/1_advanced/9_zkp_revocation.ts b/bindings/wasm/examples/src/1_advanced/9_zkp_revocation.ts
new file mode 100644
index 0000000000..e8c3d586a1
--- /dev/null
+++ b/bindings/wasm/examples/src/1_advanced/9_zkp_revocation.ts
@@ -0,0 +1,281 @@
+import {
+ Credential,
+ Duration,
+ FailFast,
+ IotaDID,
+ IotaDocument,
+ IotaIdentityClient,
+ JptCredentialValidationOptions,
+ JptCredentialValidator,
+ JptCredentialValidatorUtils,
+ JptPresentationValidationOptions,
+ JptPresentationValidator,
+ JptPresentationValidatorUtils,
+ JwkMemStore,
+ JwpCredentialOptions,
+ JwpPresentationOptions,
+ KeyIdMemStore,
+ MethodScope,
+ ProofAlgorithm,
+ RevocationBitmap,
+ RevocationTimeframeStatus,
+ SelectiveDisclosurePresentation,
+ Status,
+ StatusCheck,
+ Storage,
+ Timestamp,
+} from "@iota/identity-wasm/node";
+import {
+ type Address,
+ AliasOutput,
+ Client,
+ MnemonicSecretManager,
+ SecretManager,
+ SecretManagerType,
+ Utils,
+} from "@iota/sdk-wasm/node";
+import { API_ENDPOINT, ensureAddressHasFunds } 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.BLS12381_SHA256,
+ undefined,
+ MethodScope.VerificationMethod(),
+ );
+ const revocationBitmap = new RevocationBitmap();
+ const serviceId = document.id().toUrl().join("#my-revocation-service");
+ const service = revocationBitmap.toService(serviceId);
+
+ document.insertService(service);
+ // 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_revocation() {
+ // Create a new client to interact with the IOTA ledger.
+ const client = new Client({
+ primaryNode: API_ENDPOINT,
+ localPow: true,
+ });
+
+ // Creates a new wallet and identity (see "0_create_did" example).
+ const issuerSecretManager: MnemonicSecretManager = {
+ mnemonic: Utils.generateMnemonic(),
+ };
+ const issuerStorage: Storage = new Storage(
+ new JwkMemStore(),
+ new KeyIdMemStore(),
+ );
+ let { document: issuerDocument, fragment: issuerFragment } = await createDid(
+ client,
+ issuerSecretManager,
+ issuerStorage,
+ );
+ const holderSecretManager: MnemonicSecretManager = {
+ mnemonic: Utils.generateMnemonic(),
+ };
+ const holderStorage: Storage = new Storage(
+ new JwkMemStore(),
+ new KeyIdMemStore(),
+ );
+ let { document: holderDocument, fragment: holderFragment } = await createDid(
+ client,
+ holderSecretManager,
+ holderStorage,
+ );
+ // =========================================================================================
+ // Step 1: Create a new RevocationTimeframeStatus containing the current validityTimeframe
+ // =======================================================================================
+
+ const timeframeId = issuerDocument.id().toUrl().join("#my-revocation-service");
+ let revocationTimeframeStatus = new RevocationTimeframeStatus(
+ timeframeId.toString(),
+ 5,
+ Duration.minutes(1),
+ Timestamp.nowUTC(),
+ );
+
+ // Create a credential subject indicating the degree earned by Alice.
+ const subject = {
+ name: "Alice",
+ mainCourses: ["Object-oriented Programming", "Mathematics"],
+ degree: {
+ type: "BachelorDegree",
+ name: "Bachelor of Science and Arts",
+ },
+ GPA: 4.0,
+ };
+
+ // Build credential using the above subject and issuer.
+ const credential = new Credential({
+ id: "https:/example.edu/credentials/3732",
+ issuer: issuerDocument.id(),
+ type: "UniversityDegreeCredential",
+ credentialSubject: subject,
+ credentialStatus: revocationTimeframeStatus as any as Status,
+ });
+ const credentialJpt = await issuerDocument
+ .createCredentialJpt(
+ credential,
+ issuerStorage,
+ issuerFragment,
+ new JwpCredentialOptions(),
+ );
+ // Validate the credential's proof using the issuer's DID Document, the credential's semantic structure,
+ // that the issuance date is not in the future and that the expiration date is not in the past:
+ const decodedJpt = JptCredentialValidator.validate(
+ credentialJpt,
+ issuerDocument,
+ new JptCredentialValidationOptions(),
+ FailFast.FirstError,
+ );
+
+ console.log("Sending credential (as JPT) to the holder: " + credentialJpt.toString());
+
+ // Holder validates the credential and retrieve the JwpIssued, needed to construct the JwpPresented
+ let decodedCredential = JptCredentialValidator.validate(
+ credentialJpt,
+ issuerDocument,
+ new JptCredentialValidationOptions(),
+ FailFast.FirstError,
+ );
+
+ // ===========================================================================
+ // Credential's Status check
+ // ===========================================================================
+ JptCredentialValidatorUtils.checkTimeframesAndRevocationWithValidityTimeframe2024(
+ decodedCredential.credential(),
+ issuerDocument,
+ undefined,
+ StatusCheck.Strict,
+ );
+
+ // A unique random challenge generated by the requester per presentation can mitigate replay attacks.
+ const challenge = "475a7984-1bb5-4c4c-a56f-822bccd46440";
+
+ const methodId = decodedCredential
+ .decodedJwp()
+ .getIssuerProtectedHeader()
+ .kid!;
+
+ const selectiveDisclosurePresentation = new SelectiveDisclosurePresentation(decodedCredential.decodedJwp());
+ selectiveDisclosurePresentation.concealInSubject("mainCourses[1]");
+ selectiveDisclosurePresentation.concealInSubject("degree.name");
+
+ // Construct a JPT(JWP in the Presentation form) representing the Selectively Disclosed Verifiable Credential
+ const presentationOptions = new JwpPresentationOptions();
+ presentationOptions.nonce = challenge;
+ const presentationJpt = await issuerDocument
+ .createPresentationJpt(
+ selectiveDisclosurePresentation,
+ methodId,
+ presentationOptions,
+ );
+
+ console.log("Sending presentation (as JPT) to the verifier: " + presentationJpt.toString());
+
+ // ===========================================================================
+ // Step 2: Verifier receives the Presentation and verifies it.
+ // ===========================================================================
+
+ const presentationValidationOptions = new JptPresentationValidationOptions({ nonce: challenge });
+ const decodedPresentedCredential = JptPresentationValidator.validate(
+ presentationJpt,
+ issuerDocument,
+ presentationValidationOptions,
+ FailFast.FirstError,
+ );
+
+ JptPresentationValidatorUtils.checkTimeframesWithValidityTimeframe2024(
+ decodedPresentedCredential.credential(),
+ undefined,
+ StatusCheck.Strict,
+ );
+
+ console.log("Presented credential successfully validated: " + decodedPresentedCredential.credential());
+
+ // ===========================================================================
+ // Step 2b: Waiting for the next validityTimeframe, will result in the Credential timeframe interval NOT valid
+ // ===========================================================================
+
+ try {
+ const now = new Date();
+ const timeInTwoMinutes = new Date(now.setMinutes(now.getMinutes() + 2));
+ JptPresentationValidatorUtils.checkTimeframesWithValidityTimeframe2024(
+ decodedPresentedCredential.credential(),
+ Timestamp.parse(timeInTwoMinutes.toISOString()),
+ StatusCheck.Strict,
+ );
+ } catch (_) {
+ console.log("successfully expired!");
+ }
+
+ // ===========================================================================
+ // Issuer decides to Revoke Holder's Credential
+ // ===========================================================================
+
+ console.log("Issuer decides to revoke the Credential");
+
+ const identityClient = new IotaIdentityClient(client);
+
+ // Update the RevocationBitmap service in the issuer's DID Document.
+ // This revokes the credential's unique index.
+ issuerDocument.revokeCredentials("my-revocation-service", 5);
+ let aliasOutput = await identityClient.updateDidOutput(issuerDocument);
+ const rent = await identityClient.getRentStructure();
+ aliasOutput = await client.buildAliasOutput({
+ ...aliasOutput,
+ amount: Utils.computeStorageDeposit(aliasOutput, rent),
+ aliasId: aliasOutput.getAliasId(),
+ unlockConditions: aliasOutput.getUnlockConditions(),
+ });
+ issuerDocument = await identityClient.publishDidOutput(issuerSecretManager, aliasOutput);
+
+ // Holder checks if his credential has been revoked by the Issuer
+ try {
+ JptCredentialValidatorUtils.checkRevocationWithValidityTimeframe2024(
+ decodedCredential.credential(),
+ issuerDocument,
+ StatusCheck.Strict,
+ );
+ } catch (_) {
+ console.log("Credential revoked!");
+ }
+}
diff --git a/bindings/wasm/examples/src/main.ts b/bindings/wasm/examples/src/main.ts
index 145980e649..0a074d3fd2 100644
--- a/bindings/wasm/examples/src/main.ts
+++ b/bindings/wasm/examples/src/main.ts
@@ -17,6 +17,8 @@ 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";
async function main() {
// Extract example name.
@@ -58,6 +60,10 @@ async function main() {
return await sdJwt();
case "7_status_list_2021":
return await statusList2021();
+ case "8_zkp":
+ return await zkp();
+ case "9_zkp_revocation":
+ return await zkp_revocation();
default:
throw "Unknown example name: '" + argument + "'";
}
diff --git a/bindings/wasm/examples/src/tests/8_zkp.ts b/bindings/wasm/examples/src/tests/8_zkp.ts
new file mode 100644
index 0000000000..52d5b72bc4
--- /dev/null
+++ b/bindings/wasm/examples/src/tests/8_zkp.ts
@@ -0,0 +1,8 @@
+import { zkp } from "../1_advanced/8_zkp";
+
+// Only verifies that no uncaught exceptions are thrown, including syntax errors etc.
+describe("Test node examples", function() {
+ it("zkp", async () => {
+ await zkp();
+ });
+});
diff --git a/bindings/wasm/examples/src/tests/9_zkp_revocation.ts b/bindings/wasm/examples/src/tests/9_zkp_revocation.ts
new file mode 100644
index 0000000000..96075765f3
--- /dev/null
+++ b/bindings/wasm/examples/src/tests/9_zkp_revocation.ts
@@ -0,0 +1,8 @@
+import { zkp_revocation } from "../1_advanced/9_zkp_revocation";
+
+// Only verifies that no uncaught exceptions are thrown, including syntax errors etc.
+describe("Test node examples", function() {
+ it("zkp_revocation", async () => {
+ await zkp_revocation();
+ });
+});
diff --git a/bindings/wasm/examples/src/util.ts b/bindings/wasm/examples/src/util.ts
index 41a0a5a7ee..3fc2be116e 100644
--- a/bindings/wasm/examples/src/util.ts
+++ b/bindings/wasm/examples/src/util.ts
@@ -16,8 +16,8 @@ import {
Utils,
} from "@iota/sdk-wasm/node";
-export const API_ENDPOINT = "http://localhost:14265";
-export const FAUCET_ENDPOINT = "http://localhost:8091/api/enqueue";
+export const API_ENDPOINT = "http://localhost";
+export const FAUCET_ENDPOINT = "http://localhost/faucet/api/enqueue";
/** Creates a DID Document and publishes it in a new Alias Output.
diff --git a/bindings/wasm/lib/jwk_storage.ts b/bindings/wasm/lib/jwk_storage.ts
index 2c1156e5ac..235abcc8ce 100644
--- a/bindings/wasm/lib/jwk_storage.ts
+++ b/bindings/wasm/lib/jwk_storage.ts
@@ -1,5 +1,5 @@
import * as ed from "@noble/ed25519";
-import { decodeB64, encodeB64, Jwk, JwkGenOutput, JwkStorage } from "~identity_wasm";
+import { decodeB64, encodeB64, Jwk, JwkGenOutput, JwkStorage, ProofAlgorithm, ProofUpdateCtx } from "~identity_wasm";
import { EdCurve, JwkType, JwsAlgorithm } from "./jose";
type Ed25519PrivateKey = Uint8Array;
@@ -18,6 +18,10 @@ export class JwkMemStore implements JwkStorage {
return "Ed25519";
}
+ private _get_key(keyId: string): Jwk | undefined {
+ return this._keys.get(keyId);
+ }
+
public async generate(keyType: string, algorithm: JwsAlgorithm): Promise {
if (keyType !== JwkMemStore.ed25519KeyType()) {
throw new Error(`unsupported key type ${keyType}`);
@@ -126,6 +130,23 @@ function decodeJwk(jwk: Jwk): [Ed25519PrivateKey, Ed25519PublicKey] {
}
}
+export interface JwkStorageBBSPlusExt {
+ // Generate a new BLS12381 key represented as a JSON Web Key.
+ generateBBS: (algorithm: ProofAlgorithm) => Promise;
+ /** Signs a chunk of data together with an optional header
+ * using the private key corresponding to the given `keyId` and according
+ * to `publicKey`'s requirements.
+ */
+ signBBS: (keyId: string, data: Uint8Array[], publicKey: Jwk, header?: Uint8Array) => Promise;
+ // Updates the timeframe validity period information of a given signature.
+ updateBBSSignature: (
+ keyId: string,
+ publicKey: Jwk,
+ signature: Uint8Array,
+ proofCtx: ProofUpdateCtx,
+ ) => Promise;
+}
+
// 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.
diff --git a/bindings/wasm/package-lock.json b/bindings/wasm/package-lock.json
index d8d0f1b22c..ebdd96ae40 100644
--- a/bindings/wasm/package-lock.json
+++ b/bindings/wasm/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@iota/identity-wasm",
- "version": "1.0.0",
+ "version": "1.3.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@iota/identity-wasm",
- "version": "1.0.0",
+ "version": "1.3.0",
"license": "Apache-2.0",
"dependencies": {
"@noble/ed25519": "^1.7.3",
diff --git a/bindings/wasm/package.json b/bindings/wasm/package.json
index 7f00d683d8..9faa31edc8 100644
--- a/bindings/wasm/package.json
+++ b/bindings/wasm/package.json
@@ -1,6 +1,6 @@
{
"name": "@iota/identity-wasm",
- "version": "1.0.0",
+ "version": "1.3.0",
"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",
"repository": {
"type": "git",
@@ -15,7 +15,7 @@
"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",
- "build:docs": "node ./build/docs",
+ "build:docs": "npm run fix_js_doc && node ./build/docs",
"build:examples:web": "tsc --project ./examples/tsconfig.web.json && node ./build/replace_paths ./examples/tsconfig.web.json ./examples/dist resolve",
"build": "npm run build:web && npm run build:nodejs && npm run build:docs",
"example:node": "ts-node --project tsconfig.node.json -r tsconfig-paths/register ./examples/src/main.ts",
@@ -25,9 +25,11 @@
"test:browser:parallel": "npm run build:examples:web && cypress-parallel -s test:browser -t 4 -d cypress/e2e -a '\"--quiet\"'",
"test:browser": "cypress run --headless",
"test:readme": "mocha ./tests/txm_readme.js --retries 3 --timeout 180000 --exit",
+ "test:readme:rust": "mocha ./tests/txm_readme_rust.js --retries 3 --timeout 360000 --exit",
"test:unit:node": "ts-mocha -p tsconfig.node.json ./tests/*.ts --parallel --exit",
"cypress": "cypress open",
- "fmt": "dprint fmt"
+ "fmt": "dprint fmt",
+ "fix_js_doc": "sed -Ei 's/\\((.*)\\)\\[\\]/\\1\\[\\]/' ./node/identity_wasm.js"
},
"config": {
"CYPRESS_VERIFY_TIMEOUT": 100000
diff --git a/bindings/wasm/src/common/types.rs b/bindings/wasm/src/common/types.rs
index 295e0ea447..8264e923ce 100644
--- a/bindings/wasm/src/common/types.rs
+++ b/bindings/wasm/src/common/types.rs
@@ -75,3 +75,9 @@ impl TryFrom<&Object> for MapStringAny {
Ok(map.unchecked_into::())
}
}
+
+impl Default for MapStringAny {
+ fn default() -> Self {
+ js_sys::Map::new().unchecked_into()
+ }
+}
diff --git a/bindings/wasm/src/credential/jpt.rs b/bindings/wasm/src/credential/jpt.rs
new file mode 100644
index 0000000000..e3e3daab2b
--- /dev/null
+++ b/bindings/wasm/src/credential/jpt.rs
@@ -0,0 +1,45 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+use identity_iota::credential::Jpt;
+use wasm_bindgen::prelude::*;
+
+/// A JSON Proof Token (JPT).
+#[wasm_bindgen(js_name = Jpt)]
+pub struct WasmJpt(pub(crate) Jpt);
+
+#[wasm_bindgen(js_class = Jpt)]
+impl WasmJpt {
+ /// Creates a new {@link Jpt}.
+ #[wasm_bindgen(constructor)]
+ pub fn new(jpt_string: String) -> Self {
+ WasmJpt(Jpt::new(jpt_string))
+ }
+
+ // Returns the string representation for this {@link Jpt}.
+ #[allow(clippy::inherent_to_string)]
+ #[wasm_bindgen(js_name = "toString")]
+ pub fn to_string(&self) -> String {
+ self.0.as_str().to_owned()
+ }
+}
+
+impl_wasm_clone!(WasmJpt, Jpt);
+
+impl From for WasmJpt {
+ fn from(value: Jpt) -> Self {
+ WasmJpt(value)
+ }
+}
+
+impl From for Jpt {
+ fn from(value: WasmJpt) -> Self {
+ value.0
+ }
+}
+
+#[wasm_bindgen]
+extern "C" {
+ #[wasm_bindgen(typescript_type = "Promise")]
+ pub type PromiseJpt;
+}
diff --git a/bindings/wasm/src/credential/jpt_credential_validator/decoded_jpt_credential.rs b/bindings/wasm/src/credential/jpt_credential_validator/decoded_jpt_credential.rs
new file mode 100644
index 0000000000..46c999a40f
--- /dev/null
+++ b/bindings/wasm/src/credential/jpt_credential_validator/decoded_jpt_credential.rs
@@ -0,0 +1,52 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+use identity_iota::core::Object;
+use identity_iota::credential::DecodedJptCredential;
+use wasm_bindgen::prelude::*;
+
+use crate::common::MapStringAny;
+use crate::credential::WasmCredential;
+use crate::error::Result;
+use crate::jpt::WasmJwpIssued;
+
+#[wasm_bindgen(js_name = DecodedJptCredential)]
+pub struct WasmDecodedJptCredential(pub(crate) DecodedJptCredential);
+
+impl_wasm_clone!(WasmDecodedJptCredential, DecodedJptCredential);
+
+#[wasm_bindgen(js_class = DecodedJptCredential)]
+impl WasmDecodedJptCredential {
+ /// Returns the {@link Credential} embedded into this JPT.
+ #[wasm_bindgen]
+ pub fn credential(&self) -> WasmCredential {
+ WasmCredential(self.0.credential.clone())
+ }
+
+ /// Returns the custom claims parsed from the JPT.
+ #[wasm_bindgen(js_name = "customClaims")]
+ pub fn custom_claims(&self) -> Result {
+ match self.0.custom_claims.clone() {
+ Some(obj) => MapStringAny::try_from(obj),
+ None => Ok(MapStringAny::default()),
+ }
+ }
+
+ // The decoded and verified issued JWP, will be used to construct the presented JWP.
+ #[wasm_bindgen(js_name = decodedJwp)]
+ pub fn decoded_jwp(&self) -> WasmJwpIssued {
+ WasmJwpIssued(self.0.decoded_jwp.clone())
+ }
+}
+
+impl From for WasmDecodedJptCredential {
+ fn from(value: DecodedJptCredential) -> Self {
+ WasmDecodedJptCredential(value)
+ }
+}
+
+impl From for DecodedJptCredential {
+ fn from(value: WasmDecodedJptCredential) -> Self {
+ value.0
+ }
+}
diff --git a/bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validation_options.rs b/bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validation_options.rs
new file mode 100644
index 0000000000..aefc6ec443
--- /dev/null
+++ b/bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validation_options.rs
@@ -0,0 +1,80 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+use identity_iota::credential::JptCredentialValidationOptions;
+use wasm_bindgen::prelude::*;
+
+use crate::error::Result;
+use crate::error::WasmResult;
+
+/// Options to declare validation criteria for {@link Jpt}.
+#[derive(Debug, Default, Clone)]
+#[wasm_bindgen(js_name = "JptCredentialValidationOptions", inspectable)]
+pub struct WasmJptCredentialValidationOptions(pub(crate) JptCredentialValidationOptions);
+
+impl_wasm_clone!(WasmJptCredentialValidationOptions, JptCredentialValidationOptions);
+impl_wasm_json!(WasmJptCredentialValidationOptions, JptCredentialValidationOptions);
+
+#[wasm_bindgen(js_class = JptCredentialValidationOptions)]
+impl WasmJptCredentialValidationOptions {
+ /// Creates a new default istance.
+ #[wasm_bindgen(constructor)]
+ pub fn new(opts: Option) -> Result {
+ if let Some(opts) = opts {
+ opts.into_serde().wasm_result().map(WasmJptCredentialValidationOptions)
+ } else {
+ Ok(WasmJptCredentialValidationOptions::default())
+ }
+ }
+}
+
+impl From for WasmJptCredentialValidationOptions {
+ fn from(value: JptCredentialValidationOptions) -> Self {
+ WasmJptCredentialValidationOptions(value)
+ }
+}
+
+impl From for JptCredentialValidationOptions {
+ fn from(value: WasmJptCredentialValidationOptions) -> Self {
+ value.0
+ }
+}
+
+#[wasm_bindgen]
+extern "C" {
+ #[wasm_bindgen(typescript_type = "IJptCredentialValidationOptions")]
+ pub type IJptCredentialValidationOptions;
+}
+
+#[wasm_bindgen(typescript_custom_section)]
+const I_JPT_CREDENTIAL_VALIDATION_OPTIONS: &'static str = r#"
+/** Holds options to create a new {@link JptCredentialValidationOptions}. */
+interface IJptCredentialValidationOptions {
+ /**
+ * Declare that the credential is **not** considered valid if it expires before this {@link Timestamp}.
+ * Uses the current datetime during validation if not set.
+ */
+ readonly earliestExpiryDate?: Timestamp;
+
+ /**
+ * Declare that the credential is **not** considered valid if it was issued later than this {@link Timestamp}.
+ * Uses the current datetime during validation if not set.
+ */
+ readonly latestIssuanceDate?: Timestamp;
+
+ /**
+ * Validation behaviour for [`credentialStatus`](https://www.w3.org/TR/vc-data-model/#status).
+ */
+ readonly status?: StatusCheck;
+
+ /** Declares how credential subjects must relate to the presentation holder during validation.
+ *
+ *
+ */
+ readonly subjectHolderRelationship?: [string, SubjectHolderRelationship];
+
+ /**
+ * Options which affect the verification of the proof on the credential.
+ */
+ readonly verificationOptions?: JwpVerificationOptions;
+}"#;
diff --git a/bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validator.rs b/bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validator.rs
new file mode 100644
index 0000000000..10876fe96f
--- /dev/null
+++ b/bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validator.rs
@@ -0,0 +1,33 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+use crate::common::ImportedDocumentLock;
+use crate::credential::WasmDecodedJptCredential;
+use crate::credential::WasmFailFast;
+use crate::credential::WasmJpt;
+use crate::credential::WasmJptCredentialValidationOptions;
+use crate::did::IToCoreDocument;
+use crate::error::Result;
+use crate::error::WasmResult;
+use identity_iota::credential::JptCredentialValidator;
+use wasm_bindgen::prelude::*;
+
+#[wasm_bindgen(js_name = JptCredentialValidator)]
+pub struct WasmJptCredentialValidator;
+
+#[wasm_bindgen(js_class = JptCredentialValidator)]
+impl WasmJptCredentialValidator {
+ #[wasm_bindgen]
+ pub fn validate(
+ credential_jpt: &WasmJpt,
+ issuer: &IToCoreDocument,
+ options: &WasmJptCredentialValidationOptions,
+ fail_fast: WasmFailFast,
+ ) -> Result {
+ let issuer_lock = ImportedDocumentLock::from(issuer);
+ let issuer_guard = issuer_lock.try_read()?;
+ JptCredentialValidator::validate(&credential_jpt.0, &issuer_guard, &options.0, fail_fast.into())
+ .wasm_result()
+ .map(WasmDecodedJptCredential)
+ }
+}
diff --git a/bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validator_utils.rs b/bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validator_utils.rs
new file mode 100644
index 0000000000..cfdf9c6e9e
--- /dev/null
+++ b/bindings/wasm/src/credential/jpt_credential_validator/jpt_credential_validator_utils.rs
@@ -0,0 +1,102 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+use crate::common::ImportedDocumentLock;
+use crate::common::WasmTimestamp;
+use crate::credential::options::WasmStatusCheck;
+use crate::credential::WasmCredential;
+use crate::credential::WasmJpt;
+use crate::did::IToCoreDocument;
+use crate::did::WasmCoreDID;
+use crate::error::Result;
+use crate::error::WasmResult;
+use identity_iota::core::Object;
+use identity_iota::credential::JptCredentialValidatorUtils;
+use identity_iota::did::CoreDID;
+use wasm_bindgen::prelude::*;
+
+/// Utility functions for validating JPT credentials.
+#[wasm_bindgen(js_name = JptCredentialValidatorUtils)]
+#[derive(Default)]
+pub struct WasmJptCredentialValidatorUtils;
+
+#[wasm_bindgen(js_class = JptCredentialValidatorUtils)]
+impl WasmJptCredentialValidatorUtils {
+ #[wasm_bindgen(constructor)]
+ pub fn new() -> WasmJptCredentialValidatorUtils {
+ WasmJptCredentialValidatorUtils
+ }
+
+ /// 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 {
+ JptCredentialValidatorUtils::extract_issuer::(&credential.0)
+ .wasm_result()
+ .map(WasmCoreDID::from)
+ }
+ /// Utility for extracting the issuer field of a credential in JPT representation as DID.
+ /// # Errors
+ /// If the JPT decoding fails or the issuer field is not a valid DID.
+ #[wasm_bindgen(js_name = "extractIssuerFromIssuedJpt")]
+ pub fn extract_issuer_from_issued_jpt(credential: &WasmJpt) -> Result {
+ JptCredentialValidatorUtils::extract_issuer_from_issued_jpt::(&credential.0)
+ .wasm_result()
+ .map(WasmCoreDID::from)
+ }
+
+ #[wasm_bindgen(js_name = "checkTimeframesWithValidityTimeframe2024")]
+ pub fn check_timeframes_with_validity_timeframe_2024(
+ credential: &WasmCredential,
+ validity_timeframe: Option,
+ status_check: WasmStatusCheck,
+ ) -> Result<()> {
+ JptCredentialValidatorUtils::check_timeframes_with_validity_timeframe_2024(
+ &credential.0,
+ validity_timeframe.map(|t| t.0),
+ status_check.into(),
+ )
+ .wasm_result()
+ }
+
+ /// Checks whether the credential status has been revoked.
+ ///
+ /// Only supports `RevocationTimeframe2024`.
+ #[wasm_bindgen(js_name = "checkRevocationWithValidityTimeframe2024")]
+ pub fn check_revocation_with_validity_timeframe_2024(
+ credential: &WasmCredential,
+ issuer: &IToCoreDocument,
+ status_check: WasmStatusCheck,
+ ) -> Result<()> {
+ let issuer_lock = ImportedDocumentLock::from(issuer);
+ let issuer_guard = issuer_lock.try_read()?;
+ JptCredentialValidatorUtils::check_revocation_with_validity_timeframe_2024(
+ &credential.0,
+ &issuer_guard,
+ status_check.into(),
+ )
+ .wasm_result()
+ }
+
+ /// Checks whether the credential status has been revoked or the timeframe interval is INVALID
+ ///
+ /// Only supports `RevocationTimeframe2024`.
+ #[wasm_bindgen(js_name = "checkTimeframesAndRevocationWithValidityTimeframe2024")]
+ pub fn check_timeframes_and_revocation_with_validity_timeframe_2024(
+ credential: &WasmCredential,
+ issuer: &IToCoreDocument,
+ validity_timeframe: Option,
+ status_check: WasmStatusCheck,
+ ) -> Result<()> {
+ let issuer_lock = ImportedDocumentLock::from(issuer);
+ let issuer_guard = issuer_lock.try_read()?;
+ JptCredentialValidatorUtils::check_timeframes_and_revocation_with_validity_timeframe_2024(
+ &credential.0,
+ &issuer_guard,
+ validity_timeframe.map(|t| t.0),
+ status_check.into(),
+ )
+ .wasm_result()
+ }
+}
diff --git a/bindings/wasm/src/credential/jpt_credential_validator/jwp_credential_options.rs b/bindings/wasm/src/credential/jpt_credential_validator/jwp_credential_options.rs
new file mode 100644
index 0000000000..907e793996
--- /dev/null
+++ b/bindings/wasm/src/credential/jpt_credential_validator/jwp_credential_options.rs
@@ -0,0 +1,49 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+use crate::error::Result;
+use crate::error::WasmResult;
+use identity_iota::credential::JwpCredentialOptions;
+use serde::Deserialize;
+use serde::Serialize;
+use wasm_bindgen::prelude::*;
+
+#[wasm_bindgen(js_name = JwpCredentialOptions, getter_with_clone, inspectable)]
+#[derive(Serialize, Deserialize, Default)]
+pub struct WasmJwpCredentialOptions {
+ pub kid: Option,
+}
+
+#[wasm_bindgen(js_class = JwpCredentialOptions)]
+impl WasmJwpCredentialOptions {
+ #[wasm_bindgen(constructor)]
+ pub fn new() -> WasmJwpCredentialOptions {
+ WasmJwpCredentialOptions::default()
+ }
+
+ #[wasm_bindgen(js_name = fromJSON)]
+ pub fn from_json(value: JsValue) -> Result {
+ value.into_serde().wasm_result()
+ }
+
+ #[wasm_bindgen(js_name = toJSON)]
+ pub fn to_json(&self) -> Result {
+ JsValue::from_serde(self).wasm_result()
+ }
+}
+
+impl From for JwpCredentialOptions {
+ fn from(value: WasmJwpCredentialOptions) -> Self {
+ let WasmJwpCredentialOptions { kid } = value;
+ let mut jwp_options = JwpCredentialOptions::default();
+ jwp_options.kid = kid;
+
+ jwp_options
+ }
+}
+
+impl From for WasmJwpCredentialOptions {
+ fn from(value: JwpCredentialOptions) -> Self {
+ WasmJwpCredentialOptions { kid: value.kid }
+ }
+}
diff --git a/bindings/wasm/src/credential/jpt_credential_validator/jwp_verification_options.rs b/bindings/wasm/src/credential/jpt_credential_validator/jwp_verification_options.rs
new file mode 100644
index 0000000000..d7ef8b5b89
--- /dev/null
+++ b/bindings/wasm/src/credential/jpt_credential_validator/jwp_verification_options.rs
@@ -0,0 +1,48 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+use crate::error::Result;
+use crate::error::WasmResult;
+use identity_iota::document::verifiable::JwpVerificationOptions;
+use wasm_bindgen::prelude::*;
+
+#[wasm_bindgen(js_name = JwpVerificationOptions, inspectable)]
+#[derive(Clone, Debug, Default)]
+pub struct WasmJwpVerificationOptions(pub(crate) JwpVerificationOptions);
+
+impl_wasm_clone!(WasmJwpVerificationOptions, JwpVerificationOptions);
+impl_wasm_json!(WasmJwpVerificationOptions, JwpVerificationOptions);
+
+#[wasm_bindgen(js_class = JwpVerificationOptions)]
+impl WasmJwpVerificationOptions {
+ pub fn new(opts: Option) -> Result {
+ if let Some(opts) = opts {
+ opts.into_serde().wasm_result().map(WasmJwpVerificationOptions)
+ } else {
+ Ok(WasmJwpVerificationOptions::default())
+ }
+ }
+}
+
+// Interface to allow creating {@link JwpVerificationOptions} easily.
+#[wasm_bindgen]
+extern "C" {
+ #[wasm_bindgen(typescript_type = "IJwpVerificationOptions")]
+ pub type IJwpVerificationOptions;
+}
+
+#[wasm_bindgen(typescript_custom_section)]
+const I_JWP_VERIFICATION_OPTIONS: &'static str = r#"
+/** Holds options to create a new {@link JwpVerificationOptions}. */
+interface IJwpVerificationOptions {
+ /**
+ * Verify the signing verification method relation matches this.
+ */
+ readonly methodScope?: MethodScope;
+
+ /**
+ * The DID URL of the method, whose JWK should be used to verify the JWP.
+ * If unset, the `kid` of the JWP is used as the DID URL.
+ */
+ readonly methodId?: DIDUrl;
+}"#;
diff --git a/bindings/wasm/src/credential/jpt_credential_validator/mod.rs b/bindings/wasm/src/credential/jpt_credential_validator/mod.rs
new file mode 100644
index 0000000000..7da2b15114
--- /dev/null
+++ b/bindings/wasm/src/credential/jpt_credential_validator/mod.rs
@@ -0,0 +1,16 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+mod decoded_jpt_credential;
+mod jpt_credential_validation_options;
+mod jpt_credential_validator;
+mod jpt_credential_validator_utils;
+mod jwp_credential_options;
+mod jwp_verification_options;
+
+pub use decoded_jpt_credential::*;
+pub use jpt_credential_validation_options::*;
+pub use jpt_credential_validator::*;
+pub use jpt_credential_validator_utils::*;
+pub use jwp_credential_options::*;
+pub use jwp_verification_options::*;
diff --git a/bindings/wasm/src/credential/jpt_presentiation_validation/decoded_jpt_presentation.rs b/bindings/wasm/src/credential/jpt_presentiation_validation/decoded_jpt_presentation.rs
new file mode 100644
index 0000000000..698b9e3410
--- /dev/null
+++ b/bindings/wasm/src/credential/jpt_presentiation_validation/decoded_jpt_presentation.rs
@@ -0,0 +1,51 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+use identity_iota::core::Object;
+use identity_iota::credential::DecodedJptPresentation;
+use wasm_bindgen::prelude::*;
+
+use crate::common::MapStringAny;
+use crate::credential::WasmCredential;
+use crate::error::Result;
+
+#[wasm_bindgen(js_name = DecodedJptPresentation)]
+pub struct WasmDecodedJptPresentation(pub(crate) DecodedJptPresentation);
+
+impl_wasm_clone!(WasmDecodedJptPresentation, DecodedJptPresentation);
+
+#[wasm_bindgen(js_class = DecodedJptPresentation)]
+impl WasmDecodedJptPresentation {
+ /// Returns the {@link Credential} embedded into this JPT.
+ #[wasm_bindgen]
+ pub fn credential(&self) -> WasmCredential {
+ WasmCredential(self.0.credential.clone())
+ }
+
+ /// Returns the custom claims parsed from the JPT.
+ #[wasm_bindgen(js_name = "customClaims")]
+ pub fn custom_claims(&self) -> Result {
+ match self.0.custom_claims.clone() {
+ Some(obj) => MapStringAny::try_from(obj),
+ None => Ok(MapStringAny::default()),
+ }
+ }
+
+ /// Returns the `aud` property parsed from the JWT claims.
+ #[wasm_bindgen]
+ pub fn aud(&self) -> Option {
+ self.0.aud.as_ref().map(ToString::to_string)
+ }
+}
+
+impl From for WasmDecodedJptPresentation {
+ fn from(value: DecodedJptPresentation) -> Self {
+ WasmDecodedJptPresentation(value)
+ }
+}
+
+impl From for DecodedJptPresentation {
+ fn from(value: WasmDecodedJptPresentation) -> Self {
+ value.0
+ }
+}
diff --git a/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validation_options.rs b/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validation_options.rs
new file mode 100644
index 0000000000..2576437ed4
--- /dev/null
+++ b/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validation_options.rs
@@ -0,0 +1,64 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+use identity_iota::credential::JptPresentationValidationOptions;
+use wasm_bindgen::prelude::*;
+
+use crate::error::Result;
+use crate::error::WasmResult;
+
+/// Options to declare validation criteria for a {@link Jpt} presentation.
+#[derive(Debug, Default, Clone)]
+#[wasm_bindgen(js_name = "JptPresentationValidationOptions", inspectable)]
+pub struct WasmJptPresentationValidationOptions(pub(crate) JptPresentationValidationOptions);
+
+impl_wasm_clone!(WasmJptPresentationValidationOptions, JptPresentationValidationOptions);
+impl_wasm_json!(WasmJptPresentationValidationOptions, JptPresentationValidationOptions);
+
+#[wasm_bindgen(js_class = JptPresentationValidationOptions)]
+impl WasmJptPresentationValidationOptions {
+ #[wasm_bindgen(constructor)]
+ pub fn new(opts: Option) -> Result {
+ if let Some(opts) = opts {
+ opts
+ .into_serde()
+ .wasm_result()
+ .map(WasmJptPresentationValidationOptions)
+ } else {
+ Ok(WasmJptPresentationValidationOptions::default())
+ }
+ }
+}
+
+impl From for WasmJptPresentationValidationOptions {
+ fn from(value: JptPresentationValidationOptions) -> Self {
+ WasmJptPresentationValidationOptions(value)
+ }
+}
+
+impl From for JptPresentationValidationOptions {
+ fn from(value: WasmJptPresentationValidationOptions) -> Self {
+ value.0
+ }
+}
+
+#[wasm_bindgen]
+extern "C" {
+ #[wasm_bindgen(typescript_type = "IJptPresentationValidationOptions")]
+ pub type IJptPresentationValidationOptions;
+}
+
+#[wasm_bindgen(typescript_custom_section)]
+const I_JPT_PRESENTATION_VALIDATION_OPTIONS: &'static str = r#"
+/** Holds options to create a new {@link JptPresentationValidationOptions}. */
+interface IJptPresentationValidationOptions {
+ /**
+ * The nonce to be placed in the Presentation Protected Header.
+ */
+ readonly nonce?: string;
+
+ /**
+ * Options which affect the verification of the proof on the credential.
+ */
+ readonly verificationOptions?: JwpVerificationOptions;
+}"#;
diff --git a/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validator.rs b/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validator.rs
new file mode 100644
index 0000000000..3843b48b81
--- /dev/null
+++ b/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validator.rs
@@ -0,0 +1,41 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+use crate::common::ImportedDocumentLock;
+use crate::credential::WasmDecodedJptPresentation;
+use crate::credential::WasmFailFast;
+use crate::credential::WasmJpt;
+use crate::credential::WasmJptPresentationValidationOptions;
+use crate::did::IToCoreDocument;
+use crate::error::Result;
+use crate::error::WasmResult;
+use identity_iota::credential::JptPresentationValidator;
+use wasm_bindgen::prelude::*;
+
+#[wasm_bindgen(js_name = JptPresentationValidator)]
+pub struct WasmJptPresentationValidator;
+
+#[wasm_bindgen(js_class = JptPresentationValidator)]
+impl WasmJptPresentationValidator {
+ /// Decodes and validates a Presented {@link Credential} issued as a JPT (JWP Presented Form). A
+ /// {@link DecodedJptPresentation} is returned upon success.
+ ///
+ /// The following properties are validated according to `options`:
+ /// - the holder's proof on the JWP,
+ /// - the expiration date,
+ /// - the issuance date,
+ /// - the semantic structure.
+ #[wasm_bindgen]
+ pub fn validate(
+ presentation_jpt: &WasmJpt,
+ issuer: &IToCoreDocument,
+ options: &WasmJptPresentationValidationOptions,
+ fail_fast: WasmFailFast,
+ ) -> Result {
+ let issuer_lock = ImportedDocumentLock::from(issuer);
+ let issuer_guard = issuer_lock.try_read()?;
+ JptPresentationValidator::validate(&presentation_jpt.0, &issuer_guard, &options.0, fail_fast.into())
+ .wasm_result()
+ .map(WasmDecodedJptPresentation)
+ }
+}
diff --git a/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validator_utils.rs b/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validator_utils.rs
new file mode 100644
index 0000000000..d3d927b82f
--- /dev/null
+++ b/bindings/wasm/src/credential/jpt_presentiation_validation/jpt_presentation_validator_utils.rs
@@ -0,0 +1,44 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+use crate::common::WasmTimestamp;
+use crate::credential::options::WasmStatusCheck;
+use crate::credential::WasmCredential;
+use crate::credential::WasmJpt;
+use crate::did::WasmCoreDID;
+use crate::error::Result;
+use crate::error::WasmResult;
+use identity_iota::credential::JptPresentationValidatorUtils;
+use wasm_bindgen::prelude::*;
+
+/// Utility functions for verifying JPT presentations.
+#[wasm_bindgen(js_name = JptPresentationValidatorUtils)]
+pub struct WasmJptPresentationValidatorUtils;
+
+#[wasm_bindgen(js_class = JptPresentationValidatorUtils)]
+impl WasmJptPresentationValidatorUtils {
+ /// Utility for extracting the issuer field of a credential in JPT representation as DID.
+ /// # Errors
+ /// If the JPT decoding fails or the issuer field is not a valid DID.
+ #[wasm_bindgen(js_name = "extractIssuerFromPresentedJpt")]
+ pub fn extract_issuer_from_presented_jpt(presentation: &WasmJpt) -> Result {
+ JptPresentationValidatorUtils::extract_issuer_from_presented_jpt(&presentation.0)
+ .wasm_result()
+ .map(WasmCoreDID)
+ }
+
+ /// Check timeframe interval in credentialStatus with `RevocationTimeframeStatus`.
+ #[wasm_bindgen(js_name = "checkTimeframesWithValidityTimeframe2024")]
+ pub fn check_timeframes_with_validity_timeframe_2024(
+ credential: &WasmCredential,
+ validity_timeframe: Option,
+ status_check: WasmStatusCheck,
+ ) -> Result<()> {
+ JptPresentationValidatorUtils::check_timeframes_with_validity_timeframe_2024(
+ &credential.0,
+ validity_timeframe.map(|t| t.0),
+ status_check.into(),
+ )
+ .wasm_result()
+ }
+}
diff --git a/bindings/wasm/src/credential/jpt_presentiation_validation/jwp_presentation_options.rs b/bindings/wasm/src/credential/jpt_presentiation_validation/jwp_presentation_options.rs
new file mode 100644
index 0000000000..7bc30851a5
--- /dev/null
+++ b/bindings/wasm/src/credential/jpt_presentiation_validation/jwp_presentation_options.rs
@@ -0,0 +1,37 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+use identity_iota::core::Url;
+use identity_iota::credential::JwpPresentationOptions;
+use wasm_bindgen::prelude::*;
+
+/// Options to be set in the JWT claims of a verifiable presentation.
+#[wasm_bindgen(js_name = JwpPresentationOptions, inspectable, getter_with_clone)]
+#[derive(Default, Clone)]
+pub struct WasmJwpPresentationOptions {
+ /// Sets the audience for presentation (`aud` property in JWP Presentation Header).
+ pub audience: Option,
+ /// The nonce to be placed in the Presentation Protected Header.
+ pub nonce: Option,
+}
+
+#[wasm_bindgen(js_class = JwpPresentationOptions)]
+impl WasmJwpPresentationOptions {
+ #[wasm_bindgen(constructor)]
+ pub fn new() -> WasmJwpPresentationOptions {
+ Self::default()
+ }
+}
+
+impl TryFrom for JwpPresentationOptions {
+ type Error = JsError;
+ fn try_from(value: WasmJwpPresentationOptions) -> Result {
+ let WasmJwpPresentationOptions { audience, nonce } = value;
+ let audience = audience
+ .map(Url::parse)
+ .transpose()
+ .map_err(|e| JsError::new(&e.to_string()))?;
+
+ Ok(JwpPresentationOptions { audience, nonce })
+ }
+}
diff --git a/bindings/wasm/src/credential/jpt_presentiation_validation/mod.rs b/bindings/wasm/src/credential/jpt_presentiation_validation/mod.rs
new file mode 100644
index 0000000000..8a2663c85f
--- /dev/null
+++ b/bindings/wasm/src/credential/jpt_presentiation_validation/mod.rs
@@ -0,0 +1,14 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+mod decoded_jpt_presentation;
+mod jpt_presentation_validation_options;
+mod jpt_presentation_validator;
+mod jpt_presentation_validator_utils;
+mod jwp_presentation_options;
+
+pub use decoded_jpt_presentation::*;
+pub use jpt_presentation_validation_options::*;
+pub use jpt_presentation_validator::*;
+pub use jpt_presentation_validator_utils::*;
+pub use jwp_presentation_options::*;
diff --git a/bindings/wasm/src/credential/mod.rs b/bindings/wasm/src/credential/mod.rs
index 832eac1cd4..033a8cefd6 100644
--- a/bindings/wasm/src/credential/mod.rs
+++ b/bindings/wasm/src/credential/mod.rs
@@ -6,6 +6,9 @@
pub use self::credential::WasmCredential;
pub use self::credential_builder::*;
pub use self::domain_linkage_configuration::WasmDomainLinkageConfiguration;
+pub use self::jpt::*;
+pub use self::jpt_credential_validator::*;
+pub use self::jpt_presentiation_validation::*;
pub use self::jws::WasmJws;
pub use self::jwt::WasmJwt;
pub use self::jwt_credential_validation::*;
@@ -22,6 +25,9 @@ mod credential_builder;
mod domain_linkage_configuration;
mod domain_linkage_credential_builder;
mod domain_linkage_validator;
+mod jpt;
+mod jpt_credential_validator;
+mod jpt_presentiation_validation;
mod jws;
mod jwt;
mod jwt_credential_validation;
diff --git a/bindings/wasm/src/credential/revocation/mod.rs b/bindings/wasm/src/credential/revocation/mod.rs
index 7ad04980b4..c0f075df39 100644
--- a/bindings/wasm/src/credential/revocation/mod.rs
+++ b/bindings/wasm/src/credential/revocation/mod.rs
@@ -2,3 +2,4 @@
// SPDX-License-Identifier: Apache-2.0
pub mod status_list_2021;
+pub mod validity_timeframe_2024;
diff --git a/bindings/wasm/src/credential/revocation/validity_timeframe_2024/mod.rs b/bindings/wasm/src/credential/revocation/validity_timeframe_2024/mod.rs
new file mode 100644
index 0000000000..36474c70bb
--- /dev/null
+++ b/bindings/wasm/src/credential/revocation/validity_timeframe_2024/mod.rs
@@ -0,0 +1,6 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+mod status;
+
+pub use status::*;
diff --git a/bindings/wasm/src/credential/revocation/validity_timeframe_2024/status.rs b/bindings/wasm/src/credential/revocation/validity_timeframe_2024/status.rs
new file mode 100644
index 0000000000..fb85bbeee3
--- /dev/null
+++ b/bindings/wasm/src/credential/revocation/validity_timeframe_2024/status.rs
@@ -0,0 +1,75 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+use identity_iota::core::Url;
+use identity_iota::credential::RevocationTimeframeStatus;
+use wasm_bindgen::prelude::*;
+
+use crate::common::WasmDuration;
+use crate::common::WasmTimestamp;
+use crate::error::Result;
+use crate::error::WasmResult;
+
+/// Information used to determine the current status of a {@link Credential}.
+#[wasm_bindgen(js_name = RevocationTimeframeStatus, inspectable)]
+pub struct WasmRevocationTimeframeStatus(pub(crate) RevocationTimeframeStatus);
+
+impl_wasm_clone!(WasmRevocationTimeframeStatus, RevocationTimeframeStatus);
+impl_wasm_json!(WasmRevocationTimeframeStatus, RevocationTimeframeStatus);
+
+#[wasm_bindgen(js_class = RevocationTimeframeStatus)]
+impl WasmRevocationTimeframeStatus {
+ /// Creates a new `RevocationTimeframeStatus`.
+ #[wasm_bindgen(constructor)]
+ pub fn new(
+ id: String,
+ index: u32,
+ duration: WasmDuration,
+ start_validity: Option,
+ ) -> Result {
+ RevocationTimeframeStatus::new(
+ start_validity.map(|t| t.0),
+ duration.0,
+ Url::parse(id).wasm_result()?,
+ index,
+ )
+ .wasm_result()
+ .map(WasmRevocationTimeframeStatus)
+ }
+
+ /// Get startValidityTimeframe value.
+ #[wasm_bindgen(js_name = "startValidityTimeframe")]
+ pub fn start_validity_timeframe(&self) -> WasmTimestamp {
+ self.0.start_validity_timeframe().into()
+ }
+
+ /// Get endValidityTimeframe value.
+ #[wasm_bindgen(js_name = "endValidityTimeframe")]
+ pub fn end_validity_timeframe(&self) -> WasmTimestamp {
+ self.0.end_validity_timeframe().into()
+ }
+
+ /// Return the URL fo the `RevocationBitmapStatus`.
+ #[wasm_bindgen]
+ pub fn id(&self) -> String {
+ self.0.id().to_string()
+ }
+
+ /// Return the index of the credential in the issuer's revocation bitmap
+ #[wasm_bindgen]
+ pub fn index(&self) -> Option {
+ self.0.index()
+ }
+}
+
+impl From for WasmRevocationTimeframeStatus {
+ fn from(value: RevocationTimeframeStatus) -> Self {
+ WasmRevocationTimeframeStatus(value)
+ }
+}
+
+impl From for RevocationTimeframeStatus {
+ fn from(value: WasmRevocationTimeframeStatus) -> Self {
+ value.0
+ }
+}
diff --git a/bindings/wasm/src/did/wasm_core_document.rs b/bindings/wasm/src/did/wasm_core_document.rs
index 0bae16c048..0fe08e6675 100644
--- a/bindings/wasm/src/did/wasm_core_document.rs
+++ b/bindings/wasm/src/did/wasm_core_document.rs
@@ -835,6 +835,9 @@ extern "C" {
#[wasm_bindgen(typescript_type = "Promise")]
pub type PromiseJwt;
+
+ #[wasm_bindgen(typescript_type = "Promise")]
+ pub type PromiseJpt;
}
#[wasm_bindgen(typescript_custom_section)]
diff --git a/bindings/wasm/src/error.rs b/bindings/wasm/src/error.rs
index d7e8dfa3d8..035e7838bf 100644
--- a/bindings/wasm/src/error.rs
+++ b/bindings/wasm/src/error.rs
@@ -126,6 +126,8 @@ macro_rules! impl_wasm_error_from_with_struct_name {
}
}
+impl_wasm_error_from_with_struct_name!(jsonprooftoken::errors::CustomError);
+
// identity_iota::iota now has some errors where the error message does not include the source error's error message.
// This is in compliance with the Rust error handling project group's recommendation:
// * An error type with a source error should either return that error via source or include that source's error message
diff --git a/bindings/wasm/src/iota/iota_document.rs b/bindings/wasm/src/iota/iota_document.rs
index 8f8cbe6823..777a00e679 100644
--- a/bindings/wasm/src/iota/iota_document.rs
+++ b/bindings/wasm/src/iota/iota_document.rs
@@ -5,12 +5,14 @@ use std::rc::Rc;
use identity_iota::core::Object;
use identity_iota::core::OneOrMany;
+
use identity_iota::core::OrderedSet;
use identity_iota::core::Timestamp;
use identity_iota::core::Url;
use identity_iota::credential::Credential;
use identity_iota::credential::JwtPresentationOptions;
use identity_iota::credential::Presentation;
+
use identity_iota::did::DIDUrl;
use identity_iota::iota::block::output::dto::AliasOutputDto;
use identity_iota::iota::block::output::AliasOutput;
@@ -42,8 +44,12 @@ use crate::common::RecordStringAny;
use crate::common::UDIDUrlQuery;
use crate::common::UOneOrManyNumber;
use crate::common::WasmTimestamp;
+use crate::credential::PromiseJpt;
use crate::credential::UnknownCredential;
use crate::credential::WasmCredential;
+use crate::credential::WasmJpt;
+use crate::credential::WasmJwpCredentialOptions;
+use crate::credential::WasmJwpPresentationOptions;
use crate::credential::WasmJws;
use crate::credential::WasmJwt;
use crate::credential::WasmPresentation;
@@ -62,6 +68,9 @@ use crate::iota::WasmIotaDocumentMetadata;
use crate::iota::WasmStateMetadataEncoding;
use crate::jose::WasmDecodedJws;
use crate::jose::WasmJwsAlgorithm;
+use crate::jpt::WasmJptClaims;
+use crate::jpt::WasmProofAlgorithm;
+use crate::jpt::WasmSelectiveDisclosurePresentation;
use crate::storage::WasmJwsSignatureOptions;
use crate::storage::WasmJwtPresentationOptions;
use crate::storage::WasmStorage;
@@ -72,6 +81,7 @@ use crate::verification::WasmJwsVerifier;
use crate::verification::WasmMethodRelationship;
use crate::verification::WasmMethodScope;
use crate::verification::WasmVerificationMethod;
+use identity_iota::storage::JwpDocumentExt;
pub(crate) struct IotaDocumentLock(tokio::sync::RwLock);
@@ -156,6 +166,20 @@ impl WasmIotaDocument {
)
}
+ /// Sets the controllers of the document.
+ ///
+ /// Note: Duplicates will be ignored.
+ /// Use `null` to remove all controllers.
+ #[wasm_bindgen(js_name = setController)]
+ pub fn set_controller(&mut self, controller: &OptionArrayIotaDID) -> Result<()> {
+ let controller: Option> = controller.into_serde().wasm_result()?;
+ match controller {
+ Some(controller) => self.0.try_write()?.set_controller(controller),
+ None => self.0.try_write()?.set_controller([]),
+ };
+ Ok(())
+ }
+
/// Returns a copy of the document's `alsoKnownAs` set.
#[wasm_bindgen(js_name = alsoKnownAs)]
pub fn also_known_as(&self) -> Result {
@@ -835,6 +859,140 @@ impl WasmIotaDocument {
});
Ok(promise.unchecked_into())
}
+
+ #[wasm_bindgen(js_name = generateMethodJwp)]
+ pub fn generate_method_jwp(
+ &self,
+ storage: &WasmStorage,
+ alg: WasmProofAlgorithm,
+ fragment: Option,
+ scope: WasmMethodScope,
+ ) -> Result {
+ let document_lock_clone: Rc = self.0.clone();
+ let storage_clone: Rc = storage.0.clone();
+ let promise: Promise = future_to_promise(async move {
+ let method_fragment: String = document_lock_clone
+ .write()
+ .await
+ .generate_method_jwp(
+ &storage_clone,
+ KeyType::from_static_str("BLS12381"),
+ alg.into(),
+ fragment.as_deref(),
+ scope.0,
+ )
+ .await
+ .wasm_result()?;
+ Ok(JsValue::from(method_fragment))
+ });
+
+ Ok(promise.unchecked_into())
+ }
+
+ #[wasm_bindgen(js_name = createIssuedJwp)]
+ pub fn create_issued_jwp(
+ &self,
+ storage: &WasmStorage,
+ fragment: String,
+ jpt_claims: WasmJptClaims,
+ options: WasmJwpCredentialOptions,
+ ) -> Result {
+ let document_lock_clone: Rc = self.0.clone();
+ let jpt_claims = jpt_claims.into_serde().wasm_result()?;
+ let storage_clone: Rc = storage.0.clone();
+ let options = options.into();
+ let promise: Promise = future_to_promise(async move {
+ let jwp: String = document_lock_clone
+ .write()
+ .await
+ .create_issued_jwp(&storage_clone, fragment.as_str(), &jpt_claims, &options)
+ .await
+ .wasm_result()?;
+ Ok(JsValue::from(jwp))
+ });
+
+ Ok(promise.unchecked_into())
+ }
+
+ #[wasm_bindgen(js_name = createPresentedJwp)]
+ pub fn create_presented_jwp(
+ &self,
+ presentation: WasmSelectiveDisclosurePresentation,
+ method_id: String,
+ options: WasmJwpPresentationOptions,
+ ) -> Result {
+ let document_lock_clone: Rc = self.0.clone();
+ let options = options.try_into()?;
+ let promise: Promise = future_to_promise(async move {
+ let mut presentation = presentation.0;
+ let jwp: String = document_lock_clone
+ .write()
+ .await
+ .create_presented_jwp(&mut presentation, method_id.as_str(), &options)
+ .await
+ .wasm_result()?;
+ Ok(JsValue::from(jwp))
+ });
+
+ Ok(promise.unchecked_into())
+ }
+
+ #[wasm_bindgen(js_name = createCredentialJpt)]
+ pub fn create_credential_jpt(
+ &self,
+ credential: WasmCredential,
+ storage: &WasmStorage,
+ fragment: String,
+ options: WasmJwpCredentialOptions,
+ custom_claims: Option,
+ ) -> Result {
+ let document_lock_clone: Rc = self.0.clone();
+ let storage_clone: Rc = storage.0.clone();
+ let options = options.into();
+ let custom_claims = custom_claims.and_then(|claims| claims.into_serde().ok());
+ let promise: Promise = future_to_promise(async move {
+ let jpt = document_lock_clone
+ .write()
+ .await
+ .create_credential_jpt(
+ &credential.0,
+ &storage_clone,
+ fragment.as_str(),
+ &options,
+ custom_claims,
+ )
+ .await
+ .map(WasmJpt)
+ .wasm_result()?;
+ Ok(JsValue::from(jpt))
+ });
+
+ Ok(promise.unchecked_into())
+ }
+
+ #[wasm_bindgen(js_name = createPresentationJpt)]
+ pub fn create_presentation_jpt(
+ &self,
+ presentation: WasmSelectiveDisclosurePresentation,
+ method_id: String,
+ options: WasmJwpPresentationOptions,
+ ) -> Result {
+ let document_lock_clone: Rc = self.0.clone();
+ let options = options.try_into()?;
+ let promise: Promise = future_to_promise(async move {
+ let mut presentation = presentation.0;
+ let jpt = document_lock_clone
+ .write()
+ .await
+ .create_presentation_jpt(&mut presentation, method_id.as_str(), &options)
+ .await
+ .map(WasmJpt)
+ .wasm_result()?;
+ Ok(JsValue::from(jpt))
+ });
+
+ Ok(promise.unchecked_into())
+ }
}
impl From for WasmIotaDocument {
@@ -845,6 +1003,9 @@ impl From for WasmIotaDocument {
#[wasm_bindgen]
extern "C" {
+ #[wasm_bindgen(typescript_type = "IotaDID[] | null")]
+ pub type OptionArrayIotaDID;
+
#[wasm_bindgen(typescript_type = "IotaDID[]")]
pub type ArrayIotaDID;
diff --git a/bindings/wasm/src/jpt/encoding.rs b/bindings/wasm/src/jpt/encoding.rs
new file mode 100644
index 0000000000..e36a5307a5
--- /dev/null
+++ b/bindings/wasm/src/jpt/encoding.rs
@@ -0,0 +1,29 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+use jsonprooftoken::encoding::SerializationType;
+use wasm_bindgen::prelude::*;
+
+#[wasm_bindgen(js_name = SerializationType)]
+pub enum WasmSerializationType {
+ COMPACT = 0,
+ JSON = 1,
+}
+
+impl From for SerializationType {
+ fn from(value: WasmSerializationType) -> Self {
+ match value {
+ WasmSerializationType::COMPACT => SerializationType::COMPACT,
+ WasmSerializationType::JSON => SerializationType::JSON,
+ }
+ }
+}
+
+impl From for WasmSerializationType {
+ fn from(value: SerializationType) -> Self {
+ match value {
+ SerializationType::COMPACT => WasmSerializationType::COMPACT,
+ SerializationType::JSON => WasmSerializationType::JSON,
+ }
+ }
+}
diff --git a/bindings/wasm/src/jpt/issuer_protected_header.rs b/bindings/wasm/src/jpt/issuer_protected_header.rs
new file mode 100644
index 0000000000..4499d42b69
--- /dev/null
+++ b/bindings/wasm/src/jpt/issuer_protected_header.rs
@@ -0,0 +1,52 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+use crate::jpt::WasmProofAlgorithm;
+use jsonprooftoken::jwp::header::IssuerProtectedHeader;
+use wasm_bindgen::prelude::*;
+
+#[wasm_bindgen(js_name = IssuerProtectedHeader, getter_with_clone, inspectable)]
+pub struct WasmIssuerProtectedHeader {
+ /// JWP type (JPT).
+ pub typ: Option,
+ /// Algorithm used for the JWP.
+ pub alg: WasmProofAlgorithm,
+ /// ID for the key used for the JWP.
+ pub kid: Option,
+ /// Not handled for now. Will be used in the future to resolve external claims
+ pub cid: Option,
+ /// Claims.
+ claims: Vec,
+}
+
+#[wasm_bindgen(js_class = IssuerProtectedHeader)]
+impl WasmIssuerProtectedHeader {
+ #[wasm_bindgen]
+ pub fn claims(&self) -> Vec {
+ self.claims.clone()
+ }
+}
+
+impl From for IssuerProtectedHeader {
+ fn from(value: WasmIssuerProtectedHeader) -> Self {
+ let WasmIssuerProtectedHeader { typ, alg, kid, cid, .. } = value;
+ let mut header = IssuerProtectedHeader::new(alg.into());
+ header.set_typ(typ);
+ header.set_kid(kid);
+ header.set_cid(cid);
+
+ header
+ }
+}
+
+impl From for WasmIssuerProtectedHeader {
+ fn from(value: IssuerProtectedHeader) -> Self {
+ WasmIssuerProtectedHeader {
+ typ: value.typ().cloned(),
+ alg: value.alg().into(),
+ kid: value.kid().cloned(),
+ cid: value.cid().cloned(),
+ claims: value.claims().map(|claims| claims.clone().0).unwrap_or_default(),
+ }
+ }
+}
diff --git a/bindings/wasm/src/jpt/jpt_claims.rs b/bindings/wasm/src/jpt/jpt_claims.rs
new file mode 100644
index 0000000000..ae9a6e0822
--- /dev/null
+++ b/bindings/wasm/src/jpt/jpt_claims.rs
@@ -0,0 +1,31 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+use wasm_bindgen::prelude::*;
+
+#[wasm_bindgen]
+extern "C" {
+ #[wasm_bindgen(typescript_type = "JptClaims")]
+ pub type WasmJptClaims;
+}
+
+#[wasm_bindgen(typescript_custom_section)]
+const I_JPT_CLAIMS: &'static str = r#"
+/** JPT claims */
+
+interface JptClaims {
+ /** Who issued the JWP*/
+ readonly iss?: string;
+ /** Subject of the JPT. */
+ readonly sub?: string;
+ /** Expiration time. */
+ readonly exp?: number;
+ /** Issuance date. */
+ readonly iat?: number;
+ /** Time before which the JPT MUST NOT be accepted */
+ readonly nbf?: number;
+ /** Unique ID for the JPT. */
+ readonly jti?: string;
+ /** Custom claims. */
+ readonly [properties: string]: any;
+}"#;
diff --git a/bindings/wasm/src/jpt/jwp_issued.rs b/bindings/wasm/src/jpt/jwp_issued.rs
new file mode 100644
index 0000000000..e2c3826621
--- /dev/null
+++ b/bindings/wasm/src/jpt/jwp_issued.rs
@@ -0,0 +1,50 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+use jsonprooftoken::jwp::issued::JwpIssued;
+use wasm_bindgen::prelude::*;
+
+use super::WasmPayloads;
+use super::WasmSerializationType;
+use crate::error::Result;
+use crate::error::WasmResult;
+use crate::jpt::WasmIssuerProtectedHeader;
+
+#[wasm_bindgen(js_name = JwpIssued)]
+pub struct WasmJwpIssued(pub(crate) JwpIssued);
+
+impl_wasm_json!(WasmJwpIssued, JwpIssued);
+impl_wasm_clone!(WasmJwpIssued, JwpIssued);
+
+#[wasm_bindgen(js_class = JwpIssued)]
+impl WasmJwpIssued {
+ #[wasm_bindgen]
+ pub fn encode(&self, serialization: WasmSerializationType) -> Result {
+ self.0.encode(serialization.into()).wasm_result()
+ }
+
+ #[wasm_bindgen(js_name = "setProof")]
+ pub fn set_proof(&mut self, proof: &[u8]) {
+ self.0.set_proof(proof)
+ }
+
+ #[wasm_bindgen(js_name = "getProof")]
+ pub fn get_proof(&self) -> Vec {
+ self.0.get_proof().to_owned()
+ }
+
+ #[wasm_bindgen(js_name = "getPayloads")]
+ pub fn get_payloads(&self) -> WasmPayloads {
+ self.0.get_payloads().clone().into()
+ }
+
+ #[wasm_bindgen(js_name = "setPayloads")]
+ pub fn set_payloads(&mut self, payloads: WasmPayloads) {
+ self.0.set_payloads(payloads.into())
+ }
+
+ #[wasm_bindgen(js_name = getIssuerProtectedHeader)]
+ pub fn get_issuer_protected_header(&self) -> WasmIssuerProtectedHeader {
+ self.0.get_issuer_protected_header().clone().into()
+ }
+}
diff --git a/bindings/wasm/src/jpt/jwp_presentation_builder.rs b/bindings/wasm/src/jpt/jwp_presentation_builder.rs
new file mode 100644
index 0000000000..64797ee4cf
--- /dev/null
+++ b/bindings/wasm/src/jpt/jwp_presentation_builder.rs
@@ -0,0 +1,83 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+use super::WasmJwpIssued;
+use super::WasmPresentationProtectedHeader;
+use crate::error::Result;
+use crate::error::WasmResult;
+use identity_iota::credential::SelectiveDisclosurePresentation;
+use wasm_bindgen::prelude::*;
+
+/// Used to construct a JwpPresentedBuilder and handle the selective disclosure of attributes
+/// - @context MUST NOT be blinded
+/// - id MUST be blinded
+/// - type MUST NOT be blinded
+/// - issuer MUST NOT be blinded
+/// - issuanceDate MUST be blinded (if Timeframe Revocation mechanism is used)
+/// - expirationDate MUST be blinded (if Timeframe Revocation mechanism is used)
+/// - credentialSubject (User have to choose which attribute must be blinded)
+/// - credentialSchema MUST NOT be blinded
+/// - credentialStatus MUST NOT be blinded
+/// - refreshService MUST NOT be blinded (probably will be used for Timeslot Revocation mechanism)
+/// - termsOfUse NO reason to use it in ZK VC (will be in any case blinded)
+/// - evidence (User have to choose which attribute must be blinded)
+#[wasm_bindgen(js_name = SelectiveDisclosurePresentation)]
+pub struct WasmSelectiveDisclosurePresentation(pub(crate) SelectiveDisclosurePresentation);
+
+impl From for SelectiveDisclosurePresentation {
+ fn from(value: WasmSelectiveDisclosurePresentation) -> Self {
+ value.0
+ }
+}
+
+impl From for WasmSelectiveDisclosurePresentation {
+ fn from(value: SelectiveDisclosurePresentation) -> Self {
+ WasmSelectiveDisclosurePresentation(value)
+ }
+}
+
+#[wasm_bindgen(js_class = SelectiveDisclosurePresentation)]
+impl WasmSelectiveDisclosurePresentation {
+ /// Initialize a presentation starting from an Issued JWP.
+ /// The properties `jti`, `nbf`, `issuanceDate`, `expirationDate` and `termsOfUse` are concealed by default.
+ #[wasm_bindgen(constructor)]
+ pub fn new(issued_jwp: WasmJwpIssued) -> WasmSelectiveDisclosurePresentation {
+ SelectiveDisclosurePresentation::new(&issued_jwp.0).into()
+ }
+
+ /// Selectively disclose "credentialSubject" attributes.
+ /// # Example
+ /// ```
+ /// {
+ /// "id": 1234,
+ /// "name": "Alice",
+ /// "mainCourses": ["Object-oriented Programming", "Mathematics"],
+ /// "degree": {
+ /// "type": "BachelorDegree",
+ /// "name": "Bachelor of Science and Arts",
+ /// },
+ /// "GPA": "4.0",
+ /// }
+ /// ```
+ /// If you want to undisclose for example the Mathematics course and the name of the degree:
+ /// ```
+ /// undisclose_subject("mainCourses[1]");
+ /// undisclose_subject("degree.name");
+ /// ```
+ #[wasm_bindgen(js_name = concealInSubject)]
+ pub fn conceal_in_subject(&mut self, path: String) -> Result<()> {
+ self.0.conceal_in_subject(&path).wasm_result()
+ }
+
+ /// Undiscloses "evidence" attributes.
+ #[wasm_bindgen(js_name = concealInEvidence)]
+ pub fn conceal_in_evidence(&mut self, path: String) -> Result<()> {
+ self.0.conceal_in_evidence(&path).wasm_result()
+ }
+
+ /// Sets presentation protected header.
+ #[wasm_bindgen(js_name = setPresentationHeader)]
+ pub fn set_presentation_header(&mut self, header: WasmPresentationProtectedHeader) {
+ self.0.set_presentation_header(header.into())
+ }
+}
diff --git a/bindings/wasm/src/jpt/mod.rs b/bindings/wasm/src/jpt/mod.rs
new file mode 100644
index 0000000000..631572003b
--- /dev/null
+++ b/bindings/wasm/src/jpt/mod.rs
@@ -0,0 +1,20 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+mod encoding;
+mod issuer_protected_header;
+mod jpt_claims;
+mod jwp_issued;
+mod jwp_presentation_builder;
+mod payload;
+mod presentation_protected_header;
+mod proof_algorithm;
+
+pub use encoding::*;
+pub use issuer_protected_header::*;
+pub use jpt_claims::*;
+pub use jwp_issued::*;
+pub use jwp_presentation_builder::*;
+pub use payload::*;
+pub use presentation_protected_header::*;
+pub use proof_algorithm::*;
diff --git a/bindings/wasm/src/jpt/payload.rs b/bindings/wasm/src/jpt/payload.rs
new file mode 100644
index 0000000000..cdb06638b3
--- /dev/null
+++ b/bindings/wasm/src/jpt/payload.rs
@@ -0,0 +1,151 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+use crate::error::Result;
+use crate::error::WasmError;
+use crate::error::WasmResult;
+use jsonprooftoken::jpt::payloads::PayloadType;
+use jsonprooftoken::jpt::payloads::Payloads;
+use serde_json::Value;
+use std::borrow::Cow;
+use wasm_bindgen::prelude::*;
+use wasm_bindgen::JsValue;
+
+#[wasm_bindgen(js_name = PayloadType)]
+#[derive(Clone, Copy, Debug)]
+pub enum WasmPayloadType {
+ Disclosed = 0,
+ Undisclosed = 1,
+ ProofMethods = 2,
+}
+
+impl From for PayloadType {
+ fn from(value: WasmPayloadType) -> PayloadType {
+ match value {
+ WasmPayloadType::Disclosed => PayloadType::Disclosed,
+ WasmPayloadType::ProofMethods => PayloadType::ProofMethods,
+ WasmPayloadType::Undisclosed => PayloadType::Undisclosed,
+ }
+ }
+}
+
+impl From for WasmPayloadType {
+ fn from(value: PayloadType) -> WasmPayloadType {
+ match value {
+ PayloadType::Disclosed => WasmPayloadType::Disclosed,
+ PayloadType::ProofMethods => WasmPayloadType::ProofMethods,
+ PayloadType::Undisclosed => WasmPayloadType::Undisclosed,
+ }
+ }
+}
+
+#[wasm_bindgen(js_name = PayloadEntry)]
+pub struct WasmPayloadEntry(JsValue, pub WasmPayloadType);
+
+#[wasm_bindgen(js_class = PayloadEntry)]
+impl WasmPayloadEntry {
+ #[wasm_bindgen(setter)]
+ pub fn set_value(&mut self, value: JsValue) {
+ self.0 = value;
+ }
+ #[wasm_bindgen(getter)]
+ pub fn value(&self) -> JsValue {
+ self.0.clone()
+ }
+}
+
+#[wasm_bindgen(js_name = Payloads, inspectable)]
+pub struct WasmPayloads(pub(crate) Payloads);
+
+impl_wasm_json!(WasmPayloads, Payloads);
+impl_wasm_clone!(WasmPayloads, Payloads);
+
+#[wasm_bindgen(js_class = Payloads)]
+impl WasmPayloads {
+ #[wasm_bindgen(constructor)]
+ pub fn new(entries: Vec) -> Result {
+ entries
+ .into_iter()
+ .map(|WasmPayloadEntry(value, type_)| value.into_serde().wasm_result().map(|value| (value, type_.into())))
+ .collect::>>()
+ .map(Payloads)
+ .map(WasmPayloads)
+ }
+
+ #[wasm_bindgen(js_name = newFromValues)]
+ pub fn new_from_values(values: Vec) -> Result {
+ let values = values
+ .into_iter()
+ .map(|v| v.into_serde().wasm_result())
+ .collect::>>()?;
+
+ Ok(Payloads::new_from_values(values).into())
+ }
+
+ #[wasm_bindgen(js_name = "getValues")]
+ pub fn get_values(&self) -> Result> {
+ self
+ .0
+ .get_values()
+ .into_iter()
+ .map(|value| JsValue::from_serde(&value).wasm_result())
+ .collect()
+ }
+
+ #[wasm_bindgen(js_name = "getUndisclosedIndexes")]
+ pub fn get_undisclosed_indexes(&self) -> Vec {
+ self.0.get_undisclosed_indexes()
+ }
+
+ #[wasm_bindgen(js_name = "getDisclosedIndexes")]
+ pub fn get_disclosed_indexes(&self) -> Vec {
+ self.0.get_disclosed_indexes()
+ }
+
+ #[wasm_bindgen(js_name = "getUndisclosedPayloads")]
+ pub fn get_undisclosed_payloads(&self) -> Result> {
+ self
+ .0
+ .get_undisclosed_payloads()
+ .into_iter()
+ .map(|value| JsValue::from_serde(&value).wasm_result())
+ .collect()
+ }
+
+ #[wasm_bindgen(js_name = "getDisclosedPayloads")]
+ pub fn get_disclosed_payloads(&self) -> WasmPayloads {
+ self.0.get_disclosed_payloads().into()
+ }
+
+ #[wasm_bindgen(js_name = "setUndisclosed")]
+ pub fn set_undisclosed(&mut self, index: usize) {
+ self.0.set_undisclosed(index)
+ }
+
+ #[wasm_bindgen(js_name = "replacePayloadAtIndex")]
+ pub fn replace_payload_at_index(&mut self, index: usize, value: JsValue) -> Result {
+ let value = value.into_serde().wasm_result()?;
+ self
+ .0
+ .replace_payload_at_index(index, value)
+ .map_err(|_| {
+ JsValue::from(WasmError::new(
+ Cow::Borrowed("Index out of bounds"),
+ Cow::Borrowed("The provided index exceeds the array's bounds"),
+ ))
+ })
+ .and_then(|v| JsValue::from_serde(&v).wasm_result())
+ }
+}
+
+impl From for WasmPayloads {
+ fn from(value: Payloads) -> Self {
+ WasmPayloads(value)
+ }
+}
+
+impl From for Payloads {
+ fn from(value: WasmPayloads) -> Self {
+ value.0
+ }
+}
diff --git a/bindings/wasm/src/jpt/presentation_protected_header.rs b/bindings/wasm/src/jpt/presentation_protected_header.rs
new file mode 100644
index 0000000000..398870da4c
--- /dev/null
+++ b/bindings/wasm/src/jpt/presentation_protected_header.rs
@@ -0,0 +1,86 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+use jsonprooftoken::jpa::algs::PresentationProofAlgorithm;
+use jsonprooftoken::jwp::header::PresentationProtectedHeader;
+use wasm_bindgen::prelude::*;
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+#[wasm_bindgen(js_name = PresentationProofAlgorithm)]
+#[allow(non_camel_case_types)]
+pub enum WasmPresentationProofAlgorithm {
+ BLS12381_SHA256_PROOF,
+ BLS12381_SHAKE256_PROOF,
+ SU_ES256,
+ MAC_H256,
+ MAC_H384,
+ MAC_H512,
+ MAC_K25519,
+ MAC_K448,
+ MAC_H256K,
+}
+
+impl From for PresentationProofAlgorithm {
+ fn from(value: WasmPresentationProofAlgorithm) -> Self {
+ match value {
+ WasmPresentationProofAlgorithm::BLS12381_SHA256_PROOF => PresentationProofAlgorithm::BLS12381_SHA256_PROOF,
+ WasmPresentationProofAlgorithm::BLS12381_SHAKE256_PROOF => PresentationProofAlgorithm::BLS12381_SHAKE256_PROOF,
+ WasmPresentationProofAlgorithm::SU_ES256 => PresentationProofAlgorithm::SU_ES256,
+ WasmPresentationProofAlgorithm::MAC_H256 => PresentationProofAlgorithm::MAC_H256,
+ WasmPresentationProofAlgorithm::MAC_H384 => PresentationProofAlgorithm::MAC_H384,
+ WasmPresentationProofAlgorithm::MAC_H512 => PresentationProofAlgorithm::MAC_H512,
+ WasmPresentationProofAlgorithm::MAC_K25519 => PresentationProofAlgorithm::MAC_K25519,
+ WasmPresentationProofAlgorithm::MAC_K448 => PresentationProofAlgorithm::MAC_K448,
+ WasmPresentationProofAlgorithm::MAC_H256K => PresentationProofAlgorithm::MAC_H256K,
+ }
+ }
+}
+
+impl From for WasmPresentationProofAlgorithm {
+ fn from(value: PresentationProofAlgorithm) -> Self {
+ match value {
+ PresentationProofAlgorithm::BLS12381_SHA256_PROOF => WasmPresentationProofAlgorithm::BLS12381_SHA256_PROOF,
+ PresentationProofAlgorithm::BLS12381_SHAKE256_PROOF => WasmPresentationProofAlgorithm::BLS12381_SHAKE256_PROOF,
+ PresentationProofAlgorithm::SU_ES256 => WasmPresentationProofAlgorithm::SU_ES256,
+ PresentationProofAlgorithm::MAC_H256 => WasmPresentationProofAlgorithm::MAC_H256,
+ PresentationProofAlgorithm::MAC_H384 => WasmPresentationProofAlgorithm::MAC_H384,
+ PresentationProofAlgorithm::MAC_H512 => WasmPresentationProofAlgorithm::MAC_H512,
+ PresentationProofAlgorithm::MAC_K25519 => WasmPresentationProofAlgorithm::MAC_K25519,
+ PresentationProofAlgorithm::MAC_K448 => WasmPresentationProofAlgorithm::MAC_K448,
+ PresentationProofAlgorithm::MAC_H256K => WasmPresentationProofAlgorithm::MAC_H256K,
+ }
+ }
+}
+
+#[wasm_bindgen(js_name = PresentationProtectedHeader, inspectable, getter_with_clone)]
+pub struct WasmPresentationProtectedHeader {
+ pub alg: WasmPresentationProofAlgorithm,
+ /// ID for the key used for the JWP.
+ pub kid: Option,
+ /// Who have received the JPT.
+ pub aud: Option,
+ /// For replay attacks.
+ pub nonce: Option,
+}
+
+impl From for PresentationProtectedHeader {
+ fn from(value: WasmPresentationProtectedHeader) -> Self {
+ let WasmPresentationProtectedHeader { alg, kid, aud, nonce } = value;
+ let mut protected_header = PresentationProtectedHeader::new(alg.into());
+ protected_header.set_kid(kid);
+ protected_header.set_aud(aud);
+ protected_header.set_nonce(nonce);
+ protected_header
+ }
+}
+
+impl From for WasmPresentationProtectedHeader {
+ fn from(value: PresentationProtectedHeader) -> Self {
+ let alg = value.alg().into();
+ let kid = value.kid().cloned();
+ let aud = value.aud().cloned();
+ let nonce = value.nonce().cloned();
+
+ WasmPresentationProtectedHeader { alg, kid, aud, nonce }
+ }
+}
diff --git a/bindings/wasm/src/jpt/proof_algorithm.rs b/bindings/wasm/src/jpt/proof_algorithm.rs
new file mode 100644
index 0000000000..0f7f6986f1
--- /dev/null
+++ b/bindings/wasm/src/jpt/proof_algorithm.rs
@@ -0,0 +1,52 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+use jsonprooftoken::jpa::algs::ProofAlgorithm;
+use wasm_bindgen::prelude::*;
+
+#[allow(non_camel_case_types)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+#[wasm_bindgen(js_name = ProofAlgorithm)]
+pub enum WasmProofAlgorithm {
+ BLS12381_SHA256,
+ BLS12381_SHAKE256,
+ SU_ES256,
+ MAC_H256,
+ MAC_H384,
+ MAC_H512,
+ MAC_K25519,
+ MAC_K448,
+ MAC_H256K,
+}
+
+impl From for WasmProofAlgorithm {
+ fn from(value: ProofAlgorithm) -> Self {
+ match value {
+ ProofAlgorithm::BLS12381_SHA256 => WasmProofAlgorithm::BLS12381_SHA256,
+ ProofAlgorithm::BLS12381_SHAKE256 => WasmProofAlgorithm::BLS12381_SHAKE256,
+ ProofAlgorithm::SU_ES256 => WasmProofAlgorithm::SU_ES256,
+ ProofAlgorithm::MAC_H256 => WasmProofAlgorithm::MAC_H256,
+ ProofAlgorithm::MAC_H384 => WasmProofAlgorithm::MAC_H384,
+ ProofAlgorithm::MAC_H512 => WasmProofAlgorithm::MAC_H512,
+ ProofAlgorithm::MAC_K25519 => WasmProofAlgorithm::MAC_K25519,
+ ProofAlgorithm::MAC_K448 => WasmProofAlgorithm::MAC_K448,
+ ProofAlgorithm::MAC_H256K => WasmProofAlgorithm::MAC_H256K,
+ }
+ }
+}
+
+impl From for ProofAlgorithm {
+ fn from(value: WasmProofAlgorithm) -> Self {
+ match value {
+ WasmProofAlgorithm::BLS12381_SHA256 => ProofAlgorithm::BLS12381_SHA256,
+ WasmProofAlgorithm::BLS12381_SHAKE256 => ProofAlgorithm::BLS12381_SHAKE256,
+ WasmProofAlgorithm::SU_ES256 => ProofAlgorithm::SU_ES256,
+ WasmProofAlgorithm::MAC_H256 => ProofAlgorithm::MAC_H256,
+ WasmProofAlgorithm::MAC_H384 => ProofAlgorithm::MAC_H384,
+ WasmProofAlgorithm::MAC_H512 => ProofAlgorithm::MAC_H512,
+ WasmProofAlgorithm::MAC_K25519 => ProofAlgorithm::MAC_K25519,
+ WasmProofAlgorithm::MAC_K448 => ProofAlgorithm::MAC_K448,
+ WasmProofAlgorithm::MAC_H256K => ProofAlgorithm::MAC_H256K,
+ }
+ }
+}
diff --git a/bindings/wasm/src/lib.rs b/bindings/wasm/src/lib.rs
index 208edca0d0..cf8344925a 100644
--- a/bindings/wasm/src/lib.rs
+++ b/bindings/wasm/src/lib.rs
@@ -24,6 +24,7 @@ pub mod did;
pub mod error;
pub mod iota;
pub mod jose;
+pub mod jpt;
pub mod resolver;
pub mod revocation;
pub mod sd_jwt;
diff --git a/bindings/wasm/src/sd_jwt/encoder.rs b/bindings/wasm/src/sd_jwt/encoder.rs
index 79ad041dc1..550742b42b 100644
--- a/bindings/wasm/src/sd_jwt/encoder.rs
+++ b/bindings/wasm/src/sd_jwt/encoder.rs
@@ -2,14 +2,11 @@
// SPDX-License-Identifier: Apache-2.0
use super::disclosure::WasmDisclosure;
-use crate::common::ArrayString;
use crate::common::RecordStringAny;
use crate::error::Result;
use crate::error::WasmResult;
use identity_iota::sd_jwt_payload::SdObjectEncoder;
use identity_iota::sd_jwt_payload::Sha256Hasher;
-use js_sys::Array;
-use js_sys::JsString;
use serde_json::Value;
use wasm_bindgen::prelude::*;
@@ -32,54 +29,35 @@ impl WasmSdObjectEncoder {
/// Substitutes a value with the digest of its disclosure.
/// If no salt is provided, the disclosure will be created with a random salt value.
///
- /// The value of the key specified in `path` will be concealed. E.g. for path
- /// `["claim", "subclaim"]` the value of `claim.subclaim` will be concealed.
+ /// `path` indicates the pointer to the value that will be concealed using the syntax of
+ /// [JSON pointer](https://datatracker.ietf.org/doc/html/rfc6901).
///
- /// ## Error
- /// `InvalidPath` if path is invalid or the path slice is empty.
- /// `DataTypeMismatch` if existing SD format is invalid.
+ /// For the following object:
///
- /// ## Note
- /// Use `concealArrayEntry` for values in arrays.
+ /// ```
+ /// {
+ /// "id": "did:value",
+ /// "claim1": {
+ /// "abc": true
+ /// },
+ /// "claim2": ["val_1", "val_2"]
+ /// }
+ /// ```
+ ///
+ /// Path "/id" conceals `"id": "did:value"`
+ /// Path "/claim1/abc" conceals `"abc": true`
+ /// Path "/claim2/0" conceals `val_1`
+ /// ```
+ ///
+ /// ## Errors
+ /// * `InvalidPath` if pointer is invalid.
+ /// * `DataTypeMismatch` if existing SD format is invalid.
#[wasm_bindgen(js_name = conceal)]
- pub fn conceal(&mut self, path: ArrayString, salt: Option) -> Result {
- let path: Vec = path
- .dyn_into::()?
- .iter()
- .map(|item| item.dyn_into::().map(String::from))
- .collect::>>()?;
- let path: Vec<&str> = path.iter().map(|s| &**s).collect();
+ pub fn conceal(&mut self, path: String, salt: Option) -> Result {
let disclosure = self.0.conceal(&path, salt).wasm_result()?;
Ok(WasmDisclosure(disclosure))
}
- /// Substitutes a value within an array with the digest of its disclosure.
- /// If no salt is provided, the disclosure will be created with random salt value.
- ///
- /// `path` is used to specify the array in the object, while `element_index` specifies
- /// the index of the element to be concealed (index start at 0).
- ///
- /// ## Error
- /// `InvalidPath` if path is invalid or the path slice is empty.
- /// `DataTypeMismatch` if existing SD format is invalid.
- /// `IndexOutofBounds` if `element_index` is out of bounds.
- #[wasm_bindgen(js_name = concealArrayEntry)]
- pub fn conceal_array_entry(
- &mut self,
- path: ArrayString,
- element_index: usize,
- salt: Option,
- ) -> Result {
- let path: Vec = path
- .dyn_into::()?
- .iter()
- .map(|item| item.dyn_into::().map(String::from))
- .collect::>>()?;
- let path: Vec<&str> = path.iter().map(|s| &**s).collect();
- let disclosure = self.0.conceal_array_entry(&path, element_index, salt).wasm_result()?;
- Ok(WasmDisclosure(disclosure))
- }
-
/// Adds the `_sd_alg` property to the top level of the object, with
/// its value set to "sha-256".
#[wasm_bindgen(js_name = addSdAlgProperty)]
@@ -103,7 +81,7 @@ impl WasmSdObjectEncoder {
#[wasm_bindgen(js_name = encodeToObject)]
pub fn encode_to_object(&self) -> Result {
Ok(
- JsValue::from_serde(&self.0.object())
+ JsValue::from_serde(&self.0.object().wasm_result()?)
.wasm_result()?
.unchecked_into::(),
)
@@ -112,19 +90,13 @@ impl WasmSdObjectEncoder {
/// Returns the modified object.
#[wasm_bindgen(js_name = toJSON)]
pub fn to_json(&self) -> Result {
- JsValue::from_serde(&self.0.object()).wasm_result()
+ JsValue::from_serde(&self.0.object().wasm_result()?).wasm_result()
}
/// Adds a decoy digest to the specified path.
/// If path is an empty slice, decoys will be added to the top level.
#[wasm_bindgen(js_name = addDecoys)]
- pub fn add_decoys(&mut self, path: ArrayString, number_of_decoys: usize) -> Result<()> {
- let path: Vec = path
- .dyn_into::()?
- .iter()
- .map(|item| item.dyn_into::().map(String::from))
- .collect::>>()?;
- let path: Vec<&str> = path.iter().map(|s| &**s).collect();
+ pub fn add_decoys(&mut self, path: String, number_of_decoys: usize) -> Result<()> {
self.0.add_decoys(&path, number_of_decoys).wasm_result()?;
Ok(())
}
diff --git a/bindings/wasm/src/sd_jwt/wasm_sd_jwt.rs b/bindings/wasm/src/sd_jwt/wasm_sd_jwt.rs
index 7b4f201206..c55de229e6 100644
--- a/bindings/wasm/src/sd_jwt/wasm_sd_jwt.rs
+++ b/bindings/wasm/src/sd_jwt/wasm_sd_jwt.rs
@@ -77,5 +77,4 @@ impl WasmSdJwt {
}
}
-impl_wasm_json!(WasmSdJwt, SdJwt);
impl_wasm_clone!(WasmSdJwt, SdJwt);
diff --git a/bindings/wasm/src/storage/jpt_timeframe_revocation_ext.rs b/bindings/wasm/src/storage/jpt_timeframe_revocation_ext.rs
new file mode 100644
index 0000000000..9529128e20
--- /dev/null
+++ b/bindings/wasm/src/storage/jpt_timeframe_revocation_ext.rs
@@ -0,0 +1,69 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+use identity_iota::storage::ProofUpdateCtx;
+use wasm_bindgen::prelude::*;
+
+#[wasm_bindgen(js_name = ProofUpdateCtx, inspectable, getter_with_clone)]
+pub struct WasmProofUpdateCtx {
+ /// Old `startValidityTimeframe` value
+ pub old_start_validity_timeframe: Vec,
+ /// New `startValidityTimeframe` value to be signed
+ pub new_start_validity_timeframe: Vec,
+ /// Old `endValidityTimeframe` value
+ pub old_end_validity_timeframe: Vec,
+ /// New `endValidityTimeframe` value to be signed
+ pub new_end_validity_timeframe: Vec,
+ /// Index of `startValidityTimeframe` claim inside the array of Claims
+ pub index_start_validity_timeframe: usize,
+ /// Index of `endValidityTimeframe` claim inside the array of Claims
+ pub index_end_validity_timeframe: usize,
+ /// Number of signed messages, number of payloads in a JWP
+ pub number_of_signed_messages: usize,
+}
+
+impl From for WasmProofUpdateCtx {
+ fn from(value: ProofUpdateCtx) -> Self {
+ let ProofUpdateCtx {
+ old_start_validity_timeframe,
+ new_start_validity_timeframe,
+ old_end_validity_timeframe,
+ new_end_validity_timeframe,
+ index_start_validity_timeframe,
+ index_end_validity_timeframe,
+ number_of_signed_messages,
+ } = value;
+ Self {
+ old_start_validity_timeframe,
+ new_start_validity_timeframe,
+ old_end_validity_timeframe,
+ new_end_validity_timeframe,
+ index_start_validity_timeframe,
+ index_end_validity_timeframe,
+ number_of_signed_messages,
+ }
+ }
+}
+
+impl From for ProofUpdateCtx {
+ fn from(value: WasmProofUpdateCtx) -> Self {
+ let WasmProofUpdateCtx {
+ old_start_validity_timeframe,
+ new_start_validity_timeframe,
+ old_end_validity_timeframe,
+ new_end_validity_timeframe,
+ index_start_validity_timeframe,
+ index_end_validity_timeframe,
+ number_of_signed_messages,
+ } = value;
+ Self {
+ old_start_validity_timeframe,
+ new_start_validity_timeframe,
+ old_end_validity_timeframe,
+ new_end_validity_timeframe,
+ index_start_validity_timeframe,
+ index_end_validity_timeframe,
+ number_of_signed_messages,
+ }
+ }
+}
diff --git a/bindings/wasm/src/storage/jwk_storage.rs b/bindings/wasm/src/storage/jwk_storage.rs
index 4616def075..6adf78845b 100644
--- a/bindings/wasm/src/storage/jwk_storage.rs
+++ b/bindings/wasm/src/storage/jwk_storage.rs
@@ -50,6 +50,9 @@ extern "C" {
#[wasm_bindgen(method)]
pub fn exists(this: &WasmJwkStorage, key_id: String) -> PromiseBool;
+
+ #[wasm_bindgen(method)]
+ pub(crate) fn _get_key(this: &WasmJwkStorage, key_id: &str) -> Option;
}
#[async_trait::async_trait(?Send)]
diff --git a/bindings/wasm/src/storage/jwk_storage_bbs_plus_ext.rs b/bindings/wasm/src/storage/jwk_storage_bbs_plus_ext.rs
new file mode 100644
index 0000000000..d92f12e607
--- /dev/null
+++ b/bindings/wasm/src/storage/jwk_storage_bbs_plus_ext.rs
@@ -0,0 +1,132 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+use std::str::FromStr;
+
+use crate::error::Result as WasmResult;
+use crate::error::WasmResult as _;
+use crate::jose::WasmJwk;
+use crate::jpt::WasmProofAlgorithm;
+
+use super::WasmJwkGenOutput;
+use super::WasmJwkStorage;
+use super::WasmProofUpdateCtx;
+
+use identity_iota::storage::bls::encode_bls_jwk;
+use identity_iota::storage::bls::expand_bls_jwk;
+use identity_iota::storage::bls::generate_bbs_keypair;
+use identity_iota::storage::bls::sign_bbs;
+use identity_iota::storage::bls::update_bbs_signature;
+use identity_iota::storage::JwkGenOutput;
+use identity_iota::storage::JwkStorage;
+use identity_iota::storage::JwkStorageBbsPlusExt;
+use identity_iota::storage::KeyId;
+use identity_iota::storage::KeyStorageError;
+use identity_iota::storage::KeyStorageErrorKind;
+use identity_iota::storage::KeyStorageResult;
+use identity_iota::storage::KeyType;
+use identity_iota::storage::ProofUpdateCtx;
+use identity_iota::verification::jwk::Jwk;
+use jsonprooftoken::jpa::algs::ProofAlgorithm;
+use wasm_bindgen::prelude::*;
+
+#[wasm_bindgen(js_class = JwkStorage)]
+impl WasmJwkStorage {
+ #[wasm_bindgen(js_name = generateBBS)]
+ /// Generates a new BBS+ keypair.
+ pub async fn _generate_bbs(&self, alg: WasmProofAlgorithm) -> WasmResult {
+ self
+ .generate_bbs(KeyType::from_static_str("BLS12381"), alg.into())
+ .await
+ .map(WasmJwkGenOutput::from)
+ .wasm_result()
+ }
+
+ #[wasm_bindgen(js_name = signBBS)]
+ pub async fn _sign_bbs(
+ &self,
+ key_id: String,
+ data: Vec,
+ public_key: WasmJwk,
+ header: Option>,
+ ) -> WasmResult {
+ let key_id = KeyId::new(key_id);
+ let data = data.into_iter().map(|arr| arr.to_vec()).collect::>();
+ let header = header.unwrap_or_default();
+ self
+ .sign_bbs(&key_id, &data, header.as_slice(), &public_key.into())
+ .await
+ .map(|v| js_sys::Uint8Array::from(v.as_slice()))
+ .wasm_result()
+ }
+
+ #[wasm_bindgen(js_name = updateBBSSignature)]
+ pub async fn _update_signature(
+ &self,
+ key_id: String,
+ public_key: &WasmJwk,
+ signature: Vec,
+ ctx: WasmProofUpdateCtx,
+ ) -> WasmResult {
+ let key_id = KeyId::new(key_id);
+ self
+ .update_signature(&key_id, &public_key.0, &signature, ctx.into())
+ .await
+ .map(|sig| js_sys::Uint8Array::from(sig.as_slice()))
+ .wasm_result()
+ }
+}
+
+#[async_trait::async_trait(?Send)]
+impl JwkStorageBbsPlusExt for WasmJwkStorage {
+ async fn generate_bbs(&self, _key_type: KeyType, alg: ProofAlgorithm) -> KeyStorageResult {
+ let (sk, pk) = generate_bbs_keypair(alg)?;
+
+ let (jwk, public_jwk) = encode_bls_jwk(&sk, &pk, alg);
+ let kid = ::insert(self, jwk).await?;
+
+ Ok(JwkGenOutput::new(kid, public_jwk))
+ }
+ async fn sign_bbs(
+ &self,
+ key_id: &KeyId,
+ data: &[Vec],
+ header: &[u8],
+ public_key: &Jwk,
+ ) -> KeyStorageResult> {
+ let Some(private_jwk) = WasmJwkStorage::_get_key(self, key_id.as_str()).map(Jwk::from) else {
+ return Err(KeyStorageError::new(KeyStorageErrorKind::KeyNotFound));
+ };
+ // Extract the required alg from the given public key
+ let alg = public_key
+ .alg()
+ .ok_or(KeyStorageErrorKind::UnsupportedProofAlgorithm)
+ .and_then(|alg_str| {
+ ProofAlgorithm::from_str(alg_str).map_err(|_| KeyStorageErrorKind::UnsupportedProofAlgorithm)
+ })?;
+
+ let (sk, pk) = expand_bls_jwk(&private_jwk)?;
+ sign_bbs(alg, data, &sk.expect("jwk was private"), &pk, header)
+ }
+ async fn update_signature(
+ &self,
+ key_id: &KeyId,
+ public_key: &Jwk,
+ signature: &[u8],
+ ctx: ProofUpdateCtx,
+ ) -> KeyStorageResult> {
+ // Extract the required alg from the given public key
+ let alg = public_key
+ .alg()
+ .ok_or(KeyStorageErrorKind::UnsupportedProofAlgorithm)
+ .and_then(|alg_str| {
+ ProofAlgorithm::from_str(alg_str).map_err(|_| KeyStorageErrorKind::UnsupportedProofAlgorithm)
+ })?;
+
+ let Some(private_jwk) = WasmJwkStorage::_get_key(self, key_id.as_str()).map(Jwk::from) else {
+ return Err(KeyStorageError::new(KeyStorageErrorKind::KeyNotFound));
+ };
+ let sk = expand_bls_jwk(&private_jwk)?.0.expect("jwk is private");
+ update_bbs_signature(alg, signature, &sk, &ctx)
+ }
+}
diff --git a/bindings/wasm/src/storage/mod.rs b/bindings/wasm/src/storage/mod.rs
index 8295d95e88..fe54110e9d 100644
--- a/bindings/wasm/src/storage/mod.rs
+++ b/bindings/wasm/src/storage/mod.rs
@@ -1,14 +1,17 @@
// Copyright 2020-2023 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0
+mod jpt_timeframe_revocation_ext;
mod jwk_gen_output;
mod jwk_storage;
+mod jwk_storage_bbs_plus_ext;
mod jwt_presentation_options;
mod key_id_storage;
mod method_digest;
mod signature_options;
mod wasm_storage;
+pub use jpt_timeframe_revocation_ext::*;
pub use jwk_gen_output::*;
pub use jwk_storage::*;
pub use jwt_presentation_options::*;
diff --git a/bindings/wasm/src/verification/wasm_method_data.rs b/bindings/wasm/src/verification/wasm_method_data.rs
index 5bba4aa5a9..58a9c65820 100644
--- a/bindings/wasm/src/verification/wasm_method_data.rs
+++ b/bindings/wasm/src/verification/wasm_method_data.rs
@@ -1,6 +1,7 @@
// Copyright 2020-2023 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0
+use identity_iota::verification::CustomMethodData;
use identity_iota::verification::MethodData;
use wasm_bindgen::prelude::*;
@@ -45,6 +46,27 @@ impl WasmMethodData {
Ok(Self(MethodData::PublicKeyJwk(key.0.clone())))
}
+ /// Creates a new custom {@link MethodData}.
+ #[wasm_bindgen(js_name = newCustom)]
+ pub fn new_custom(name: String, data: JsValue) -> Result {
+ let data = data.into_serde::().wasm_result()?;
+ Ok(Self(MethodData::Custom(CustomMethodData { name, data })))
+ }
+
+ /// Returns the wrapped custom method data format is `Custom`.
+ #[wasm_bindgen(js_name = tryCustom)]
+ pub fn try_custom(&self) -> Result {
+ self
+ .0
+ .custom()
+ .map(|custom| custom.clone().into())
+ .ok_or(WasmError::new(
+ Cow::Borrowed("MethodDataFormatError"),
+ Cow::Borrowed("method data format is not Custom"),
+ ))
+ .wasm_result()
+ }
+
/// Returns a `Uint8Array` containing the decoded bytes of the {@link MethodData}.
///
/// This is generally a public key identified by a {@link MethodData} value.
@@ -78,3 +100,31 @@ impl From for WasmMethodData {
WasmMethodData(data)
}
}
+
+/// A custom verification method data format.
+#[wasm_bindgen(js_name = CustomMethodData, inspectable)]
+pub struct WasmCustomMethodData(pub(crate) CustomMethodData);
+
+#[wasm_bindgen(js_class = CustomMethodData)]
+impl WasmCustomMethodData {
+ #[wasm_bindgen(constructor)]
+ pub fn new(name: String, data: JsValue) -> Result {
+ let data = data.into_serde::().wasm_result()?;
+ Ok(Self(CustomMethodData { name, data }))
+ }
+}
+
+impl From for WasmCustomMethodData {
+ fn from(value: CustomMethodData) -> Self {
+ Self(value)
+ }
+}
+
+impl From for CustomMethodData {
+ fn from(value: WasmCustomMethodData) -> Self {
+ value.0
+ }
+}
+
+impl_wasm_clone!(WasmCustomMethodData, CustomMethodData);
+impl_wasm_json!(WasmCustomMethodData, CustomMethodData);
diff --git a/bindings/wasm/src/verification/wasm_method_type.rs b/bindings/wasm/src/verification/wasm_method_type.rs
index 9fb1fff660..c143b7e53f 100644
--- a/bindings/wasm/src/verification/wasm_method_type.rs
+++ b/bindings/wasm/src/verification/wasm_method_type.rs
@@ -20,13 +20,24 @@ impl WasmMethodType {
WasmMethodType(MethodType::X25519_KEY_AGREEMENT_KEY_2019)
}
- /// A verification method for use with JWT verification as prescribed by the {@link Jwk}
- /// in the `publicKeyJwk` entry.
- #[wasm_bindgen(js_name = JsonWebKey)]
+ /// @deprecated Use {@link JsonWebKey2020} instead.
+ #[wasm_bindgen(js_name = JsonWebKey, skip_jsdoc)]
pub fn json_web_key() -> WasmMethodType {
WasmMethodType(MethodType::JSON_WEB_KEY)
}
+ /// A verification method for use with JWT verification as prescribed by the {@link Jwk}
+ /// in the `publicKeyJwk` entry.
+ #[wasm_bindgen(js_name = JsonWebKey2020)]
+ pub fn json_web_key_2020() -> WasmMethodType {
+ WasmMethodType(MethodType::JSON_WEB_KEY_2020)
+ }
+
+ /// A custom method.
+ pub fn custom(type_: String) -> WasmMethodType {
+ WasmMethodType(MethodType::custom(type_))
+ }
+
/// Returns the {@link MethodType} as a string.
#[allow(clippy::inherent_to_string)]
#[wasm_bindgen(js_name = toString)]
diff --git a/bindings/wasm/src/verification/wasm_verification_method.rs b/bindings/wasm/src/verification/wasm_verification_method.rs
index 62b5103c9d..6f01436ffe 100644
--- a/bindings/wasm/src/verification/wasm_verification_method.rs
+++ b/bindings/wasm/src/verification/wasm_verification_method.rs
@@ -8,6 +8,7 @@ use crate::did::WasmCoreDID;
use crate::did::WasmDIDUrl;
use crate::error::Result;
use crate::error::WasmResult;
+use identity_iota::core::Object;
use identity_iota::did::CoreDID;
use identity_iota::verification::VerificationMethod;
use wasm_bindgen::prelude::*;
@@ -37,6 +38,24 @@ impl WasmVerificationMethod {
.wasm_result()
}
+ /// Create a custom {@link VerificationMethod}.
+ #[wasm_bindgen(constructor)]
+ pub fn new(
+ id: &WasmDIDUrl,
+ controller: &WasmCoreDID,
+ type_: &WasmMethodType,
+ data: &WasmMethodData,
+ ) -> Result {
+ VerificationMethod::builder(Object::new())
+ .type_(type_.0.clone())
+ .data(data.0.clone())
+ .controller(controller.0.clone())
+ .id(id.0.clone())
+ .build()
+ .map(Self)
+ .wasm_result()
+ }
+
/// Returns a copy of the {@link DIDUrl} of the {@link VerificationMethod}'s `id`.
#[wasm_bindgen]
pub fn id(&self) -> WasmDIDUrl {
diff --git a/bindings/wasm/tests/core.ts b/bindings/wasm/tests/core.ts
index 396ef146d9..9bde0e335d 100644
--- a/bindings/wasm/tests/core.ts
+++ b/bindings/wasm/tests/core.ts
@@ -225,7 +225,7 @@ describe("CoreDocument", function() {
// Resolve.
const resolved = doc.resolveMethod(fragment, scope)!;
assert.deepStrictEqual(resolved.id().fragment(), fragment);
- assert.deepStrictEqual(resolved.type().toString(), MethodType.JsonWebKey().toString());
+ assert.deepStrictEqual(resolved.type().toString(), MethodType.JsonWebKey2020().toString());
assert.deepStrictEqual(resolved.controller().toString(), doc.id().toString());
assert.deepStrictEqual(resolved.data().tryPublicKeyJwk().toJSON(), JWK.toJSON());
assert.deepStrictEqual(resolved.toJSON(), method.toJSON());
diff --git a/bindings/wasm/tests/iota.ts b/bindings/wasm/tests/iota.ts
index b32279c3ae..9037dd5af4 100644
--- a/bindings/wasm/tests/iota.ts
+++ b/bindings/wasm/tests/iota.ts
@@ -100,7 +100,7 @@ describe("IotaDocument", function() {
// Resolve.
const resolved = doc.resolveMethod(fragment, scope)!;
assert.deepStrictEqual(resolved.id().fragment(), fragment);
- assert.deepStrictEqual(resolved.type().toString(), MethodType.JsonWebKey().toString());
+ assert.deepStrictEqual(resolved.type().toString(), MethodType.JsonWebKey2020().toString());
assert.deepStrictEqual(resolved.controller().toString(), doc.id().toString());
assert.deepStrictEqual(resolved.data().tryPublicKeyJwk().toJSON(), JWK.toJSON());
assert.deepStrictEqual(resolved.toJSON(), method.toJSON());
diff --git a/bindings/wasm/tests/sd_jwt.ts b/bindings/wasm/tests/sd_jwt.ts
index 3c471fab6d..31d02633e7 100644
--- a/bindings/wasm/tests/sd_jwt.ts
+++ b/bindings/wasm/tests/sd_jwt.ts
@@ -1,5 +1,5 @@
import * as assert from "assert";
-import { SdJwt, SdObjectDecoder, SdObjectEncoder } from "../node";
+import { SdObjectDecoder, SdObjectEncoder } from "../node";
describe("sd-jwt-payload", function() {
describe("#encoder", function() {
@@ -26,7 +26,7 @@ describe("sd-jwt-payload", function() {
};
let encoder = new SdObjectEncoder(obj);
- let emailDisclosure = encoder.conceal(["email"], "tstsalt");
+ let emailDisclosure = encoder.conceal("/email", "tstsalt");
console.log(emailDisclosure);
assert.deepStrictEqual(emailDisclosure.claimName(), "email");
assert.deepStrictEqual(emailDisclosure.claimValue(), "johndoe@example.com");
@@ -38,11 +38,11 @@ describe("sd-jwt-payload", function() {
let disclosures = [
emailDisclosure.toEncodedString(),
- encoder.conceal(["address", "street_address"]).toEncodedString(),
- encoder.concealArrayEntry(["nationalities"], 0).toEncodedString(),
+ encoder.conceal("/address/street_address").toEncodedString(),
+ encoder.conceal("/nationalities/0").toEncodedString(),
];
encoder.addSdAlgProperty();
- encoder.addDecoys([], 3);
+ encoder.addDecoys("", 3);
let encoded = encoder.encodeToObject();
assert.equal(encoded._sd.length, 4);
diff --git a/bindings/wasm/tests/txm_readme.js b/bindings/wasm/tests/txm_readme.js
index 0f1ed5d0c0..2a388c2dba 100644
--- a/bindings/wasm/tests/txm_readme.js
+++ b/bindings/wasm/tests/txm_readme.js
@@ -1,7 +1,21 @@
-const { execSync } = require("child_process");
+const assert = require("assert");
+const spawn = require("child_process").spawn;
-describe("Test TXM", function() {
- it("README examples pass", async () => {
- execSync("txm README.md");
+describe("Test TXM", () => {
+ before((done) => {
+ let process = spawn("txm", ["README.md"]);
+ process.stdout.on("data", function(data) {
+ console.log(data.toString());
+ });
+ process.stderr.on("data", function(data) {
+ console.log(data.toString());
+ });
+ process.on("exit", (code) => {
+ exitCode = code;
+ done();
+ });
+ });
+ it("exit code should be zero", () => {
+ assert.equal(exitCode, 0);
});
});
diff --git a/bindings/wasm/tests/txm_readme_rust.js b/bindings/wasm/tests/txm_readme_rust.js
new file mode 100644
index 0000000000..d024653fe2
--- /dev/null
+++ b/bindings/wasm/tests/txm_readme_rust.js
@@ -0,0 +1,21 @@
+const assert = require("assert");
+const spawn = require("child_process").spawn;
+
+describe("Test TXM", () => {
+ before((done) => {
+ let process = spawn("txm", ["../../README.md"]);
+ process.stdout.on("data", function(data) {
+ console.log(data.toString());
+ });
+ process.stderr.on("data", function(data) {
+ console.log(data.toString());
+ });
+ process.on("exit", (code) => {
+ exitCode = code;
+ done();
+ });
+ });
+ it("exit code should be zero", () => {
+ assert.equal(exitCode, 0);
+ });
+});
diff --git a/examples/0_basic/0_create_did.rs b/examples/0_basic/0_create_did.rs
index 03f64353fc..61f157cb37 100644
--- a/examples/0_basic/0_create_did.rs
+++ b/examples/0_basic/0_create_did.rs
@@ -30,10 +30,10 @@ use iota_sdk::types::block::output::AliasOutput;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// The API endpoint of an IOTA node, e.g. Hornet.
- let api_endpoint: &str = "http://127.0.0.1:14265";
+ let api_endpoint: &str = "http://localhost";
// The faucet endpoint allows requesting funds for testing purposes.
- let faucet_endpoint: &str = "http://127.0.0.1:8091/api/enqueue";
+ let faucet_endpoint: &str = "http://localhost/faucet/api/enqueue";
// Create a new client to interact with the IOTA ledger.
let client: Client = Client::builder()
diff --git a/examples/0_basic/7_revoke_vc.rs b/examples/0_basic/7_revoke_vc.rs
index 48d947a7ff..864041f3e3 100644
--- a/examples/0_basic/7_revoke_vc.rs
+++ b/examples/0_basic/7_revoke_vc.rs
@@ -110,6 +110,8 @@ async fn main() -> anyhow::Result<()> {
// Publish the updated Alias Output.
issuer_document = client.publish_did_output(&secret_manager_issuer, alias_output).await?;
+ println!("DID Document > {issuer_document:#}");
+
// Create a credential subject indicating the degree earned by Alice.
let subject: Subject = Subject::from_json_value(json!({
"id": alice_document.id().as_str(),
diff --git a/examples/0_basic/8_stronghold.rs b/examples/0_basic/8_stronghold.rs
index a706e467e7..0681e5b612 100644
--- a/examples/0_basic/8_stronghold.rs
+++ b/examples/0_basic/8_stronghold.rs
@@ -29,10 +29,10 @@ use iota_sdk::types::block::output::AliasOutput;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// The API endpoint of an IOTA node, e.g. Hornet.
- let api_endpoint: &str = "http://127.0.0.1:14265";
+ let api_endpoint: &str = "http://localhost";
// The faucet endpoint allows requesting funds for testing purposes.
- let faucet_endpoint: &str = "http://127.0.0.1:8091/api/enqueue";
+ let faucet_endpoint: &str = "http://localhost/faucet/api/enqueue";
// Stronghold snapshot path.
let path = random_stronghold_path();
diff --git a/examples/1_advanced/10_zkp_revocation.rs b/examples/1_advanced/10_zkp_revocation.rs
new file mode 100644
index 0000000000..a78dea0e76
--- /dev/null
+++ b/examples/1_advanced/10_zkp_revocation.rs
@@ -0,0 +1,534 @@
+// Copyright 2020-2024 IOTA Stiftung, Fondazione Links
+// SPDX-License-Identifier: Apache-2.0
+
+use examples::get_address_with_funds;
+use examples::random_stronghold_path;
+use examples::MemStorage;
+use examples::API_ENDPOINT;
+use examples::FAUCET_ENDPOINT;
+use identity_eddsa_verifier::EdDSAJwsVerifier;
+use identity_iota::core::json;
+use identity_iota::core::Duration;
+use identity_iota::core::FromJson;
+use identity_iota::core::Object;
+use identity_iota::core::Timestamp;
+use identity_iota::core::Url;
+use identity_iota::credential::Credential;
+use identity_iota::credential::CredentialBuilder;
+use identity_iota::credential::DecodedJwtPresentation;
+use identity_iota::credential::FailFast;
+use identity_iota::credential::Jpt;
+use identity_iota::credential::JptCredentialValidationOptions;
+use identity_iota::credential::JptCredentialValidator;
+use identity_iota::credential::JptCredentialValidatorUtils;
+use identity_iota::credential::JptPresentationValidationOptions;
+use identity_iota::credential::JptPresentationValidator;
+use identity_iota::credential::JptPresentationValidatorUtils;
+use identity_iota::credential::JwpCredentialOptions;
+use identity_iota::credential::JwpPresentationOptions;
+use identity_iota::credential::Jwt;
+use identity_iota::credential::JwtPresentationOptions;
+use identity_iota::credential::JwtPresentationValidationOptions;
+use identity_iota::credential::JwtPresentationValidator;
+use identity_iota::credential::JwtPresentationValidatorUtils;
+use identity_iota::credential::JwtValidationError;
+use identity_iota::credential::Presentation;
+use identity_iota::credential::PresentationBuilder;
+use identity_iota::credential::RevocationBitmap;
+use identity_iota::credential::RevocationTimeframeStatus;
+use identity_iota::credential::SelectiveDisclosurePresentation;
+use identity_iota::credential::Status;
+use identity_iota::credential::StatusCheck;
+use identity_iota::credential::Subject;
+use identity_iota::credential::SubjectHolderRelationship;
+use identity_iota::did::CoreDID;
+use identity_iota::did::DIDUrl;
+use identity_iota::did::DID;
+use identity_iota::document::verifiable::JwsVerificationOptions;
+use identity_iota::document::Service;
+use identity_iota::iota::IotaClientExt;
+use identity_iota::iota::IotaDocument;
+use identity_iota::iota::IotaIdentityClientExt;
+use identity_iota::iota::NetworkName;
+use identity_iota::resolver::Resolver;
+use identity_iota::storage::JwkDocumentExt;
+use identity_iota::storage::JwkMemStore;
+use identity_iota::storage::JwpDocumentExt;
+use identity_iota::storage::JwsSignatureOptions;
+use identity_iota::storage::KeyIdMemstore;
+use identity_iota::storage::KeyType;
+use identity_iota::storage::TimeframeRevocationExtension;
+use identity_iota::verification::jws::JwsAlgorithm;
+use identity_iota::verification::MethodScope;
+use iota_sdk::client::secret::stronghold::StrongholdSecretManager;
+use iota_sdk::client::secret::SecretManager;
+use iota_sdk::client::Client;
+use iota_sdk::client::Password;
+use iota_sdk::types::block::address::Address;
+use iota_sdk::types::block::output::AliasOutput;
+use iota_sdk::types::block::output::AliasOutputBuilder;
+use iota_sdk::types::block::output::RentStructure;
+use jsonprooftoken::jpa::algs::ProofAlgorithm;
+use std::thread;
+use std::time::Duration as SleepDuration;
+
+async fn create_did(
+ client: &Client,
+ secret_manager: &SecretManager,
+ storage: &MemStorage,
+ key_type: KeyType,
+ alg: Option,
+ proof_alg: Option,
+) -> anyhow::Result<(Address, IotaDocument, String)> {
+ // Get an address with funds for testing.
+ let address: Address = get_address_with_funds(client, secret_manager, FAUCET_ENDPOINT).await?;
+
+ // Get the Bech32 human-readable part (HRP) of the network.
+ let network_name: NetworkName = client.network_name().await?;
+
+ // Create a new DID document with a placeholder DID.
+ // The DID will be derived from the Alias Id of the Alias Output after publishing.
+ let mut document: IotaDocument = IotaDocument::new(&network_name);
+
+ // New Verification Method containing a BBS+ key
+ let fragment = if let Some(alg) = alg {
+ document
+ .generate_method(storage, key_type, alg, None, MethodScope::VerificationMethod)
+ .await?
+ } else if let Some(proof_alg) = proof_alg {
+ let fragment = document
+ .generate_method_jwp(storage, key_type, proof_alg, None, MethodScope::VerificationMethod)
+ .await?;
+
+ // Create a new empty revocation bitmap. No credential is revoked yet.
+ let revocation_bitmap: RevocationBitmap = RevocationBitmap::new();
+
+ // Add the revocation bitmap to the DID document of the issuer as a service.
+ let service_id: DIDUrl = document.id().to_url().join("#my-revocation-service")?;
+ let service: Service = revocation_bitmap.to_service(service_id)?;
+
+ assert!(document.insert_service(service).is_ok());
+
+ fragment
+ } else {
+ return Err(anyhow::Error::msg("You have to pass at least one algorithm"));
+ };
+
+ // Construct an Alias Output containing the DID document, with the wallet address
+ // set as both the state controller and governor.
+ let alias_output: AliasOutput = client.new_did_output(address, document, None).await?;
+
+ // Publish the Alias Output and get the published DID document.
+ let document: IotaDocument = client.publish_did_output(secret_manager, alias_output).await?;
+ println!("Published DID document: {document:#}");
+
+ Ok((address, document, fragment))
+}
+
+/// Demonstrates how to create an Anonymous Credential with BBS+.
+#[tokio::main]
+async fn main() -> anyhow::Result<()> {
+ // Create a new client to interact with the IOTA ledger.
+ let client: Client = Client::builder()
+ .with_primary_node(API_ENDPOINT, None)?
+ .finish()
+ .await?;
+
+ let secret_manager_issuer = SecretManager::Stronghold(
+ StrongholdSecretManager::builder()
+ .password(Password::from("secure_password_1".to_owned()))
+ .build(random_stronghold_path())?,
+ );
+
+ let storage_issuer: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new());
+
+ let secret_manager_holder = SecretManager::Stronghold(
+ StrongholdSecretManager::builder()
+ .password(Password::from("secure_password_2".to_owned()))
+ .build(random_stronghold_path())?,
+ );
+
+ let storage_holder: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new());
+
+ let (_, mut issuer_document, fragment_issuer): (Address, IotaDocument, String) = create_did(
+ &client,
+ &secret_manager_issuer,
+ &storage_issuer,
+ JwkMemStore::BLS12381G2_KEY_TYPE,
+ None,
+ Some(ProofAlgorithm::BLS12381_SHA256),
+ )
+ .await?;
+
+ let (_, holder_document, fragment_holder): (Address, IotaDocument, String) = create_did(
+ &client,
+ &secret_manager_holder,
+ &storage_holder,
+ JwkMemStore::ED25519_KEY_TYPE,
+ Some(JwsAlgorithm::EdDSA),
+ None,
+ )
+ .await?;
+
+ // Create a credential subject indicating the degree earned by Alice.
+ let subject: Subject = Subject::from_json_value(json!({
+ "id": holder_document.id().as_str(),
+ "name": "Alice",
+ "mainCourses": ["Object-oriented Programming", "Mathematics"],
+ "degree": {
+ "type": "BachelorDegree",
+ "name": "Bachelor of Science and Arts",
+ },
+ "GPA": "4.0",
+ }))?;
+
+ // =========================================================================================
+ // Step 1: Create a new RevocationTimeframeStatus containing the current validityTimeframe
+ // =======================================================================================
+ let duration = Duration::minutes(1);
+ // The issuer also chooses a unique `RevocationBitmap` index to be able to revoke it later.
+ let service_url = issuer_document.id().to_url().join("#my-revocation-service")?;
+ let credential_index: u32 = 5;
+
+ let start_validity_timeframe = Timestamp::now_utc();
+ let status: Status = RevocationTimeframeStatus::new(
+ Some(start_validity_timeframe),
+ duration,
+ service_url.into(),
+ credential_index,
+ )?
+ .into();
+
+ // Build credential using subject above and issuer.
+ let credential: Credential = CredentialBuilder::default()
+ .id(Url::parse("https://example.edu/credentials/3732")?)
+ .issuer(Url::parse(issuer_document.id().as_str())?)
+ .type_("UniversityDegreeCredential")
+ .subject(subject)
+ .status(status)
+ .build()?;
+
+ let credential_jpt: Jpt = issuer_document
+ .create_credential_jpt(
+ &credential,
+ &storage_issuer,
+ &fragment_issuer,
+ &JwpCredentialOptions::default(),
+ None,
+ )
+ .await?;
+
+ // Validate the credential's proof using the issuer's DID Document, the credential's semantic structure,
+ // that the issuance date is not in the future and that the expiration date is not in the past:
+ let decoded_jpt = JptCredentialValidator::validate::<_, Object>(
+ &credential_jpt,
+ &issuer_document,
+ &JptCredentialValidationOptions::default(),
+ FailFast::FirstError,
+ )
+ .unwrap();
+
+ assert_eq!(credential, decoded_jpt.credential);
+
+ // Issuer sends the Verifiable Credential to the holder.
+ println!(
+ "Sending credential (as JPT) to the holder: {}\n",
+ credential_jpt.as_str()
+ );
+
+ // Holder validate the credential and retrieve the JwpIssued, needed to construct the JwpPresented
+
+ let validation_result = JptCredentialValidator::validate::<_, Object>(
+ &credential_jpt,
+ &issuer_document,
+ &JptCredentialValidationOptions::default(),
+ FailFast::FirstError,
+ );
+
+ let decoded_credential = validation_result.unwrap();
+
+ // ===========================================================================
+ // Credential's Status check
+ // ===========================================================================
+
+ // Timeframe check
+ let timeframe_result = JptCredentialValidatorUtils::check_timeframes_with_validity_timeframe_2024(
+ &decoded_credential.credential,
+ None,
+ StatusCheck::Strict,
+ );
+
+ assert!(timeframe_result.is_ok());
+
+ let revocation_result = JptCredentialValidatorUtils::check_revocation_with_validity_timeframe_2024(
+ &decoded_credential.credential,
+ &issuer_document,
+ StatusCheck::Strict,
+ );
+
+ assert!(revocation_result.is_ok());
+
+ // Both checks
+
+ let revocation_result = JptCredentialValidatorUtils::check_timeframes_and_revocation_with_validity_timeframe_2024(
+ &decoded_credential.credential,
+ &issuer_document,
+ None,
+ StatusCheck::Strict,
+ );
+
+ assert!(revocation_result.is_ok());
+
+ let challenge: &str = "475a7984-1bb5-4c4c-a56f-822bccd46440";
+
+ let method_id = decoded_credential
+ .decoded_jwp
+ .get_issuer_protected_header()
+ .kid()
+ .unwrap();
+
+ let mut selective_disclosure_presentation = SelectiveDisclosurePresentation::new(&decoded_credential.decoded_jwp);
+ selective_disclosure_presentation
+ .conceal_in_subject("mainCourses[1]")
+ .unwrap();
+ selective_disclosure_presentation
+ .conceal_in_subject("degree.name")
+ .unwrap();
+
+ let presentation_jpt: Jpt = issuer_document
+ .create_presentation_jpt(
+ &mut selective_disclosure_presentation,
+ method_id,
+ &JwpPresentationOptions::default().nonce(challenge),
+ )
+ .await?;
+
+ // Holder sends a Presentation JPT to the Verifier.
+ println!(
+ "Sending presentation (as JPT) to the verifier: {}\n",
+ presentation_jpt.as_str()
+ );
+
+ // ===========================================================================
+ // Step 2a: Verifier receives the Presentation and verifies it.
+ // ===========================================================================
+
+ let presentation_validation_options = JptPresentationValidationOptions::default().nonce(challenge);
+
+ // Verifier validate the Presented Credential and retrieve the JwpPresented
+ let decoded_presented_credential = JptPresentationValidator::validate::<_, Object>(
+ &presentation_jpt,
+ &issuer_document,
+ &presentation_validation_options,
+ FailFast::FirstError,
+ )
+ .unwrap();
+
+ // Check validityTimeframe
+
+ let timeframe_result = JptPresentationValidatorUtils::check_timeframes_with_validity_timeframe_2024(
+ &decoded_presented_credential.credential,
+ None,
+ StatusCheck::Strict,
+ );
+
+ assert!(timeframe_result.is_ok());
+
+ // Since no errors were thrown by `verify_presentation` we know that the validation was successful.
+ println!(
+ "Presented Credential successfully validated: {:#}",
+ decoded_presented_credential.credential
+ );
+
+ // ===========================================================================
+ // Step 2b: Waiting for the next validityTimeframe, will result in the Credential timeframe interval NOT valid
+ // ===========================================================================
+
+ thread::sleep(SleepDuration::from_secs(61));
+
+ let timeframe_result = JptPresentationValidatorUtils::check_timeframes_with_validity_timeframe_2024(
+ &decoded_presented_credential.credential,
+ None,
+ StatusCheck::Strict,
+ );
+
+ // We expect validation to no longer succeed because the credential was NOT updated.
+ if matches!(timeframe_result.unwrap_err(), JwtValidationError::OutsideTimeframe) {
+ println!("Validity Timeframe interval NOT valid\n");
+ }
+
+ // ===========================================================================
+ // 3: Update credential
+ // ===========================================================================
+
+ // ===========================================================================
+ // 3.1: Issuer 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.
+ let challenge: &str = "475a7984-1bb5-4c4c-a56f-822bccd46440";
+
+ // The Holder and Issuer also agree that the signature should have an expiry date
+ // 10 minutes from now.
+ let expires: Timestamp = Timestamp::now_utc().checked_add(Duration::minutes(10)).unwrap();
+
+ // ===========================================================================
+ // 3.2: Holder creates and signs a verifiable presentation from the issued credential.
+ // ===========================================================================
+
+ // Create an unsigned Presentation from the previously issued ZK Verifiable Credential.
+ let presentation: Presentation =
+ PresentationBuilder::new(holder_document.id().to_url().into(), Default::default())
+ .credential(credential_jpt)
+ .build()?;
+
+ // Create a JWT verifiable presentation using the holder's verification method
+ // and include the requested challenge and expiry timestamp.
+ let presentation_jwt: Jwt = holder_document
+ .create_presentation_jwt(
+ &presentation,
+ &storage_holder,
+ &fragment_holder,
+ &JwsSignatureOptions::default().nonce(challenge.to_owned()),
+ &JwtPresentationOptions::default().expiration_date(expires),
+ )
+ .await?;
+
+ // ===========================================================================
+ // 3.3: Holder sends a verifiable presentation to the verifier.
+ // ===========================================================================
+ println!(
+ "Sending presentation (as JWT) to the Issuer: {}\n",
+ presentation_jwt.as_str()
+ );
+
+ // ===========================================================================
+ // 3.4: Issuer validate Verifiable Presentation and ZK Verifiable Credential.
+ // ===========================================================================
+
+ // ================================================
+ // 3.4.1: Issuer validate Verifiable Presentation.
+ // ================================================
+
+ let presentation_verifier_options: JwsVerificationOptions =
+ JwsVerificationOptions::default().nonce(challenge.to_owned());
+
+ let mut resolver: Resolver = Resolver::new();
+ resolver.attach_iota_handler(client.clone());
+
+ // Resolve the holder's document.
+ let holder_did: CoreDID = JwtPresentationValidatorUtils::extract_holder(&presentation_jwt)?;
+ let holder: IotaDocument = resolver.resolve(&holder_did).await?;
+
+ // Validate presentation. Note that this doesn't validate the included credentials.
+ let presentation_validation_options =
+ JwtPresentationValidationOptions::default().presentation_verifier_options(presentation_verifier_options);
+ let presentation: DecodedJwtPresentation = JwtPresentationValidator::with_signature_verifier(
+ EdDSAJwsVerifier::default(),
+ )
+ .validate(&presentation_jwt, &holder, &presentation_validation_options)?;
+
+ // =======================================================================
+ // 3.4.2: Issuer validate ZK Verifiable Credential inside the Presentation.
+ // ========================================================================
+
+ let validation_options: JptCredentialValidationOptions = JptCredentialValidationOptions::default()
+ .subject_holder_relationship(holder_did.to_url().into(), SubjectHolderRelationship::AlwaysSubject);
+
+ let jpt_credentials: &Vec = &presentation.presentation.verifiable_credential;
+
+ // Extract ZK Verifiable Credential in JPT format
+ let jpt_vc = jpt_credentials.first().unwrap();
+
+ // Issuer checks the Credential integrity.
+ let mut verified_credential_result =
+ JptCredentialValidator::validate::<_, Object>(jpt_vc, &issuer_document, &validation_options, FailFast::FirstError)
+ .unwrap();
+
+ // Issuer checks if the Credential has been revoked
+ let revocation_result = JptCredentialValidatorUtils::check_revocation_with_validity_timeframe_2024(
+ &verified_credential_result.credential,
+ &issuer_document,
+ StatusCheck::Strict,
+ );
+
+ assert!(!revocation_result.is_err_and(|e| matches!(e, JwtValidationError::Revoked)));
+
+ // ===========================================================================
+ // 3.5: Issuer ready for Update.
+ // ===========================================================================
+
+ // Since no errors were thrown during the Verifiable Presentation validation and the verification of inner Credentials
+ println!(
+ "Ready for Update - VP successfully validated: {:#?}",
+ presentation.presentation
+ );
+
+ // Issuer updates the credential
+ let new_credential_jpt = issuer_document
+ .update(
+ &storage_issuer,
+ &fragment_issuer,
+ None,
+ duration,
+ &mut verified_credential_result.decoded_jwp,
+ )
+ .await?;
+
+ // Issuer sends back the credential updated
+
+ println!(
+ "Sending updated credential (as JPT) to the holder: {}\n",
+ new_credential_jpt.as_str()
+ );
+
+ // Holder check validity of the updated credential
+
+ let validation_result = JptCredentialValidator::validate::<_, Object>(
+ &new_credential_jpt,
+ &issuer_document,
+ &JptCredentialValidationOptions::default(),
+ FailFast::FirstError,
+ )
+ .unwrap();
+
+ let timeframe_result = JptCredentialValidatorUtils::check_timeframes_with_validity_timeframe_2024(
+ &validation_result.credential,
+ None,
+ StatusCheck::Strict,
+ );
+
+ assert!(!timeframe_result
+ .as_ref()
+ .is_err_and(|e| matches!(e, JwtValidationError::OutsideTimeframe)));
+ println!("Updated credential is VALID!");
+
+ // ===========================================================================
+ // Issuer decides to Revoke Holder's Credential
+ // ===========================================================================
+
+ println!("Issuer decides to revoke the Credential");
+
+ // Update the RevocationBitmap service in the issuer's DID Document.
+ // This revokes the credential's unique index.
+
+ issuer_document.revoke_credentials("my-revocation-service", &[credential_index])?;
+
+ // Publish the changes.
+ let alias_output: AliasOutput = client.update_did_output(issuer_document.clone()).await?;
+ let rent_structure: RentStructure = client.get_rent_structure().await?;
+ let alias_output: AliasOutput = AliasOutputBuilder::from(&alias_output)
+ .with_minimum_storage_deposit(rent_structure)
+ .finish()?;
+ issuer_document = client.publish_did_output(&secret_manager_issuer, alias_output).await?;
+
+ // Holder checks if his credential has been revoked by the Issuer
+ let revocation_result = JptCredentialValidatorUtils::check_revocation_with_validity_timeframe_2024(
+ &decoded_credential.credential,
+ &issuer_document,
+ StatusCheck::Strict,
+ );
+ assert!(revocation_result.is_err_and(|e| matches!(e, JwtValidationError::Revoked)));
+ println!("Credential Revoked!");
+ Ok(())
+}
diff --git a/examples/1_advanced/7_sd_jwt.rs b/examples/1_advanced/7_sd_jwt.rs
index 90fac13307..2d2a4665ee 100644
--- a/examples/1_advanced/7_sd_jwt.rs
+++ b/examples/1_advanced/7_sd_jwt.rs
@@ -112,9 +112,9 @@ async fn main() -> anyhow::Result<()> {
// Make "locality", "postal_code" and "street_address" selectively disclosable while keeping
// other properties in plain text.
let disclosures = vec![
- encoder.conceal(&["vc", "credentialSubject", "address", "locality"], None)?,
- encoder.conceal(&["vc", "credentialSubject", "address", "postal_code"], None)?,
- encoder.conceal(&["vc", "credentialSubject", "address", "street_address"], None)?,
+ encoder.conceal("/vc/credentialSubject/address/locality", None)?,
+ encoder.conceal("/vc/credentialSubject/address/postal_code", None)?,
+ encoder.conceal("/vc/credentialSubject/address/street_address", None)?,
];
// Add the `_sd_alg` property.
diff --git a/examples/1_advanced/8_status_list_2021.rs b/examples/1_advanced/8_status_list_2021.rs
index 3f41823bfe..0a70690e91 100644
--- a/examples/1_advanced/8_status_list_2021.rs
+++ b/examples/1_advanced/8_status_list_2021.rs
@@ -6,10 +6,8 @@ use examples::random_stronghold_path;
use examples::MemStorage;
use examples::API_ENDPOINT;
use identity_eddsa_verifier::EdDSAJwsVerifier;
-
use identity_iota::core::FromJson;
use identity_iota::core::Object;
-
use identity_iota::core::ToJson;
use identity_iota::core::Url;
use identity_iota::credential::status_list_2021::StatusList2021;
@@ -17,10 +15,8 @@ use identity_iota::credential::status_list_2021::StatusList2021Credential;
use identity_iota::credential::status_list_2021::StatusList2021CredentialBuilder;
use identity_iota::credential::status_list_2021::StatusList2021Entry;
use identity_iota::credential::status_list_2021::StatusPurpose;
-
use identity_iota::credential::Credential;
use identity_iota::credential::CredentialBuilder;
-
use identity_iota::credential::FailFast;
use identity_iota::credential::Issuer;
use identity_iota::credential::Jwt;
diff --git a/examples/1_advanced/9_zkp.rs b/examples/1_advanced/9_zkp.rs
new file mode 100644
index 0000000000..eeb4246280
--- /dev/null
+++ b/examples/1_advanced/9_zkp.rs
@@ -0,0 +1,260 @@
+// Copyright 2020-2024 IOTA Stiftung, Fondazione Links
+// SPDX-License-Identifier: Apache-2.0
+
+use examples::get_address_with_funds;
+use examples::random_stronghold_path;
+use examples::MemStorage;
+use examples::API_ENDPOINT;
+use examples::FAUCET_ENDPOINT;
+use identity_iota::core::json;
+use identity_iota::core::FromJson;
+use identity_iota::core::Object;
+use identity_iota::core::Url;
+use identity_iota::credential::Credential;
+use identity_iota::credential::CredentialBuilder;
+use identity_iota::credential::FailFast;
+use identity_iota::credential::Jpt;
+use identity_iota::credential::JptCredentialValidationOptions;
+use identity_iota::credential::JptCredentialValidator;
+use identity_iota::credential::JptCredentialValidatorUtils;
+use identity_iota::credential::JptPresentationValidationOptions;
+use identity_iota::credential::JptPresentationValidator;
+use identity_iota::credential::JptPresentationValidatorUtils;
+use identity_iota::credential::JwpCredentialOptions;
+use identity_iota::credential::JwpPresentationOptions;
+use identity_iota::credential::SelectiveDisclosurePresentation;
+use identity_iota::credential::Subject;
+use identity_iota::did::CoreDID;
+use identity_iota::did::DID;
+use identity_iota::iota::IotaClientExt;
+use identity_iota::iota::IotaDocument;
+use identity_iota::iota::IotaIdentityClientExt;
+use identity_iota::iota::NetworkName;
+use identity_iota::resolver::Resolver;
+use identity_iota::storage::JwkMemStore;
+use identity_iota::storage::JwpDocumentExt;
+use identity_iota::storage::KeyIdMemstore;
+use identity_iota::storage::KeyType;
+use identity_iota::verification::MethodScope;
+use iota_sdk::client::secret::stronghold::StrongholdSecretManager;
+use iota_sdk::client::secret::SecretManager;
+use iota_sdk::client::Client;
+use iota_sdk::client::Password;
+use iota_sdk::types::block::address::Address;
+use iota_sdk::types::block::output::AliasOutput;
+use jsonprooftoken::jpa::algs::ProofAlgorithm;
+
+// Creates a DID with a JWP verification method.
+async fn create_did(
+ client: &Client,
+ secret_manager: &SecretManager,
+ storage: &MemStorage,
+ key_type: KeyType,
+ alg: ProofAlgorithm,
+) -> anyhow::Result<(Address, IotaDocument, String)> {
+ // Get an address with funds for testing.
+ let address: Address = get_address_with_funds(client, secret_manager, FAUCET_ENDPOINT).await?;
+
+ // Get the Bech32 human-readable part (HRP) of the network.
+ let network_name: NetworkName = client.network_name().await?;
+
+ // Create a new DID document with a placeholder DID.
+ // The DID will be derived from the Alias Id of the Alias Output after publishing.
+ let mut document: IotaDocument = IotaDocument::new(&network_name);
+
+ let fragment = document
+ .generate_method_jwp(storage, key_type, alg, None, MethodScope::VerificationMethod)
+ .await?;
+
+ // Construct an Alias Output containing the DID document, with the wallet address
+ // set as both the state controller and governor.
+ let alias_output: AliasOutput = client.new_did_output(address, document, None).await?;
+
+ // Publish the Alias Output and get the published DID document.
+ let document: IotaDocument = client.publish_did_output(secret_manager, alias_output).await?;
+ println!("Published DID document: {document:#}");
+
+ Ok((address, document, fragment))
+}
+
+/// Demonstrates how to create an Anonymous Credential with BBS+.
+#[tokio::main]
+async fn main() -> anyhow::Result<()> {
+ // ===========================================================================
+ // Step 1: Create identity for the issuer.
+ // ===========================================================================
+
+ // Create a new client to interact with the IOTA ledger.
+ let client: Client = Client::builder()
+ .with_primary_node(API_ENDPOINT, None)?
+ .finish()
+ .await?;
+
+ let secret_manager_issuer = SecretManager::Stronghold(
+ StrongholdSecretManager::builder()
+ .password(Password::from("secure_password_1".to_owned()))
+ .build(random_stronghold_path())?,
+ );
+
+ let storage_issuer: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new());
+
+ let (_, issuer_document, fragment_issuer): (Address, IotaDocument, String) = create_did(
+ &client,
+ &secret_manager_issuer,
+ &storage_issuer,
+ JwkMemStore::BLS12381G2_KEY_TYPE,
+ ProofAlgorithm::BLS12381_SHA256,
+ )
+ .await?;
+
+ // ===========================================================================
+ // Step 2: Issuer creates and signs a Verifiable Credential with BBS algorithm.
+ // ===========================================================================
+
+ // Create a credential subject indicating the degree earned by Alice.
+ let subject: Subject = Subject::from_json_value(json!({
+ "name": "Alice",
+ "mainCourses": ["Object-oriented Programming", "Mathematics"],
+ "degree": {
+ "type": "BachelorDegree",
+ "name": "Bachelor of Science and Arts",
+ },
+ "GPA": "4.0",
+ }))?;
+
+ // Build credential using subject above and issuer.
+ let credential: Credential = CredentialBuilder::default()
+ .id(Url::parse("https://example.edu/credentials/3732")?)
+ .issuer(Url::parse(issuer_document.id().as_str())?)
+ .type_("UniversityDegreeCredential")
+ .subject(subject)
+ .build()?;
+
+ let credential_jpt: Jpt = issuer_document
+ .create_credential_jpt(
+ &credential,
+ &storage_issuer,
+ &fragment_issuer,
+ &JwpCredentialOptions::default(),
+ None,
+ )
+ .await?;
+
+ // Validate the credential's proof using the issuer's DID Document, the credential's semantic structure,
+ // that the issuance date is not in the future and that the expiration date is not in the past:
+ let decoded_jpt = JptCredentialValidator::validate::<_, Object>(
+ &credential_jpt,
+ &issuer_document,
+ &JptCredentialValidationOptions::default(),
+ FailFast::FirstError,
+ )
+ .unwrap();
+
+ assert_eq!(credential, decoded_jpt.credential);
+
+ // ===========================================================================
+ // Step 3: Issuer sends the Verifiable Credential to the holder.
+ // ===========================================================================
+ println!(
+ "Sending credential (as JPT) to the holder: {}\n",
+ credential_jpt.as_str()
+ );
+
+ // ============================================================================================
+ // Step 4: Holder resolves Issuer's DID, retrieve Issuer's document and validate the Credential
+ // ============================================================================================
+
+ let mut resolver: Resolver = Resolver::new();
+ resolver.attach_iota_handler(client);
+
+ // Holder resolves issuer's DID
+ let issuer: CoreDID = JptCredentialValidatorUtils::extract_issuer_from_issued_jpt(&credential_jpt).unwrap();
+ let issuer_document: IotaDocument = resolver.resolve(&issuer).await?;
+
+ // Holder validates the credential and retrieve the JwpIssued, needed to construct the JwpPresented
+ let decoded_credential = JptCredentialValidator::validate::<_, Object>(
+ &credential_jpt,
+ &issuer_document,
+ &JptCredentialValidationOptions::default(),
+ FailFast::FirstError,
+ )
+ .unwrap();
+
+ // ===========================================================================
+ // Step 5: Verifier sends the holder a challenge and requests a Presentation.
+ //
+ // Please be aware that when we mention "Presentation," we are not alluding to the Verifiable Presentation standard as defined by W3C (https://www.w3.org/TR/vc-data-model/#presentations).
+ // Instead, our reference is to a JWP Presentation (https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-proof#name-presented-form), which differs from the W3C standard.
+ // ===========================================================================
+
+ // A unique random challenge generated by the requester per presentation can mitigate replay attacks.
+ let challenge: &str = "475a7984-1bb5-4c4c-a56f-822bccd46440";
+
+ // =========================================================================================================
+ // Step 6: Holder engages in the Selective Disclosure of credential's attributes.
+ // =========================================================================================================
+
+ let method_id = decoded_credential
+ .decoded_jwp
+ .get_issuer_protected_header()
+ .kid()
+ .unwrap();
+
+ let mut selective_disclosure_presentation = SelectiveDisclosurePresentation::new(&decoded_credential.decoded_jwp);
+ selective_disclosure_presentation
+ .conceal_in_subject("mainCourses[1]")
+ .unwrap();
+ selective_disclosure_presentation
+ .conceal_in_subject("degree.name")
+ .unwrap();
+
+ // =======================================================================================================================================
+ // Step 7: Holder needs Issuer's Public Key to compute the Signature Proof of Knowledge and construct the Presentation
+ // JPT.
+ // =======================================================================================================================================
+
+ // Construct a JPT(JWP in the Presentation form) representing the Selectively Disclosed Verifiable Credential
+ let presentation_jpt: Jpt = issuer_document
+ .create_presentation_jpt(
+ &mut selective_disclosure_presentation,
+ method_id,
+ &JwpPresentationOptions::default().nonce(challenge),
+ )
+ .await?;
+
+ // ===========================================================================
+ // Step 8: Holder sends a Presentation JPT to the Verifier.
+ // ===========================================================================
+
+ println!(
+ "Sending presentation (as JPT) to the verifier: {}\n",
+ presentation_jpt.as_str()
+ );
+
+ // ===========================================================================
+ // Step 9: Verifier receives the Presentation and verifies it.
+ // ===========================================================================
+
+ // Verifier resolve Issuer DID
+ let issuer: CoreDID = JptPresentationValidatorUtils::extract_issuer_from_presented_jpt(&presentation_jpt).unwrap();
+ let issuer_document: IotaDocument = resolver.resolve(&issuer).await?;
+
+ let presentation_validation_options = JptPresentationValidationOptions::default().nonce(challenge);
+
+ // Verifier validate the Presented Credential and retrieve the JwpPresented
+ let decoded_presented_credential = JptPresentationValidator::validate::<_, Object>(
+ &presentation_jpt,
+ &issuer_document,
+ &presentation_validation_options,
+ FailFast::FirstError,
+ )
+ .unwrap();
+
+ // Since no errors were thrown by `verify_presentation` we know that the validation was successful.
+ println!(
+ "Presented Credential successfully validated: {:#?}",
+ decoded_presented_credential.credential
+ );
+
+ Ok(())
+}
diff --git a/examples/Cargo.toml b/examples/Cargo.toml
index 3a0974122f..6830f2035e 100644
--- a/examples/Cargo.toml
+++ b/examples/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "examples"
-version = "1.1.0-alpha.1"
+version = "1.3.0"
authors = ["IOTA Stiftung"]
edition = "2021"
publish = false
@@ -8,12 +8,13 @@ publish = false
[dependencies]
anyhow = "1.0.62"
identity_eddsa_verifier = { path = "../identity_eddsa_verifier", default-features = false }
-identity_iota = { path = "../identity_iota", default-features = false, features = ["memstore", "domain-linkage", "revocation-bitmap", "status-list-2021"] }
-identity_stronghold = { path = "../identity_stronghold", default-features = false }
+identity_iota = { path = "../identity_iota", default-features = false, features = ["iota-client", "client", "memstore", "domain-linkage", "revocation-bitmap", "status-list-2021", "jpt-bbs-plus"] }
+identity_stronghold = { path = "../identity_stronghold", default-features = false, features = ["bbs-plus"] }
iota-sdk = { version = "1.0", default-features = false, features = ["tls", "client", "stronghold"] }
+json-proof-token.workspace = true
primitive-types = "0.12.1"
rand = "0.8.5"
-sd-jwt-payload = { version = "0.1.2", default-features = false, features = ["sha"] }
+sd-jwt-payload = { version = "0.2.1", default-features = false, features = ["sha"] }
serde_json = { version = "1.0", default-features = false }
tokio = { version = "1.29", default-features = false, features = ["rt"] }
@@ -91,3 +92,11 @@ name = "7_sd_jwt"
[[example]]
path = "1_advanced/8_status_list_2021.rs"
name = "8_status_list_2021"
+
+[[example]]
+path = "1_advanced/9_zkp.rs"
+name = "9_zkp"
+
+[[example]]
+path = "1_advanced/10_zkp_revocation.rs"
+name = "10_zkp_revocation"
diff --git a/examples/utils/utils.rs b/examples/utils/utils.rs
index a9b8ca5154..a79a74312e 100644
--- a/examples/utils/utils.rs
+++ b/examples/utils/utils.rs
@@ -28,8 +28,8 @@ use iota_sdk::types::block::address::Hrp;
use rand::distributions::DistString;
use serde_json::Value;
-pub static API_ENDPOINT: &str = "http://localhost:14265";
-pub static FAUCET_ENDPOINT: &str = "http://localhost:8091/api/enqueue";
+pub static API_ENDPOINT: &str = "http://localhost";
+pub static FAUCET_ENDPOINT: &str = "http://localhost/faucet/api/enqueue";
pub type MemStorage = Storage;
diff --git a/identity_core/Cargo.toml b/identity_core/Cargo.toml
index 284cca081d..39c15fabd0 100644
--- a/identity_core/Cargo.toml
+++ b/identity_core/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "identity_core"
-version = "1.1.0-alpha.1"
+version = "1.3.0"
authors.workspace = true
edition.workspace = true
homepage.workspace = true
@@ -35,3 +35,6 @@ quickcheck_macros = { version = "1.0" }
# RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features --no-deps --workspace --open
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
+
+[lints]
+workspace = true
diff --git a/identity_credential/Cargo.toml b/identity_credential/Cargo.toml
index 22fbb4b5da..e93fdd0699 100644
--- a/identity_credential/Cargo.toml
+++ b/identity_credential/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "identity_credential"
-version = "1.1.0-alpha.1"
+version = "1.3.0"
authors = ["IOTA Stiftung"]
edition = "2021"
homepage.workspace = true
@@ -12,26 +12,28 @@ rust-version.workspace = true
description = "An implementation of the Verifiable Credentials standard."
[dependencies]
-dataurl = { version = "0.1.2", default-features = false, optional = true }
+async-trait = { version = "0.1.64", default-features = false }
flate2 = { version = "1.0.28", default-features = false, features = ["rust_backend"], optional = true }
futures = { version = "0.3", default-features = false, optional = true }
-identity_core = { version = "=1.1.0-alpha.1", path = "../identity_core", default-features = false }
-identity_did = { version = "=1.1.0-alpha.1", path = "../identity_did", default-features = false }
-identity_document = { version = "=1.1.0-alpha.1", path = "../identity_document", default-features = false }
-identity_verification = { version = "=1.1.0-alpha.1", path = "../identity_verification", default-features = false }
+identity_core = { version = "=1.3.0", path = "../identity_core", default-features = false }
+identity_did = { version = "=1.3.0", path = "../identity_did", default-features = false }
+identity_document = { version = "=1.3.0", path = "../identity_document", default-features = false }
+identity_verification = { version = "=1.3.0", path = "../identity_verification", default-features = false }
indexmap = { version = "2.0", default-features = false, features = ["std", "serde"] }
itertools = { version = "0.11", default-features = false, features = ["use_std"], optional = true }
+json-proof-token = { workspace = true, optional = true }
once_cell = { version = "1.18", default-features = false, features = ["std"] }
reqwest = { version = "0.11", default-features = false, features = ["default-tls", "json", "stream"], optional = true }
-roaring = { version = "0.10", default-features = false, optional = true }
-sd-jwt-payload = { version = "0.1.2", default-features = false, features = ["sha"], optional = true }
+roaring = { version = "0.10.2", default-features = false, features = ["serde"], optional = true }
+sd-jwt-payload = { version = "0.2.1", default-features = false, features = ["sha"], optional = true }
serde.workspace = true
-serde-aux = { version = "4.3.1", default-features = false, optional = true }
+serde-aux = { version = "4.3.1", default-features = false }
serde_json.workspace = true
serde_repr = { version = "0.1", default-features = false, optional = true }
strum.workspace = true
thiserror.workspace = true
url = { version = "2.5", default-features = false }
+zkryptium = { workspace = true, optional = true }
[dev-dependencies]
anyhow = "1.0.62"
@@ -50,9 +52,13 @@ rustdoc-args = ["--cfg", "docsrs"]
default = ["revocation-bitmap", "validator", "credential", "presentation", "domain-linkage-fetch", "sd-jwt"]
credential = []
presentation = ["credential"]
-revocation-bitmap = ["dep:dataurl", "dep:flate2", "dep:roaring"]
-status-list-2021 = ["revocation-bitmap", "dep:serde-aux"]
+revocation-bitmap = ["dep:flate2", "dep:roaring"]
+status-list-2021 = ["revocation-bitmap"]
validator = ["dep:itertools", "dep:serde_repr", "credential", "presentation"]
domain-linkage = ["validator"]
domain-linkage-fetch = ["domain-linkage", "dep:reqwest", "dep:futures"]
-sd-jwt = ["credential", "validator", "sd-jwt-payload"]
+sd-jwt = ["credential", "validator", "dep:sd-jwt-payload"]
+jpt-bbs-plus = ["credential", "validator", "dep:zkryptium", "dep:json-proof-token"]
+
+[lints]
+workspace = true
diff --git a/identity_credential/src/credential/credential.rs b/identity_credential/src/credential/credential.rs
index decbb8b7c2..03c482c6f6 100644
--- a/identity_credential/src/credential/credential.rs
+++ b/identity_credential/src/credential/credential.rs
@@ -5,6 +5,8 @@ use core::fmt::Display;
use core::fmt::Formatter;
use identity_core::convert::ToJson;
+#[cfg(feature = "jpt-bbs-plus")]
+use jsonprooftoken::jpt::claims::JptClaims;
use once_cell::sync::Lazy;
use serde::Deserialize;
use serde::Serialize;
@@ -174,6 +176,16 @@ impl Credential {
.to_json()
.map_err(|err| Error::JwtClaimsSetSerializationError(err.into()))
}
+
+ ///Serializes the [`Credential`] as a JPT claims set
+ #[cfg(feature = "jpt-bbs-plus")]
+ pub fn serialize_jpt(&self, custom_claims: Option) -> Result
+ where
+ T: ToOwned + serde::Serialize + serde::de::DeserializeOwned,
+ {
+ let jwt_representation: CredentialJwtClaims<'_, T> = CredentialJwtClaims::new(self, custom_claims)?;
+ Ok(jwt_representation.into())
+ }
}
impl Display for Credential
diff --git a/identity_credential/src/credential/jpt.rs b/identity_credential/src/credential/jpt.rs
new file mode 100644
index 0000000000..feab003949
--- /dev/null
+++ b/identity_credential/src/credential/jpt.rs
@@ -0,0 +1,33 @@
+// Copyright 2020-2024 IOTA Stiftung, Fondazione Links
+// SPDX-License-Identifier: Apache-2.0
+
+use serde::Deserialize;
+use serde::Serialize;
+
+/// This JSON Proof Token could represent a JWP both in the Issued and Presented forms.
+#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
+pub struct Jpt(String);
+
+impl Jpt {
+ /// Creates a new `Jwt` from the given string.
+ pub fn new(jpt_string: String) -> Self {
+ Self(jpt_string)
+ }
+
+ /// Returns a reference of the JWT string.
+ pub fn as_str(&self) -> &str {
+ &self.0
+ }
+}
+
+impl From for Jpt {
+ fn from(jpt: String) -> Self {
+ Self::new(jpt)
+ }
+}
+
+impl From for String {
+ fn from(jpt: Jpt) -> Self {
+ jpt.0
+ }
+}
diff --git a/identity_credential/src/credential/jwp_credential_options.rs b/identity_credential/src/credential/jwp_credential_options.rs
new file mode 100644
index 0000000000..f607c2f68e
--- /dev/null
+++ b/identity_credential/src/credential/jwp_credential_options.rs
@@ -0,0 +1,28 @@
+// Copyright 2020-2024 IOTA Stiftung, Fondazione Links
+// SPDX-License-Identifier: Apache-2.0
+
+/// Options for creating a JSON Web Proof.
+#[non_exhaustive]
+#[derive(Debug, Default, serde::Serialize, serde::Deserialize, Eq, PartialEq, Clone)]
+#[serde(rename_all = "camelCase")]
+#[serde(default)]
+pub struct JwpCredentialOptions {
+ /// The kid to set in the Issuer Protected Header.
+ ///
+ /// If unset, the kid of the JWK with which the JWP is produced is used.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub kid: Option,
+}
+
+impl JwpCredentialOptions {
+ /// Creates a new [`JwsSignatureOptions`].
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ /// Replace the value of the `kid` field.
+ pub fn kid(mut self, value: impl Into) -> Self {
+ self.kid = Some(value.into());
+ self
+ }
+}
diff --git a/identity_credential/src/credential/jwt_serialization.rs b/identity_credential/src/credential/jwt_serialization.rs
index 8a9bc280e8..6ce3c60b67 100644
--- a/identity_credential/src/credential/jwt_serialization.rs
+++ b/identity_credential/src/credential/jwt_serialization.rs
@@ -3,6 +3,8 @@
use std::borrow::Cow;
+#[cfg(feature = "jpt-bbs-plus")]
+use jsonprooftoken::jpt::claims::JptClaims;
use serde::Deserialize;
use serde::Serialize;
@@ -360,6 +362,57 @@ where
proof: Option>,
}
+#[cfg(feature = "jpt-bbs-plus")]
+impl<'credential, T> From> for JptClaims
+where
+ T: ToOwned + Serialize,
+ ::Owned: DeserializeOwned,
+{
+ fn from(item: CredentialJwtClaims<'credential, T>) -> Self {
+ let CredentialJwtClaims {
+ exp,
+ iss,
+ issuance_date,
+ jti,
+ sub,
+ vc,
+ custom,
+ } = item;
+
+ let mut claims = JptClaims::new();
+
+ if let Some(exp) = exp {
+ claims.set_exp(exp);
+ }
+
+ claims.set_iss(iss.url().to_string());
+
+ if let Some(iat) = issuance_date.iat {
+ claims.set_iat(iat);
+ }
+
+ if let Some(nbf) = issuance_date.nbf {
+ claims.set_nbf(nbf);
+ }
+
+ if let Some(jti) = jti {
+ claims.set_jti(jti.to_string());
+ }
+
+ if let Some(sub) = sub {
+ claims.set_sub(sub.to_string());
+ }
+
+ claims.set_claim(Some("vc"), vc, true);
+
+ if let Some(custom) = custom {
+ claims.set_claim(None, custom, true);
+ }
+
+ claims
+ }
+}
+
#[cfg(test)]
mod tests {
use identity_core::common::Object;
diff --git a/identity_credential/src/credential/linked_domain_service.rs b/identity_credential/src/credential/linked_domain_service.rs
index c6efbae255..3a76b10eb5 100644
--- a/identity_credential/src/credential/linked_domain_service.rs
+++ b/identity_credential/src/credential/linked_domain_service.rs
@@ -144,6 +144,11 @@ impl LinkedDomainService {
.as_slice(),
}
}
+
+ /// Returns a reference to the `Service` id.
+ pub fn id(&self) -> &DIDUrl {
+ self.service.id()
+ }
}
#[cfg(test)]
diff --git a/identity_credential/src/credential/mod.rs b/identity_credential/src/credential/mod.rs
index efa20a3c87..72f3b5d7a8 100644
--- a/identity_credential/src/credential/mod.rs
+++ b/identity_credential/src/credential/mod.rs
@@ -9,6 +9,10 @@ mod builder;
mod credential;
mod evidence;
mod issuer;
+#[cfg(feature = "jpt-bbs-plus")]
+mod jpt;
+#[cfg(feature = "jpt-bbs-plus")]
+mod jwp_credential_options;
mod jws;
mod jwt;
mod jwt_serialization;
@@ -26,6 +30,10 @@ pub use self::builder::CredentialBuilder;
pub use self::credential::Credential;
pub use self::evidence::Evidence;
pub use self::issuer::Issuer;
+#[cfg(feature = "jpt-bbs-plus")]
+pub use self::jpt::Jpt;
+#[cfg(feature = "jpt-bbs-plus")]
+pub use self::jwp_credential_options::JwpCredentialOptions;
pub use self::jws::Jws;
pub use self::jwt::Jwt;
pub use self::linked_domain_service::LinkedDomainService;
@@ -33,6 +41,8 @@ pub use self::policy::Policy;
pub use self::proof::Proof;
pub use self::refresh::RefreshService;
#[cfg(feature = "revocation-bitmap")]
+pub use self::revocation_bitmap_status::try_index_to_u32;
+#[cfg(feature = "revocation-bitmap")]
pub use self::revocation_bitmap_status::RevocationBitmapStatus;
pub use self::schema::Schema;
pub use self::status::Status;
diff --git a/identity_credential/src/credential/proof.rs b/identity_credential/src/credential/proof.rs
index 03e4bca663..ab779014a2 100644
--- a/identity_credential/src/credential/proof.rs
+++ b/identity_credential/src/credential/proof.rs
@@ -52,7 +52,7 @@ mod tests {
assert_eq!(proof.type_, "test-proof");
let value = proof
.properties
- .get(&"signature".to_owned())
+ .get("signature")
.expect("property in proof doesn't exist");
assert_eq!(value, "abc123");
}
@@ -88,7 +88,7 @@ mod tests {
assert_eq!(proof.type_, "RsaSignature2018");
let value = proof
.properties
- .get(&"proofPurpose".to_owned())
+ .get("proofPurpose")
.expect("property in proof doesn't exist");
assert_eq!(value, "assertionMethod");
assert_eq!(proof.properties.len(), 4);
diff --git a/identity_credential/src/credential/revocation_bitmap_status.rs b/identity_credential/src/credential/revocation_bitmap_status.rs
index d4310d154a..b607e1758d 100644
--- a/identity_credential/src/credential/revocation_bitmap_status.rs
+++ b/identity_credential/src/credential/revocation_bitmap_status.rs
@@ -129,7 +129,7 @@ impl From for Status {
}
/// Attempts to convert the given index string to a u32.
-fn try_index_to_u32(index: &str, name: &str) -> Result {
+pub fn try_index_to_u32(index: &str, name: &str) -> Result {
u32::from_str(index).map_err(|err| {
Error::InvalidStatus(format!(
"{name} cannot be converted to an unsigned, 32-bit integer: {err}",
diff --git a/identity_credential/src/error.rs b/identity_credential/src/error.rs
index 356d89d3d2..468370e460 100644
--- a/identity_credential/src/error.rs
+++ b/identity_credential/src/error.rs
@@ -35,7 +35,7 @@ pub enum Error {
#[error("invalid credential status: {0}")]
InvalidStatus(String),
/// Caused when constructing an invalid `LinkedDomainService` or `DomainLinkageConfiguration`.
- #[error("domain linkage error")]
+ #[error("domain linkage error: {0}")]
DomainLinkageError(#[source] Box),
/// Caused when attempting to encode a `Credential` containing multiple subjects as a JWT.
#[error("could not create JWT claim set from verifiable credential: more than one subject")]
@@ -68,4 +68,12 @@ pub enum Error {
/// JSON.
#[error("could not deserialize JWT claims set")]
JwtClaimsSetDeserializationError(#[source] Box),
+
+ /// Caused by a failure to deserialize the JPT claims set representation of a `Credential` JSON.
+ #[error("could not deserialize JWT claims set")]
+ JptClaimsSetDeserializationError(#[source] Box),
+
+ /// Cause by an invalid attribute path
+ #[error("Attribute Not found")]
+ SelectiveDisclosureError,
}
diff --git a/identity_credential/src/presentation/jwp_presentation_builder.rs b/identity_credential/src/presentation/jwp_presentation_builder.rs
new file mode 100644
index 0000000000..e6919058a2
--- /dev/null
+++ b/identity_credential/src/presentation/jwp_presentation_builder.rs
@@ -0,0 +1,124 @@
+// Copyright 2020-2024 IOTA Stiftung, Fondazione Links
+// SPDX-License-Identifier: Apache-2.0
+
+use crate::error::Error;
+use crate::error::Result;
+use jsonprooftoken::jwp::header::PresentationProtectedHeader;
+use jsonprooftoken::jwp::issued::JwpIssued;
+use jsonprooftoken::jwp::presented::JwpPresentedBuilder;
+
+/// Used to construct a JwpPresentedBuilder and handle the selective disclosure of attributes.
+// - @context MUST NOT be blinded
+// - id MUST be blinded
+// - type MUST NOT be blinded
+// - issuer MUST NOT be blinded
+// - issuanceDate MUST be blinded (if Timeframe Revocation mechanism is used)
+// - expirationDate MUST be blinded (if Timeframe Revocation mechanism is used)
+// - credentialSubject (Users have to choose which attribute must be blinded)
+// - credentialSchema MUST NOT be blinded
+// - credentialStatus MUST NOT be blinded
+// - refreshService MUST NOT be blinded (probably will be used for Timeslot Revocation mechanism)
+// - termsOfUse NO reason to use it in ZK VC (will be in any case blinded)
+// - evidence (Users have to choose which attribute must be blinded)
+pub struct SelectiveDisclosurePresentation {
+ jwp_builder: JwpPresentedBuilder,
+}
+
+impl SelectiveDisclosurePresentation {
+ /// Initialize a presentation starting from an Issued JWP.
+ /// The following properties are concealed by default:
+ ///
+ /// - `exp`
+ /// - `expirationDate`
+ /// - `issuanceDate`
+ /// - `jti`
+ /// - `nbf`
+ /// - `sub`
+ /// - `termsOfUse`
+ /// - `vc.credentialStatus.revocationBitmapIndex`
+ /// - `vc.credentialSubject.id`
+ pub fn new(issued_jwp: &JwpIssued) -> Self {
+ let mut jwp_builder = JwpPresentedBuilder::new(issued_jwp);
+
+ jwp_builder.set_undisclosed("jti").ok(); // contains the credential's id, provides linkability
+
+ jwp_builder.set_undisclosed("issuanceDate").ok(); // Depending on the revocation method used it will be necessary or not
+ jwp_builder.set_undisclosed("nbf").ok();
+
+ jwp_builder.set_undisclosed("expirationDate").ok(); // Depending on the revocation method used it will be necessary or not
+ jwp_builder.set_undisclosed("exp").ok();
+
+ jwp_builder.set_undisclosed("termsOfUse").ok(); // Provides linkability so, there is NO reason to use it in ZK VC
+
+ jwp_builder
+ .set_undisclosed("vc.credentialStatus.revocationBitmapIndex")
+ .ok();
+
+ jwp_builder.set_undisclosed("vc.credentialSubject.id").ok();
+ jwp_builder.set_undisclosed("sub").ok();
+
+ Self { jwp_builder }
+ }
+
+ /// Selectively conceal "credentialSubject" attributes.
+ /// # Example
+ /// ```ignore
+ /// {
+ /// "id": 1234,
+ /// "name": "Alice",
+ /// "mainCourses": ["Object-oriented Programming", "Mathematics"],
+ /// "degree": {
+ /// "type": "BachelorDegree",
+ /// "name": "Bachelor of Science and Arts",
+ /// },
+ /// "GPA": "4.0",
+ /// }
+ /// ```
+ /// If you want to undisclose for example the Mathematics course and the name of the degree:
+ /// ```ignore
+ /// presentation_builder.conceal_in_subject("mainCourses[1]");
+ /// presentation_builder.conceal_in_subject("degree.name");
+ /// ```
+ pub fn conceal_in_subject(&mut self, path: &str) -> Result<(), Error> {
+ let _ = self
+ .jwp_builder
+ .set_undisclosed(&("vc.credentialSubject.".to_owned() + path))
+ .map_err(|_| Error::SelectiveDisclosureError);
+ Ok(())
+ }
+
+ /// Undisclose "evidence" attributes.
+ /// # Example
+ /// ```ignore
+ /// {
+ /// "id": "https://example.edu/evidence/f2aeec97-fc0d-42bf-8ca7-0548192d4231",
+ /// "type": ["DocumentVerification"],
+ /// "verifier": "https://example.edu/issuers/14",
+ /// "evidenceDocument": "DriversLicense",
+ /// "subjectPresence": "Physical",
+ /// "documentPresence": "Physical",
+ /// "licenseNumber": "123AB4567"
+ /// }
+ /// ```
+ /// To conceal the `licenseNumber` field:
+ /// ```ignore
+ /// presentation_builder.conceal_in_evidence("licenseNumber");
+ /// ```
+ pub fn conceal_in_evidence(&mut self, path: &str) -> Result<(), Error> {
+ let _ = self
+ .jwp_builder
+ .set_undisclosed(&("vc.evidence.".to_owned() + path))
+ .map_err(|_| Error::SelectiveDisclosureError);
+ Ok(())
+ }
+
+ /// Set Presentation Protected Header.
+ pub fn set_presentation_header(&mut self, ph: PresentationProtectedHeader) {
+ self.jwp_builder.set_presentation_protected_header(ph);
+ }
+
+ /// Get the builder.
+ pub fn builder(&self) -> &JwpPresentedBuilder {
+ &self.jwp_builder
+ }
+}
diff --git a/identity_credential/src/presentation/jwp_presentation_options.rs b/identity_credential/src/presentation/jwp_presentation_options.rs
new file mode 100644
index 0000000000..fba35a7f1f
--- /dev/null
+++ b/identity_credential/src/presentation/jwp_presentation_options.rs
@@ -0,0 +1,33 @@
+// Copyright 2020-2024 IOTA Stiftung, Fondazione Links
+// SPDX-License-Identifier: Apache-2.0
+
+use identity_core::common::Url;
+use serde::Deserialize;
+use serde::Serialize;
+
+/// Options to be set in the JWT claims of a verifiable presentation.
+#[derive(Clone, Debug, Serialize, Deserialize, Default)]
+pub struct JwpPresentationOptions {
+ /// Sets the audience for presentation (`aud` property in JWP Presentation Header).
+ /// Default: `None`.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub audience: Option,
+
+ /// The nonce to be placed in the Presentation Protected Header.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub nonce: Option,
+}
+
+impl JwpPresentationOptions {
+ /// Sets the audience for presentation (`aud` property in JWT claims).
+ pub fn audience(mut self, audience: Url) -> Self {
+ self.audience = Some(audience);
+ self
+ }
+
+ /// Replace the value of the `nonce` field.
+ pub fn nonce(mut self, value: impl Into) -> Self {
+ self.nonce = Some(value.into());
+ self
+ }
+}
diff --git a/identity_credential/src/presentation/mod.rs b/identity_credential/src/presentation/mod.rs
index 94f8768e02..76adc145c6 100644
--- a/identity_credential/src/presentation/mod.rs
+++ b/identity_credential/src/presentation/mod.rs
@@ -5,14 +5,22 @@
#![allow(clippy::module_inception)]
+#[cfg(feature = "jpt-bbs-plus")]
+mod jwp_presentation_builder;
+#[cfg(feature = "jpt-bbs-plus")]
+mod jwp_presentation_options;
mod jwt_presentation_options;
mod jwt_serialization;
mod presentation;
mod presentation_builder;
+#[cfg(feature = "jpt-bbs-plus")]
+pub use self::jwp_presentation_builder::SelectiveDisclosurePresentation;
pub use self::jwt_presentation_options::JwtPresentationOptions;
pub use self::presentation::Presentation;
pub use self::presentation_builder::PresentationBuilder;
+#[cfg(feature = "jpt-bbs-plus")]
+pub use jwp_presentation_options::JwpPresentationOptions;
#[cfg(feature = "validator")]
pub(crate) use self::jwt_serialization::PresentationJwtClaims;
diff --git a/identity_credential/src/revocation/mod.rs b/identity_credential/src/revocation/mod.rs
index 6732ff4194..1553022c74 100644
--- a/identity_credential/src/revocation/mod.rs
+++ b/identity_credential/src/revocation/mod.rs
@@ -9,6 +9,11 @@ mod revocation_bitmap_2022;
#[cfg(feature = "status-list-2021")]
pub mod status_list_2021;
+#[cfg(feature = "jpt-bbs-plus")]
+pub mod validity_timeframe_2024;
+
pub use self::error::RevocationError;
pub use self::error::RevocationResult;
pub use revocation_bitmap_2022::*;
+#[cfg(feature = "jpt-bbs-plus")]
+pub use validity_timeframe_2024::*;
diff --git a/identity_credential/src/revocation/revocation_bitmap_2022/bitmap.rs b/identity_credential/src/revocation/revocation_bitmap_2022/bitmap.rs
index 6f47db97be..2dd61ba324 100644
--- a/identity_credential/src/revocation/revocation_bitmap_2022/bitmap.rs
+++ b/identity_credential/src/revocation/revocation_bitmap_2022/bitmap.rs
@@ -1,9 +1,9 @@
// Copyright 2020-2023 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0
+use std::borrow::Cow;
use std::io::Write;
-use dataurl::DataUrl;
use flate2::write::ZlibDecoder;
use flate2::write::ZlibEncoder;
use flate2::Compression;
@@ -18,7 +18,7 @@ use crate::revocation::error::RevocationError;
use identity_document::service::Service;
use identity_document::service::ServiceEndpoint;
-const DATA_URL_MEDIA_TYPE: &str = "application/octet-stream";
+const DATA_URL_PATTERN: &str = "data:application/octet-stream;base64,";
/// A compressed bitmap for managing credential revocation.
#[derive(Clone, Debug, Default, PartialEq)]
@@ -80,11 +80,8 @@ impl RevocationBitmap {
pub(crate) fn to_endpoint(&self) -> Result {
let endpoint_data: String = self.serialize_compressed_base64()?;
- let mut data_url: DataUrl = DataUrl::new();
- data_url.set_media_type(Some(DATA_URL_MEDIA_TYPE.to_owned()));
- data_url.set_is_base64_encoded(true);
- data_url.set_data(endpoint_data.as_bytes());
- Url::parse(data_url.to_string())
+ let data_url = format!("{DATA_URL_PATTERN}{endpoint_data}");
+ Url::parse(data_url)
.map(ServiceEndpoint::One)
.map_err(|e| RevocationError::UrlConstructionError(e.into()))
}
@@ -92,19 +89,13 @@ impl RevocationBitmap {
/// Construct a `RevocationBitmap` from a data url embedded in `service_endpoint`.
pub(crate) fn try_from_endpoint(service_endpoint: &ServiceEndpoint) -> Result {
if let ServiceEndpoint::One(url) = service_endpoint {
- let data_url: DataUrl = DataUrl::parse(url.as_str())
- .map_err(|_| RevocationError::InvalidService("invalid url - expected a data url"))?;
-
- if !data_url.get_is_base64_encoded() || data_url.get_media_type() != DATA_URL_MEDIA_TYPE {
+ let Some(encoded_bitmap) = url.as_str().strip_prefix(DATA_URL_PATTERN) else {
return Err(RevocationError::InvalidService(
"invalid url - expected an `application/octet-stream;base64` data url",
));
- }
+ };
- RevocationBitmap::deserialize_compressed_base64(
- std::str::from_utf8(data_url.get_data())
- .map_err(|_| RevocationError::InvalidService("invalid data url - expected valid utf-8"))?,
- )
+ RevocationBitmap::deserialize_compressed_base64(encoded_bitmap)
} else {
Err(RevocationError::InvalidService(
"invalid endpoint - expected a single data url",
@@ -117,7 +108,22 @@ impl RevocationBitmap {
where
T: AsRef + ?Sized,
{
- let decoded_data: Vec = BaseEncoding::decode(data, Base::Base64Url)
+ // Fixes issue #1291.
+ // Before this fix, revocation bitmaps had been encoded twice, like so:
+ // Base64Url(Base64(compressed_bitmap)).
+ // This fix checks if the encoded string it receives as input has undergone such process
+ // and undo the inner Base64 encoding before processing the input further.
+ let mut data = Cow::Borrowed(data.as_ref());
+ if !data.starts_with("eJy") {
+ // Base64 encoded zlib default compression header
+ let decoded = BaseEncoding::decode(&data, Base::Base64)
+ .map_err(|e| RevocationError::Base64DecodingError(data.into_owned(), e))?;
+ data = Cow::Owned(
+ String::from_utf8(decoded)
+ .map_err(|_| RevocationError::InvalidService("invalid data url - expected valid utf-8"))?,
+ );
+ }
+ let decoded_data: Vec = BaseEncoding::decode(&data, Base::Base64Url)
.map_err(|e| RevocationError::Base64DecodingError(data.as_ref().to_owned(), e))?;
let decompressed_data: Vec = Self::decompress_zlib(decoded_data)?;
Self::deserialize_slice(&decompressed_data)
@@ -215,7 +221,7 @@ mod tests {
#[test]
fn test_revocation_bitmap_test_vector_1() {
- const URL: &str = "data:application/octet-stream;base64,ZUp5ek1tQUFBd0FES0FCcg==";
+ const URL: &str = "data:application/octet-stream;base64,eJyzMmAAAwADKABr";
let bitmap: RevocationBitmap = RevocationBitmap::try_from_endpoint(
&identity_document::service::ServiceEndpoint::One(Url::parse(URL).unwrap()),
@@ -227,8 +233,8 @@ mod tests {
#[test]
fn test_revocation_bitmap_test_vector_2() {
- const URL: &str = "data:application/octet-stream;base64,ZUp5ek1tQmdZR0lBQVVZZ1pHQ1FBR0laSUdabDZHUGN3UW9BRXVvQjlB";
- const EXPECTED: &[u32] = &[5, 398, 67000];
+ const URL: &str = "data:application/octet-stream;base64,eJyzMmBgYGQAAWYGATDNysDGwMEAAAscAJI";
+ const EXPECTED: &[u32] = &[0, 5, 6, 8];
let bitmap: RevocationBitmap = RevocationBitmap::try_from_endpoint(
&identity_document::service::ServiceEndpoint::One(Url::parse(URL).unwrap()),
@@ -239,22 +245,38 @@ mod tests {
assert!(bitmap.is_revoked(*revoked));
}
- assert_eq!(bitmap.len(), 3);
+ assert_eq!(bitmap.len(), 4);
}
#[test]
fn test_revocation_bitmap_test_vector_3() {
- const URL: &str = "data:application/octet-stream;base64,ZUp6dHhERVJBQ0FNQkxESEFWS1lXZkN2Q3E0MmFESmtyMlNrM0ROckFLQ2RBQUFBQUFBQTMzbGhHZm9q";
+ const URL: &str = "data:application/octet-stream;base64,eJyzMmBgYGQAAWYGASCpxbCEMUNAYAkAEpcCeg";
+ const EXPECTED: &[u32] = &[42, 420, 4200, 42000];
let bitmap: RevocationBitmap = RevocationBitmap::try_from_endpoint(
&identity_document::service::ServiceEndpoint::One(Url::parse(URL).unwrap()),
)
.unwrap();
- for index in 0..2u32.pow(14) {
+ for &index in EXPECTED {
assert!(bitmap.is_revoked(index));
}
+ }
+
+ #[test]
+ fn test_revocation_bitmap_pre_1291_fix() {
+ const URL: &str = "data:application/octet-stream;base64,ZUp5ek1tQmdZR0lBQVVZZ1pHQ1FBR0laSUdabDZHUGN3UW9BRXVvQjlB";
+ const EXPECTED: &[u32] = &[5, 398, 67000];
+
+ let bitmap: RevocationBitmap = RevocationBitmap::try_from_endpoint(
+ &identity_document::service::ServiceEndpoint::One(Url::parse(URL).unwrap()),
+ )
+ .unwrap();
+
+ for revoked in EXPECTED {
+ assert!(bitmap.is_revoked(*revoked));
+ }
- assert_eq!(bitmap.len(), 2u64.pow(14));
+ assert_eq!(bitmap.len(), 3);
}
}
diff --git a/identity_credential/src/revocation/status_list_2021/credential.rs b/identity_credential/src/revocation/status_list_2021/credential.rs
index 3588772e82..4402283e1a 100644
--- a/identity_credential/src/revocation/status_list_2021/credential.rs
+++ b/identity_credential/src/revocation/status_list_2021/credential.rs
@@ -20,7 +20,7 @@ const CREDENTIAL_SUBJECT_TYPE: &str = "StatusList2021";
/// [Error](std::error::Error) type that represents the possible errors that can be
/// encountered when dealing with [`StatusList2021Credential`]s.
-#[derive(Clone, Debug, Error, strum::IntoStaticStr)]
+#[derive(Clone, Debug, Error, strum::IntoStaticStr, PartialEq, Eq)]
pub enum StatusList2021CredentialError {
/// The provided [`Credential`] has more than one `credentialSubject`.
#[error("A StatusList2021Credential may only have one credentialSubject")]
@@ -34,9 +34,12 @@ pub enum StatusList2021CredentialError {
/// Inner status list failures.
#[error(transparent)]
StatusListError(#[from] StatusListError),
- /// Missing status list id
+ /// Missing status list id.
#[error("Cannot set the status of a credential without a \"credentialSubject.id\".")]
Unreferenceable,
+ /// Credentials cannot be unrevoked.
+ #[error("A previously revoked credential cannot be unrevoked.")]
+ UnreversibleRevocation,
}
use crate::credential::Credential;
@@ -117,6 +120,11 @@ impl StatusList2021Credential {
/// Sets the credential status of a given [`Credential`],
/// mapping it to the `index`-th entry of this [`StatusList2021Credential`].
+ ///
+ /// ## Note:
+ /// - A revoked credential cannot ever be unrevoked and will lead to a
+ /// [`StatusList2021CredentialError::UnreversibleRevocation`].
+ /// - Trying to set `revoked_or_suspended` to `false` for an already valid credential will have no impact.
pub fn set_credential_status(
&mut self,
credential: &mut Credential,
@@ -135,9 +143,28 @@ impl StatusList2021Credential {
Ok(entry)
}
+ /// Apply `update_fn` to the status list encoded in this credential.
+ pub fn update(&mut self, update_fn: F) -> Result<(), StatusList2021CredentialError>
+ where
+ F: FnOnce(&mut MutStatusList) -> Result<(), StatusList2021CredentialError>,
+ {
+ let mut encapsuled_status_list = MutStatusList {
+ status_list: self.status_list()?,
+ purpose: self.purpose(),
+ };
+ update_fn(&mut encapsuled_status_list)?;
+
+ self.subject.encoded_list = encapsuled_status_list.status_list.into_encoded_str();
+ Ok(())
+ }
+
/// Sets the `index`-th entry to `value`
pub(crate) fn set_entry(&mut self, index: usize, value: bool) -> Result<(), StatusList2021CredentialError> {
let mut status_list = self.status_list()?;
+ let entry_status = status_list.get(index)?;
+ if self.purpose() == StatusPurpose::Revocation && !value && entry_status {
+ return Err(StatusList2021CredentialError::UnreversibleRevocation);
+ }
status_list.set(index, value)?;
self.subject.encoded_list = status_list.into_encoded_str();
@@ -155,6 +182,25 @@ impl StatusList2021Credential {
}
}
+/// A wrapper over the [`StatusList2021`] contained in a [`StatusList2021Credential`]
+/// that allows for its mutation.
+pub struct MutStatusList {
+ status_list: StatusList2021,
+ purpose: StatusPurpose,
+}
+
+impl MutStatusList {
+ /// Sets the value of the `index`-th entry in the status list.
+ pub fn set_entry(&mut self, index: usize, value: bool) -> Result<(), StatusList2021CredentialError> {
+ let entry_status = self.status_list.get(index)?;
+ if self.purpose == StatusPurpose::Revocation && !value && entry_status {
+ return Err(StatusList2021CredentialError::UnreversibleRevocation);
+ }
+ self.status_list.set(index, value)?;
+ Ok(())
+ }
+}
+
/// The status of a credential referenced inside a [`StatusList2021Credential`]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum CredentialStatus {
@@ -229,11 +275,11 @@ impl From for Subject {
impl StatusList2021CredentialSubject {
/// Parse a StatusListCredentialSubject out of a credential, without copying.
fn try_from_credential(credential: &mut Credential) -> Result {
- let OneOrMany::One(subject) = &mut credential.credential_subject else {
+ let OneOrMany::One(mut subject) = std::mem::take(&mut credential.credential_subject) else {
return Err(StatusList2021CredentialError::MultipleCredentialSubject);
};
if let Some(subject_type) = subject.properties.get("type") {
- if !subject_type.as_str().is_some_and(|t| t == CREDENTIAL_SUBJECT_TYPE) {
+ if subject_type.as_str() != Some(CREDENTIAL_SUBJECT_TYPE) {
return Err(StatusList2021CredentialError::InvalidProperty("credentialSubject.type"));
}
} else {
@@ -271,7 +317,7 @@ impl StatusList2021CredentialSubject {
.map(std::mem::take)?;
Ok(StatusList2021CredentialSubject {
- id: std::mem::take(&mut subject.id),
+ id: subject.id,
encoded_list,
status_purpose,
})
@@ -351,11 +397,17 @@ impl StatusList2021CredentialBuilder {
.inner_builder
.type_(CREDENTIAL_TYPE)
.issuance_date(Timestamp::now_utc())
- .subject(self.credential_subject.clone().into())
+ .subject(Subject {
+ id: self.credential_subject.id.clone(),
+ ..Default::default()
+ })
.build()
- .map(|credential| StatusList2021Credential {
- subject: self.credential_subject,
- inner: credential,
+ .map(|mut credential| {
+ credential.credential_subject = OneOrMany::default();
+ StatusList2021Credential {
+ subject: self.credential_subject,
+ inner: credential,
+ }
})
}
}
@@ -403,4 +455,35 @@ mod tests {
.expect("Failed to deserialize");
assert_eq!(credential.purpose(), StatusPurpose::Revocation);
}
+ #[test]
+ fn revoked_credential_cannot_be_unrevoked() {
+ let url = Url::parse("http://example.com").unwrap();
+ let mut status_list_credential = StatusList2021CredentialBuilder::new(StatusList2021::default())
+ .issuer(Issuer::Url(url.clone()))
+ .purpose(StatusPurpose::Revocation)
+ .subject_id(url)
+ .build()
+ .unwrap();
+
+ assert!(status_list_credential.set_entry(420, false).is_ok());
+ status_list_credential.set_entry(420, true).unwrap();
+ assert_eq!(
+ status_list_credential.set_entry(420, false),
+ Err(StatusList2021CredentialError::UnreversibleRevocation)
+ );
+ }
+ #[test]
+ fn suspended_credential_can_be_unsuspended() {
+ let url = Url::parse("http://example.com").unwrap();
+ let mut status_list_credential = StatusList2021CredentialBuilder::new(StatusList2021::default())
+ .issuer(Issuer::Url(url.clone()))
+ .purpose(StatusPurpose::Suspension)
+ .subject_id(url)
+ .build()
+ .unwrap();
+
+ assert!(status_list_credential.set_entry(420, false).is_ok());
+ status_list_credential.set_entry(420, true).unwrap();
+ assert!(status_list_credential.set_entry(420, false).is_ok());
+ }
}
diff --git a/identity_credential/src/revocation/validity_timeframe_2024/mod.rs b/identity_credential/src/revocation/validity_timeframe_2024/mod.rs
new file mode 100644
index 0000000000..179d5696ec
--- /dev/null
+++ b/identity_credential/src/revocation/validity_timeframe_2024/mod.rs
@@ -0,0 +1,8 @@
+// Copyright 2020-2024 IOTA Stiftung, Fondazione Links
+// SPDX-License-Identifier: Apache-2.0
+
+//! Implementation of a new Revocation mechanism for ZK Verifiable Credentials.
+
+mod revocation_timeframe_status;
+
+pub use revocation_timeframe_status::*;
diff --git a/identity_credential/src/revocation/validity_timeframe_2024/revocation_timeframe_status.rs b/identity_credential/src/revocation/validity_timeframe_2024/revocation_timeframe_status.rs
new file mode 100644
index 0000000000..0a70589112
--- /dev/null
+++ b/identity_credential/src/revocation/validity_timeframe_2024/revocation_timeframe_status.rs
@@ -0,0 +1,220 @@
+// Copyright 2020-2024 IOTA Stiftung, Fondazione Links
+// SPDX-License-Identifier: Apache-2.0
+
+use crate::credential::Status;
+use crate::error::Error;
+use crate::error::Result;
+use identity_core::common::Duration;
+use identity_core::common::Object;
+use identity_core::common::Timestamp;
+use identity_core::common::Url;
+use identity_core::common::Value;
+use serde::de::Visitor;
+use serde::Deserialize;
+use serde::Serialize;
+
+fn deserialize_status_entry_type<'de, D>(deserializer: D) -> Result
+where
+ D: serde::Deserializer<'de>,
+{
+ struct ExactStrVisitor(&'static str);
+ impl<'a> Visitor<'a> for ExactStrVisitor {
+ type Value = &'static str;
+ fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(formatter, "the exact string \"{}\"", self.0)
+ }
+ fn visit_str(self, str: &str) -> Result {
+ if str == self.0 {
+ Ok(self.0)
+ } else {
+ Err(E::custom(format!("not \"{}\"", self.0)))
+ }
+ }
+ }
+
+ deserializer
+ .deserialize_str(ExactStrVisitor(RevocationTimeframeStatus::TYPE))
+ .map(ToOwned::to_owned)
+}
+
+/// Information used to determine the current status of a [`Credential`][crate::credential::Credential]
+#[derive(Debug, Clone, Serialize, Deserialize, Hash, Eq, PartialEq)]
+#[serde(rename_all = "camelCase")]
+pub struct RevocationTimeframeStatus {
+ id: Url,
+ #[serde(rename = "type", deserialize_with = "deserialize_status_entry_type")]
+ type_: String,
+ start_validity_timeframe: Timestamp,
+ end_validity_timeframe: Timestamp,
+ #[serde(
+ deserialize_with = "serde_aux::prelude::deserialize_option_number_from_string",
+ skip_serializing_if = "Option::is_none"
+ )]
+ revocation_bitmap_index: Option,
+}
+
+impl RevocationTimeframeStatus {
+ /// startValidityTimeframe property name.
+ pub const START_TIMEFRAME_PROPERTY: &'static str = "startValidityTimeframe";
+ /// endValidityTimeframe property name.
+ pub const END_TIMEFRAME_PROPERTY: &'static str = "endValidityTimeframe";
+ /// Type name of the revocation mechanism.
+ pub const TYPE: &'static str = "RevocationTimeframe2024";
+ /// index property name for [`Status`] conversion
+ const INDEX_PROPERTY: &'static str = "revocationBitmapIndex";
+
+ /// Creates a new `RevocationTimeframeStatus`.
+ pub fn new(start_validity: Option, duration: Duration, id: Url, index: u32) -> Result {
+ let start_validity_timeframe = start_validity.unwrap_or(Timestamp::now_utc());
+ let end_validity_timeframe = start_validity_timeframe
+ .checked_add(duration)
+ .ok_or(Error::InvalidStatus(
+ "With that granularity, endValidityTimeFrame will turn out not to be in the valid range for RFC 3339"
+ .to_owned(),
+ ))?;
+
+ Ok(Self {
+ id,
+ type_: Self::TYPE.to_owned(),
+ start_validity_timeframe,
+ end_validity_timeframe,
+ revocation_bitmap_index: Some(index),
+ })
+ }
+
+ /// Get startValidityTimeframe value.
+ pub fn start_validity_timeframe(&self) -> Timestamp {
+ self.start_validity_timeframe
+ }
+
+ /// Get endValidityTimeframe value.
+ pub fn end_validity_timeframe(&self) -> Timestamp {
+ self.end_validity_timeframe
+ }
+
+ /// Returns the [`Url`] of the `RevocationBitmapStatus`, which should resolve
+ /// to a `RevocationBitmap2022` service in a DID Document.
+ pub fn id(&self) -> &Url {
+ &self.id
+ }
+
+ /// Returns the index of the credential in the issuer's revocation bitmap if it can be decoded.
+ pub fn index(&self) -> Option {
+ self.revocation_bitmap_index
+ }
+}
+
+impl TryFrom<&Status> for RevocationTimeframeStatus {
+ type Error = Error;
+ fn try_from(status: &Status) -> Result {
+ // serialize into String to ensure macros work properly
+ // see [issue](https://github.com/iddm/serde-aux/issues/34#issuecomment-1508207530) in `serde-aux`
+ let json_status: String = serde_json::to_string(&status)
+ .map_err(|err| Self::Error::InvalidStatus(format!("failed to read `Status`; {}", &err.to_string())))?;
+ serde_json::from_str(&json_status).map_err(|err| {
+ Self::Error::InvalidStatus(format!(
+ "failed to convert `Status` to `RevocationTimeframeStatus`; {}",
+ &err.to_string(),
+ ))
+ })
+ }
+}
+
+impl From for Status {
+ fn from(revocation_timeframe_status: RevocationTimeframeStatus) -> Self {
+ let mut properties = Object::new();
+ properties.insert(
+ RevocationTimeframeStatus::START_TIMEFRAME_PROPERTY.to_owned(),
+ Value::String(revocation_timeframe_status.start_validity_timeframe().to_rfc3339()),
+ );
+ properties.insert(
+ RevocationTimeframeStatus::END_TIMEFRAME_PROPERTY.to_owned(),
+ Value::String(revocation_timeframe_status.end_validity_timeframe().to_rfc3339()),
+ );
+ if let Some(value) = revocation_timeframe_status.index() {
+ properties.insert(
+ RevocationTimeframeStatus::INDEX_PROPERTY.to_owned(),
+ Value::String(value.to_string()),
+ );
+ }
+
+ Status::new_with_properties(
+ revocation_timeframe_status.id,
+ RevocationTimeframeStatus::TYPE.to_owned(),
+ properties,
+ )
+ }
+}
+
+/// Verifier
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct VerifierRevocationTimeframeStatus(pub(crate) RevocationTimeframeStatus);
+
+impl TryFrom for VerifierRevocationTimeframeStatus {
+ type Error = Error;
+
+ fn try_from(status: Status) -> Result {
+ Ok(Self((&status).try_into().map_err(|err: Error| {
+ Self::Error::InvalidStatus(format!(
+ "failed to convert `Status` to `VerifierRevocationTimeframeStatus`; {}",
+ &err.to_string()
+ ))
+ })?))
+ }
+}
+
+impl From for Status {
+ fn from(status: VerifierRevocationTimeframeStatus) -> Self {
+ status.0.into()
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ const EXAMPLE_SERIALIZED: &str = r#"{
+ "id": "did:iota:snd:0xae6ccfdb155a69e0ef153fb5fcfd50c08a8fee36babe1f7d71dede8f4e202432#my-revocation-service",
+ "startValidityTimeframe": "2024-03-19T13:57:50Z",
+ "endValidityTimeframe": "2024-03-19T13:58:50Z",
+ "revocationBitmapIndex": "5",
+ "type": "RevocationTimeframe2024"
+ }"#;
+
+ fn get_example_status() -> anyhow::Result {
+ let duration = Duration::minutes(1);
+ let service_url = Url::parse(
+ "did:iota:snd:0xae6ccfdb155a69e0ef153fb5fcfd50c08a8fee36babe1f7d71dede8f4e202432#my-revocation-service",
+ )?;
+ let credential_index: u32 = 5;
+ let start_validity_timeframe = Timestamp::parse("2024-03-19T13:57:50Z")?;
+
+ Ok(RevocationTimeframeStatus::new(
+ Some(start_validity_timeframe),
+ duration,
+ service_url,
+ credential_index,
+ )?)
+ }
+
+ #[test]
+ fn revocation_timeframe_status_serialization_works() -> anyhow::Result<()> {
+ let status = get_example_status()?;
+
+ let serialized = serde_json::to_string(&status).expect("Failed to deserialize");
+ dbg!(&serialized);
+
+ Ok(())
+ }
+
+ #[test]
+ fn revocation_timeframe_status_deserialization_works() -> anyhow::Result<()> {
+ let status = get_example_status()?;
+ let deserialized =
+ serde_json::from_str::(EXAMPLE_SERIALIZED).expect("Failed to deserialize");
+
+ assert_eq!(status, deserialized);
+
+ Ok(())
+ }
+}
diff --git a/identity_credential/src/validator/jpt_credential_validation/decoded_jpt_credential.rs b/identity_credential/src/validator/jpt_credential_validation/decoded_jpt_credential.rs
new file mode 100644
index 0000000000..b574abfa13
--- /dev/null
+++ b/identity_credential/src/validator/jpt_credential_validation/decoded_jpt_credential.rs
@@ -0,0 +1,19 @@
+// Copyright 2020-2024 IOTA Stiftung, Fondazione Links
+// SPDX-License-Identifier: Apache-2.0
+
+use identity_core::common::Object;
+use jsonprooftoken::jwp::issued::JwpIssued;
+
+use crate::credential::Credential;
+
+/// Decoded [`Credential`] from a cryptographically verified JWP.
+#[non_exhaustive]
+#[derive(Debug, Clone)]
+pub struct DecodedJptCredential {
+ /// The decoded credential parsed to the [Verifiable Credentials Data model](https://www.w3.org/TR/vc-data-model/).
+ pub credential: Credential,
+ /// The custom claims parsed from the JPT.
+ pub custom_claims: Option,
+ /// The decoded and verifier Issued JWP, will be used to construct the Presented JWP
+ pub decoded_jwp: JwpIssued,
+}
diff --git a/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validation_options.rs b/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validation_options.rs
new file mode 100644
index 0000000000..2cbaafac28
--- /dev/null
+++ b/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validation_options.rs
@@ -0,0 +1,87 @@
+// Copyright 2020-2024 IOTA Stiftung, Fondazione Links
+// SPDX-License-Identifier: Apache-2.0
+
+use crate::validator::SubjectHolderRelationship;
+use identity_core::common::Timestamp;
+use identity_core::common::Url;
+use identity_document::verifiable::JwpVerificationOptions;
+use serde::Deserialize;
+use serde::Serialize;
+
+/// Options to declare validation criteria for [`Credential`](crate::credential::Credential)s.
+#[non_exhaustive]
+#[derive(Debug, Default, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct JptCredentialValidationOptions {
+ /// Declares that the credential is **not** considered valid if it expires before this
+ /// [`Timestamp`].
+ /// Uses the current datetime during validation if not set.
+ #[serde(default)]
+ pub earliest_expiry_date: Option,
+
+ /// Declares that the credential is **not** considered valid if it was issued later than this
+ /// [`Timestamp`].
+ /// Uses the current datetime during validation if not set.
+ #[serde(default)]
+ pub latest_issuance_date: Option,
+
+ /// Validation behaviour for [`credentialStatus`](https://www.w3.org/TR/vc-data-model/#status).
+ ///
+ /// Default: [`StatusCheck::Strict`](crate::validator::StatusCheck::Strict).
+ #[serde(default)]
+ pub status: crate::validator::StatusCheck,
+
+ /// Declares how credential subjects must relate to the presentation holder during validation.
+ ///
+ ///
+ pub subject_holder_relationship: Option<(Url, SubjectHolderRelationship)>,
+
+ /// Options which affect the verification of the proof on the credential.
+ #[serde(default)]
+ pub verification_options: JwpVerificationOptions,
+}
+
+impl JptCredentialValidationOptions {
+ /// Constructor that sets all options to their defaults.
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ /// Declare that the credential is **not** considered valid if it expires before this [`Timestamp`].
+ /// Uses the current datetime during validation if not set.
+ pub fn earliest_expiry_date(mut self, timestamp: Timestamp) -> Self {
+ self.earliest_expiry_date = Some(timestamp);
+ self
+ }
+
+ /// Declare that the credential is **not** considered valid if it was issued later than this [`Timestamp`].
+ /// Uses the current datetime during validation if not set.
+ pub fn latest_issuance_date(mut self, timestamp: Timestamp) -> Self {
+ self.latest_issuance_date = Some(timestamp);
+ self
+ }
+
+ /// Sets the validation behaviour for [`credentialStatus`](https://www.w3.org/TR/vc-data-model/#status).
+ pub fn status_check(mut self, status_check: crate::validator::StatusCheck) -> Self {
+ self.status = status_check;
+ self
+ }
+
+ /// Declares how credential subjects must relate to the presentation holder during validation.
+ ///
+ ///
+ pub fn subject_holder_relationship(
+ mut self,
+ holder: Url,
+ subject_holder_relationship: SubjectHolderRelationship,
+ ) -> Self {
+ self.subject_holder_relationship = Some((holder, subject_holder_relationship));
+ self
+ }
+
+ /// Set options which affect the verification of the JWP proof.
+ pub fn verification_options(mut self, options: JwpVerificationOptions) -> Self {
+ self.verification_options = options;
+ self
+ }
+}
diff --git a/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validator.rs b/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validator.rs
new file mode 100644
index 0000000000..3639d1a229
--- /dev/null
+++ b/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validator.rs
@@ -0,0 +1,225 @@
+// Copyright 2020-2024 IOTA Stiftung, Fondazione Links
+// SPDX-License-Identifier: Apache-2.0
+
+use identity_core::convert::FromJson;
+use identity_core::convert::ToJson;
+use identity_did::CoreDID;
+use identity_did::DIDUrl;
+use identity_document::document::CoreDocument;
+use identity_document::verifiable::JwpVerificationOptions;
+use jsonprooftoken::encoding::SerializationType;
+use jsonprooftoken::jpt::claims::JptClaims;
+use jsonprooftoken::jwk::key::Jwk as JwkExt;
+use jsonprooftoken::jwp::issued::JwpIssuedDecoder;
+
+use super::DecodedJptCredential;
+use crate::credential::Credential;
+use crate::credential::CredentialJwtClaims;
+use crate::credential::Jpt;
+use crate::validator::jwt_credential_validation::SignerContext;
+use crate::validator::CompoundCredentialValidationError;
+use crate::validator::FailFast;
+use crate::validator::JptCredentialValidationOptions;
+use crate::validator::JwtCredentialValidatorUtils;
+use crate::validator::JwtValidationError;
+
+/// A type for decoding and validating [`Credential`]s in JPT format.
+#[non_exhaustive]
+pub struct JptCredentialValidator;
+
+impl JptCredentialValidator {
+ /// Decodes and validates a [`Credential`] issued as a JPT (JWP Issued Form). A [`DecodedJptCredential`] is returned
+ /// upon success.
+ ///
+ /// The following properties are validated according to `options`:
+ /// - the issuer's proof on the JWP,
+ /// - the expiration date,
+ /// - the issuance date,
+ /// - the semantic structure.
+ pub fn validate(
+ credential_jpt: &Jpt,
+ issuer: &DOC,
+ options: &JptCredentialValidationOptions,
+ fail_fast: FailFast,
+ ) -> Result, CompoundCredentialValidationError>
+ where
+ T: ToOwned + serde::Serialize + serde::de::DeserializeOwned,
+ DOC: AsRef,
+ {
+ // First verify the JWP proof and decode the result into a credential token, then apply all other validations.
+ let credential_token =
+ Self::verify_proof(credential_jpt, issuer, &options.verification_options).map_err(|err| {
+ CompoundCredentialValidationError {
+ validation_errors: [err].into(),
+ }
+ })?;
+
+ let credential: &Credential = &credential_token.credential;
+
+ Self::validate_credential::(credential, options, fail_fast)?;
+
+ Ok(credential_token)
+ }
+
+ pub(crate) fn validate_credential(
+ credential: &Credential,
+ options: &JptCredentialValidationOptions,
+ fail_fast: FailFast,
+ ) -> Result<(), CompoundCredentialValidationError>
+ where
+ T: ToOwned + serde::Serialize + serde::de::DeserializeOwned,
+ {
+ // Run all single concern Credential validations in turn and fail immediately if `fail_fast` is true.
+ let expiry_date_validation = std::iter::once_with(|| {
+ JwtCredentialValidatorUtils::check_expires_on_or_after(
+ credential,
+ options.earliest_expiry_date.unwrap_or_default(),
+ )
+ });
+
+ let issuance_date_validation = std::iter::once_with(|| {
+ JwtCredentialValidatorUtils::check_issued_on_or_before(
+ credential,
+ options.latest_issuance_date.unwrap_or_default(),
+ )
+ });
+
+ let structure_validation = std::iter::once_with(|| JwtCredentialValidatorUtils::check_structure(credential));
+
+ let subject_holder_validation = std::iter::once_with(|| {
+ options
+ .subject_holder_relationship
+ .as_ref()
+ .map(|(holder, relationship)| {
+ JwtCredentialValidatorUtils::check_subject_holder_relationship(credential, holder, *relationship)
+ })
+ .unwrap_or(Ok(()))
+ });
+
+ let validation_units_iter = issuance_date_validation
+ .chain(expiry_date_validation)
+ .chain(structure_validation)
+ .chain(subject_holder_validation);
+
+ let validation_units_error_iter = validation_units_iter.filter_map(|result| result.err());
+ let validation_errors: Vec = match fail_fast {
+ FailFast::FirstError => validation_units_error_iter.take(1).collect(),
+ FailFast::AllErrors => validation_units_error_iter.collect(),
+ };
+
+ if validation_errors.is_empty() {
+ Ok(())
+ } else {
+ Err(CompoundCredentialValidationError { validation_errors })
+ }
+ }
+
+ /// Proof verification function
+ fn verify_proof(
+ credential: &Jpt,
+ issuer: &DOC,
+ options: &JwpVerificationOptions,
+ ) -> Result, JwtValidationError>
+ where
+ T: ToOwned + serde::Serialize + serde::de::DeserializeOwned,
+ DOC: AsRef,
+ {
+ let decoded = JwpIssuedDecoder::decode(credential.as_str(), SerializationType::COMPACT)
+ .map_err(JwtValidationError::JwpDecodingError)?;
+
+ // If no method_url is set, parse the `kid` to a DID Url which should be the identifier
+ // of a verification method in a trusted issuer's DID document.
+ let method_id: DIDUrl = match &options.method_id {
+ Some(method_id) => method_id.clone(),
+ None => {
+ let kid: &str = decoded
+ .get_header()
+ .kid()
+ .ok_or(JwtValidationError::MethodDataLookupError {
+ source: None,
+ message: "could not extract kid from protected header",
+ signer_ctx: SignerContext::Issuer,
+ })?;
+
+ // Convert kid to DIDUrl
+ DIDUrl::parse(kid).map_err(|err| JwtValidationError::MethodDataLookupError {
+ source: Some(err.into()),
+ message: "could not parse kid as a DID Url",
+ signer_ctx: SignerContext::Issuer,
+ })?
+ }
+ };
+
+ // check issuer
+ let issuer: &CoreDocument = issuer.as_ref();
+
+ if issuer.id() != method_id.did() {
+ return Err(JwtValidationError::DocumentMismatch(SignerContext::Issuer));
+ }
+
+ // Obtain the public key from the issuer's DID document
+ let public_key: JwkExt = issuer
+ .resolve_method(&method_id, options.method_scope)
+ .and_then(|method| method.data().public_key_jwk())
+ .and_then(|k| k.try_into().ok()) //Conversio into jsonprooftoken::Jwk type
+ .ok_or_else(|| JwtValidationError::MethodDataLookupError {
+ source: None,
+ message: "could not extract JWK from a method identified by kid",
+ signer_ctx: SignerContext::Issuer,
+ })?;
+
+ let credential_token = Self::verify_decoded_jwp(decoded, &public_key)?;
+
+ // Check that the DID component of the parsed `kid` does indeed correspond to the issuer in the credential before
+ // returning.
+ let issuer_id: CoreDID = JwtCredentialValidatorUtils::extract_issuer(&credential_token.credential)?;
+ if &issuer_id != method_id.did() {
+ return Err(JwtValidationError::IdentifierMismatch {
+ signer_ctx: SignerContext::Issuer,
+ });
+ };
+ Ok(credential_token)
+ }
+
+ /// Verify the decoded issued JWP proof using the given `public_key`.
+ fn verify_decoded_jwp(
+ decoded: JwpIssuedDecoder,
+ public_key: &JwkExt,
+ ) -> Result, JwtValidationError>
+ where
+ T: ToOwned + serde::Serialize + serde::de::DeserializeOwned,
+ {
+ // Verify Jwp proof
+ let decoded_jwp = decoded
+ .verify(public_key)
+ .map_err(JwtValidationError::JwpProofVerificationError)?;
+
+ let claims = decoded_jwp.get_claims().ok_or("Claims not present").map_err(|err| {
+ JwtValidationError::CredentialStructure(crate::Error::JptClaimsSetDeserializationError(err.into()))
+ })?;
+ let payloads = decoded_jwp.get_payloads();
+ let jpt_claims = JptClaims::from_claims_and_payloads(claims, payloads);
+ let jpt_claims_json = jpt_claims.to_json_vec().map_err(|err| {
+ JwtValidationError::CredentialStructure(crate::Error::JptClaimsSetDeserializationError(err.into()))
+ })?;
+
+ // Deserialize the raw claims
+ let credential_claims: CredentialJwtClaims<'_, T> = CredentialJwtClaims::from_json_slice(&jpt_claims_json)
+ .map_err(|err| {
+ JwtValidationError::CredentialStructure(crate::Error::JwtClaimsSetDeserializationError(err.into()))
+ })?;
+
+ let custom_claims = credential_claims.custom.clone();
+
+ // Construct the credential token containing the credential and the protected header.
+ let credential: Credential = credential_claims
+ .try_into_credential()
+ .map_err(JwtValidationError::CredentialStructure)?;
+
+ Ok(DecodedJptCredential {
+ credential,
+ custom_claims,
+ decoded_jwp,
+ })
+ }
+}
diff --git a/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validator_utils.rs b/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validator_utils.rs
new file mode 100644
index 0000000000..258df619d4
--- /dev/null
+++ b/identity_credential/src/validator/jpt_credential_validation/jpt_credential_validator_utils.rs
@@ -0,0 +1,242 @@
+// Copyright 2020-2024 IOTA Stiftung, Fondazione Links
+// SPDX-License-Identifier: Apache-2.0
+
+use crate::credential::Credential;
+use crate::revocation::RevocationDocumentExt;
+use crate::revocation::RevocationTimeframeStatus;
+use std::str::FromStr;
+
+use identity_core::common::Object;
+use identity_core::common::Timestamp;
+use identity_core::convert::FromJson;
+use identity_core::convert::ToJson;
+use identity_did::DID;
+use jsonprooftoken::encoding::SerializationType;
+use jsonprooftoken::jpt::claims::JptClaims;
+use jsonprooftoken::jwp::issued::JwpIssuedDecoder;
+
+use crate::credential::CredentialJwtClaims;
+use crate::credential::Jpt;
+use crate::validator::JwtValidationError;
+use crate::validator::SignerContext;
+
+/// Utility functions for verifying JPT credentials.
+#[derive(Debug)]
+#[non_exhaustive]
+pub struct JptCredentialValidatorUtils;
+
+type ValidationUnitResult = std::result::Result;
+
+impl JptCredentialValidatorUtils {
+ /// Utility for extracting the issuer field of a [`Credential`] as a DID.
+ ///
+ /// # Errors
+ ///
+ /// Fails if the issuer field is not a valid DID.
+ pub fn extract_issuer(credential: &Credential) -> std::result::Result
+ where
+ D: DID,
+ ::Err: std::error::Error + Send + Sync + 'static,
+ {
+ D::from_str(credential.issuer.url().as_str()).map_err(|err| JwtValidationError::SignerUrl {
+ signer_ctx: SignerContext::Issuer,
+ source: err.into(),
+ })
+ }
+
+ /// Utility for extracting the issuer field of a credential in JPT representation as DID.
+ ///
+ /// # Errors
+ ///
+ /// If the JPT decoding fails or the issuer field is not a valid DID.
+ pub fn extract_issuer_from_issued_jpt(credential: &Jpt) -> std::result::Result
+ where
+ D: DID,
+ ::Err: std::error::Error + Send + Sync + 'static,
+ {
+ let decoded = JwpIssuedDecoder::decode(credential.as_str(), SerializationType::COMPACT)
+ .map_err(JwtValidationError::JwpDecodingError)?;
+ let claims = decoded
+ .get_header()
+ .claims()
+ .ok_or("Claims not present")
+ .map_err(|err| {
+ JwtValidationError::CredentialStructure(crate::Error::JptClaimsSetDeserializationError(err.into()))
+ })?;
+ let payloads = decoded.get_payloads();
+ let jpt_claims = JptClaims::from_claims_and_payloads(claims, payloads);
+ let jpt_claims_json = jpt_claims.to_json_vec().map_err(|err| {
+ JwtValidationError::CredentialStructure(crate::Error::JptClaimsSetDeserializationError(err.into()))
+ })?;
+
+ // Deserialize the raw claims
+ let credential_claims: CredentialJwtClaims<'_, Object> = CredentialJwtClaims::from_json_slice(&jpt_claims_json)
+ .map_err(|err| {
+ JwtValidationError::CredentialStructure(crate::Error::JwtClaimsSetDeserializationError(err.into()))
+ })?;
+
+ D::from_str(credential_claims.iss.url().as_str()).map_err(|err| JwtValidationError::SignerUrl {
+ signer_ctx: SignerContext::Issuer,
+ source: err.into(),
+ })
+ }
+
+ /// Check timeframe interval in credentialStatus with `RevocationTimeframeStatus`.
+ pub fn check_timeframes_with_validity_timeframe_2024(
+ credential: &Credential,
+ validity_timeframe: Option,
+ status_check: crate::validator::StatusCheck,
+ ) -> ValidationUnitResult {
+ if status_check == crate::validator::StatusCheck::SkipAll {
+ return Ok(());
+ }
+
+ match &credential.credential_status {
+ None => Ok(()),
+ Some(status) => {
+ if status.type_ == RevocationTimeframeStatus::TYPE {
+ let status: RevocationTimeframeStatus =
+ RevocationTimeframeStatus::try_from(status).map_err(JwtValidationError::InvalidStatus)?;
+
+ Self::check_validity_timeframe(status, validity_timeframe)
+ } else {
+ if status_check == crate::validator::StatusCheck::SkipUnsupported {
+ return Ok(());
+ }
+ Err(JwtValidationError::InvalidStatus(crate::Error::InvalidStatus(format!(
+ "unsupported type '{}'",
+ status.type_
+ ))))
+ }
+ }
+ }
+ }
+
+ pub(crate) fn check_validity_timeframe(
+ status: RevocationTimeframeStatus,
+ validity_timeframe: Option,
+ ) -> ValidationUnitResult {
+ let timeframe = validity_timeframe.unwrap_or(Timestamp::now_utc());
+
+ let check = timeframe >= status.start_validity_timeframe() && timeframe <= status.end_validity_timeframe();
+
+ if !check {
+ Err(JwtValidationError::OutsideTimeframe)
+ } else {
+ Ok(())
+ }
+ }
+
+ /// Checks whether the credential status has been revoked.
+ ///
+ /// Only supports `RevocationTimeframe2024`.
+ pub fn check_revocation_with_validity_timeframe_2024<
+ DOC: AsRef + ?Sized,
+ T,
+ >(
+ credential: &Credential