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 Credentials 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 Credentials 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, + issuer: &DOC, + 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_revocation_bitmap(issuer, status) + } else { + if status_check == crate::validator::StatusCheck::SkipUnsupported { + return Ok(()); + } + Err(JwtValidationError::InvalidStatus(crate::Error::InvalidStatus(format!( + "unsupported type '{}'", + status.type_ + )))) + } + } + } + } + + /// Check the given `status` against the matching [`RevocationBitmap`] service in the issuer's DID Document. + fn check_revocation_bitmap + ?Sized>( + issuer: &DOC, + status: RevocationTimeframeStatus, + ) -> ValidationUnitResult { + let issuer_service_url: identity_did::DIDUrl = + identity_did::DIDUrl::parse(status.id().to_string()).map_err(|err| { + JwtValidationError::InvalidStatus(crate::Error::InvalidStatus(format!( + "could not convert status id to DIDUrl; {}", + err, + ))) + })?; + + // Check whether index is revoked. + let revocation_bitmap: crate::revocation::RevocationBitmap = issuer + .as_ref() + .resolve_revocation_bitmap(issuer_service_url.into()) + .map_err(|_| JwtValidationError::ServiceLookupError)?; + + if let Some(index) = status.index() { + if revocation_bitmap.is_revoked(index) { + return Err(JwtValidationError::Revoked); + } + } + Ok(()) + } + + /// Checks whether the credential status has been revoked or the timeframe interval is INVALID + /// + /// Only supports `RevocationTimeframe2024`. + pub fn check_timeframes_and_revocation_with_validity_timeframe_2024< + DOC: AsRef + ?Sized, + T, + >( + credential: &Credential, + issuer: &DOC, + 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)?; + + let revocation = std::iter::once_with(|| Self::check_revocation_bitmap(issuer, status.clone())); + + let timeframes = std::iter::once_with(|| Self::check_validity_timeframe(status.clone(), validity_timeframe)); + + let checks_iter = revocation.chain(timeframes); + + let checks_error_iter = checks_iter.filter_map(|result| result.err()); + + let mut checks_errors: Vec = checks_error_iter.take(1).collect(); + + match checks_errors.pop() { + Some(err) => Err(err), + None => Ok(()), + } + } else { + if status_check == crate::validator::StatusCheck::SkipUnsupported { + return Ok(()); + } + Err(JwtValidationError::InvalidStatus(crate::Error::InvalidStatus(format!( + "unsupported type '{}'", + status.type_ + )))) + } + } + } + } +} diff --git a/identity_credential/src/validator/jpt_credential_validation/mod.rs b/identity_credential/src/validator/jpt_credential_validation/mod.rs new file mode 100644 index 0000000000..60455ba606 --- /dev/null +++ b/identity_credential/src/validator/jpt_credential_validation/mod.rs @@ -0,0 +1,12 @@ +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +mod decoded_jpt_credential; +mod jpt_credential_validation_options; +mod jpt_credential_validator; +mod jpt_credential_validator_utils; + +pub use decoded_jpt_credential::*; +pub use jpt_credential_validation_options::*; +pub use jpt_credential_validator::*; +pub use jpt_credential_validator_utils::*; diff --git a/identity_credential/src/validator/jpt_presentation_validation/decoded_jpt_presentation.rs b/identity_credential/src/validator/jpt_presentation_validation/decoded_jpt_presentation.rs new file mode 100644 index 0000000000..fb62181057 --- /dev/null +++ b/identity_credential/src/validator/jpt_presentation_validation/decoded_jpt_presentation.rs @@ -0,0 +1,22 @@ +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +use identity_core::common::Object; +use identity_core::common::Url; +use jsonprooftoken::jwp::presented::JwpPresented; + +use crate::credential::Credential; + +/// Decoded [`Credential`] from a cryptographically verified JWP. +#[non_exhaustive] +#[derive(Debug, Clone)] +pub struct DecodedJptPresentation { + /// The decoded credential parsed to the [Verifiable Credentials Data model](https://www.w3.org/TR/vc-data-model/). + pub credential: Credential, + /// The `aud` property parsed from the JWT claims. + pub aud: Option, + /// 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: JwpPresented, +} diff --git a/identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validation_options.rs b/identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validation_options.rs new file mode 100644 index 0000000000..302b45f8c4 --- /dev/null +++ b/identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validation_options.rs @@ -0,0 +1,40 @@ +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +use identity_document::verifiable::JwpVerificationOptions; +use serde::Deserialize; +use serde::Serialize; + +/// Criteria for validating a [`Presentation`](crate::presentation::Presentation). +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +#[non_exhaustive] +#[serde(rename_all = "camelCase")] +pub struct JptPresentationValidationOptions { + /// The nonce to be placed in the Presentation Protected Header. + #[serde(default)] + pub nonce: Option, + + /// Options which affect the verification of the proof on the credential. + #[serde(default)] + pub verification_options: JwpVerificationOptions, +} + +impl JptPresentationValidationOptions { + /// Constructor that sets all options to their defaults. + pub fn new() -> Self { + Self::default() + } + + /// Declare that the presentation is **not** considered valid if it expires before this [`Timestamp`]. + /// Uses the current datetime during validation if not set. + pub fn nonce(mut self, nonce: impl Into) -> Self { + self.nonce = Some(nonce.into()); + 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_presentation_validation/jpt_presentation_validator.rs b/identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validator.rs new file mode 100644 index 0000000000..ac32e9878f --- /dev/null +++ b/identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validator.rs @@ -0,0 +1,226 @@ +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +use std::str::FromStr; + +use identity_core::common::Url; +use identity_core::convert::FromJson; +use identity_core::convert::ToJson; +use identity_did::CoreDID; +use identity_did::DIDUrl; +use identity_document::document::CoreDocument; +use jsonprooftoken::encoding::SerializationType; +use jsonprooftoken::jpt::claims::JptClaims; +use jsonprooftoken::jwk::key::Jwk as JwkExt; +use jsonprooftoken::jwp::presented::JwpPresentedDecoder; + +use crate::credential::Credential; +use crate::credential::CredentialJwtClaims; +use crate::credential::Jpt; +use crate::validator::CompoundCredentialValidationError; +use crate::validator::FailFast; +use crate::validator::JwtCredentialValidatorUtils; +use crate::validator::JwtValidationError; +use crate::validator::SignerContext; + +use super::DecodedJptPresentation; +use super::JptPresentationValidationOptions; + +/// A type for decoding and validating Presented [`Credential`]s in JPT format. +#[non_exhaustive] +pub struct JptPresentationValidator; + +impl JptPresentationValidator { + /// Decodes and validates a Presented [`Credential`] issued as a JPT (JWP Presented Form). A + /// [`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. + pub fn validate( + presentation_jpt: &Jpt, + issuer: &DOC, + options: &JptPresentationValidationOptions, + 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 presented credential token, then apply all other + // validations. + let presented_credential_token = + Self::verify_proof(presentation_jpt, issuer, options).map_err(|err| CompoundCredentialValidationError { + validation_errors: [err].into(), + })?; + + let credential: &Credential = &presented_credential_token.credential; + + Self::validate_presented_credential::(credential, fail_fast)?; + + Ok(presented_credential_token) + } + + pub(crate) fn validate_presented_credential( + credential: &Credential, + fail_fast: FailFast, + ) -> Result<(), CompoundCredentialValidationError> + where + T: ToOwned + serde::Serialize + serde::de::DeserializeOwned, + { + let structure_validation = std::iter::once_with(|| JwtCredentialValidatorUtils::check_structure(credential)); + + let validation_units_iter = structure_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( + presentation_jpt: &Jpt, + issuer: &DOC, + options: &JptPresentationValidationOptions, + ) -> Result, JwtValidationError> + where + T: ToOwned + serde::Serialize + serde::de::DeserializeOwned, + DOC: AsRef, + { + let decoded: JwpPresentedDecoder = + JwpPresentedDecoder::decode(presentation_jpt.as_str(), SerializationType::COMPACT) + .map_err(JwtValidationError::JwpDecodingError)?; + + let nonce: Option<&String> = options.nonce.as_ref(); + // Validate the nonce + if decoded.get_presentation_header().nonce() != nonce { + return Err(JwtValidationError::JwsDecodingError( + identity_verification::jose::error::Error::InvalidParam("invalid nonce value"), + )); + } + + // 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.verification_options.method_id { + Some(method_id) => method_id.clone(), + None => { + let kid: &str = decoded + .get_issuer_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.verification_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 presented JWP proof using the given `public_key`. + fn verify_decoded_jwp( + decoded: JwpPresentedDecoder, + 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 mut jpt_claims = JptClaims::from_claims_and_payloads(claims, payloads); + // if not set the deserializatioon will throw an error since even the iat is not set, so we set this to 0 + jpt_claims.nbf.map_or_else( + || { + jpt_claims.set_nbf(0); + }, + |_| (), + ); + + 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)?; + + let aud: Option = decoded_jwp.get_presentation_protected_header().aud().and_then(|aud| { + Url::from_str(aud) + .map_err(|_| { + JwtValidationError::JwsDecodingError(identity_verification::jose::error::Error::InvalidParam( + "invalid audience value", + )) + }) + .ok() + }); + + Ok(DecodedJptPresentation { + credential, + aud, + custom_claims, + decoded_jwp, + }) + } +} diff --git a/identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validator_utils.rs b/identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validator_utils.rs new file mode 100644 index 0000000000..3bdf17a00e --- /dev/null +++ b/identity_credential/src/validator/jpt_presentation_validation/jpt_presentation_validator_utils.rs @@ -0,0 +1,99 @@ +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +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::presented::JwpPresentedDecoder; + +use crate::credential::Credential; +use crate::credential::CredentialJwtClaims; +use crate::credential::Jpt; +use crate::revocation::RevocationTimeframeStatus; +use crate::revocation::VerifierRevocationTimeframeStatus; +use crate::validator::JptCredentialValidatorUtils; +use crate::validator::JwtValidationError; +use crate::validator::SignerContext; + +/// Utility functions for verifying JPT credentials. +#[derive(Debug)] +#[non_exhaustive] +pub struct JptPresentationValidatorUtils; + +type ValidationUnitResult = std::result::Result; + +impl JptPresentationValidatorUtils { + /// 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_presented_jpt(presentation: &Jpt) -> std::result::Result + where + D: DID, + ::Err: std::error::Error + Send + Sync + 'static, + { + let decoded = JwpPresentedDecoder::decode(presentation.as_str(), SerializationType::COMPACT) + .map_err(JwtValidationError::JwpDecodingError)?; + let claims = decoded + .get_issuer_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: VerifierRevocationTimeframeStatus = + VerifierRevocationTimeframeStatus::try_from(status.clone()).map_err(JwtValidationError::InvalidStatus)?; + + JptCredentialValidatorUtils::check_validity_timeframe(status.0, validity_timeframe) + } else { + if status_check == crate::validator::StatusCheck::SkipUnsupported { + return Ok(()); + } + Err(JwtValidationError::InvalidStatus(crate::Error::InvalidStatus(format!( + "unsupported type '{}'", + status.type_ + )))) + } + } + } + } +} diff --git a/identity_credential/src/validator/jpt_presentation_validation/mod.rs b/identity_credential/src/validator/jpt_presentation_validation/mod.rs new file mode 100644 index 0000000000..1cab953dc5 --- /dev/null +++ b/identity_credential/src/validator/jpt_presentation_validation/mod.rs @@ -0,0 +1,12 @@ +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +mod decoded_jpt_presentation; +mod jpt_presentation_validation_options; +mod jpt_presentation_validator; +mod jpt_presentation_validator_utils; + +pub use decoded_jpt_presentation::*; +pub use jpt_presentation_validation_options::*; +pub use jpt_presentation_validator::*; +pub use jpt_presentation_validator_utils::*; diff --git a/identity_credential/src/validator/jwt_credential_validation/error.rs b/identity_credential/src/validator/jwt_credential_validation/error.rs index 073ffe303c..a531f088d7 100644 --- a/identity_credential/src/validator/jwt_credential_validation/error.rs +++ b/identity_credential/src/validator/jwt_credential_validation/error.rs @@ -104,6 +104,18 @@ pub enum JwtValidationError { /// Indicates that the credential has been suspended. #[error("credential has been suspended")] Suspended, + /// Indicates that the credential's timeframe interval is not valid + #[cfg(feature = "jpt-bbs-plus")] + #[error("timeframe interval not valid")] + OutsideTimeframe, + /// Indicates that the JWP representation of an issued credential or presentation could not be decoded. + #[cfg(feature = "jpt-bbs-plus")] + #[error("could not decode jwp")] + JwpDecodingError(#[source] jsonprooftoken::errors::CustomError), + /// Indicates that the verification of the JWP has failed + #[cfg(feature = "jpt-bbs-plus")] + #[error("could not verify jwp")] + JwpProofVerificationError(#[source] jsonprooftoken::errors::CustomError), } /// Specifies whether an error is related to a credential issuer or the presentation holder. diff --git a/identity_credential/src/validator/jwt_credential_validation/jwt_credential_validator_utils.rs b/identity_credential/src/validator/jwt_credential_validation/jwt_credential_validator_utils.rs index e7a43bcdab..d454122c15 100644 --- a/identity_credential/src/validator/jwt_credential_validation/jwt_credential_validator_utils.rs +++ b/identity_credential/src/validator/jwt_credential_validation/jwt_credential_validator_utils.rs @@ -171,7 +171,7 @@ impl JwtCredentialValidatorUtils { /// Check the given `status` against the matching [`RevocationBitmap`] service in the /// issuer's DID Document. #[cfg(feature = "revocation-bitmap")] - fn check_revocation_bitmap_status + ?Sized>( + pub fn check_revocation_bitmap_status + ?Sized>( issuer: &DOC, status: crate::credential::RevocationBitmapStatus, ) -> ValidationUnitResult { diff --git a/identity_credential/src/validator/mod.rs b/identity_credential/src/validator/mod.rs index 37611334c3..2266618ddd 100644 --- a/identity_credential/src/validator/mod.rs +++ b/identity_credential/src/validator/mod.rs @@ -3,6 +3,10 @@ //! Verifiable Credential and Presentation validators. +#[cfg(feature = "jpt-bbs-plus")] +pub use self::jpt_credential_validation::*; +#[cfg(feature = "jpt-bbs-plus")] +pub use self::jpt_presentation_validation::*; pub use self::jwt_credential_validation::*; pub use self::jwt_presentation_validation::*; pub use self::options::FailFast; @@ -11,6 +15,10 @@ pub use self::options::SubjectHolderRelationship; #[cfg(feature = "sd-jwt")] pub use self::sd_jwt::*; +#[cfg(feature = "jpt-bbs-plus")] +mod jpt_credential_validation; +#[cfg(feature = "jpt-bbs-plus")] +mod jpt_presentation_validation; mod jwt_credential_validation; mod jwt_presentation_validation; mod options; diff --git a/identity_credential/src/validator/test_utils.rs b/identity_credential/src/validator/test_utils.rs index 9febb41a1f..22a18a7605 100644 --- a/identity_credential/src/validator/test_utils.rs +++ b/identity_credential/src/validator/test_utils.rs @@ -19,7 +19,7 @@ pub(crate) fn encode_public_ed25519_jwk(public_key: &PublicKey) -> Jwk { let mut params = JwkParamsOkp::new(); params.x = x; params.d = None; - params.crv = EdCurve::Ed25519.name().to_owned(); + params.crv = EdCurve::Ed25519.name().to_string(); let mut jwk = Jwk::from_params(params); jwk.set_alg(JwsAlgorithm::EdDSA.name()); jwk diff --git a/identity_did/Cargo.toml b/identity_did/Cargo.toml index 9492997598..5fdca4b370 100644 --- a/identity_did/Cargo.toml +++ b/identity_did/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "identity_did" -version = "1.1.0-alpha.1" +version = "1.3.0" authors.workspace = true edition = "2021" homepage.workspace = true @@ -11,9 +11,9 @@ repository.workspace = true description = "Agnostic implementation of the Decentralized Identifiers (DID) standard." [dependencies] -did_url = { version = "0.1", default-features = false, features = ["std", "serde"] } +did_url_parser = { version = "0.2.0", features = ["std", "serde"] } form_urlencoded = { version = "1.2.0", default-features = false, features = ["alloc"] } -identity_core = { version = "=1.1.0-alpha.1", path = "../identity_core" } +identity_core = { version = "=1.3.0", path = "../identity_core" } serde.workspace = true strum.workspace = true thiserror.workspace = true @@ -27,3 +27,6 @@ serde_json.workspace = true # 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_did/src/did.rs b/identity_did/src/did.rs index eb07f61a08..de9cf61183 100644 --- a/identity_did/src/did.rs +++ b/identity_did/src/did.rs @@ -8,7 +8,7 @@ use core::fmt::Formatter; use core::str::FromStr; use std::hash::Hash; -use did_url::DID as BaseDIDUrl; +use did_url_parser::DID as BaseDIDUrl; use identity_core::common::KeyComparable; @@ -111,14 +111,7 @@ impl CoreDID { /// /// Returns `Err` if the input is not a valid [`DID`]. pub fn parse(input: impl AsRef) -> Result { - let base_did_url: BaseDIDUrl = BaseDIDUrl::parse(input).map_err(Error::from)?; - Self::try_from_base_did(base_did_url) - } - - /// Try convert a [`BaseDIDUrl`] into a [`CoreDID`]. - fn try_from_base_did(base_did_url: BaseDIDUrl) -> Result { - Self::check_validity(&base_did_url)?; - Ok(Self(base_did_url)) + BaseDIDUrl::parse(input).map(Self).map_err(Error::from) } /// Set the method name of the [`DID`]. @@ -145,9 +138,23 @@ impl CoreDID { /// Validates whether a string is a valid [`DID`] method-id. pub fn valid_method_id(value: &str) -> Result<(), Error> { - if !value.chars().all(is_char_method_id) { - return Err(Error::InvalidMethodId); + // if !value.chars().all(is_char_method_id) { + // return Err(Error::InvalidMethodId); + // } + let mut chars = value.chars(); + while let Some(c) = chars.next() { + match c { + '%' => { + let digits = chars.clone().take(2).collect::(); + u8::from_str_radix(&digits, 16).map_err(|_| Error::InvalidMethodId)?; + chars.next(); + chars.next(); + } + c if is_char_method_id(c) => (), + _ => return Err(Error::InvalidMethodId), + } } + Ok(()) } @@ -185,7 +192,7 @@ impl TryFrom for CoreDID { type Error = Error; fn try_from(base_did_url: BaseDIDUrl) -> Result { - Self::try_from_base_did(base_did_url) + Ok(Self(base_did_url)) } } diff --git a/identity_did/src/did_url.rs b/identity_did/src/did_url.rs index 60c7d6c84e..0e8eebcace 100644 --- a/identity_did/src/did_url.rs +++ b/identity_did/src/did_url.rs @@ -10,7 +10,7 @@ use std::cmp::Ordering; use std::hash::Hash; use std::hash::Hasher; -use did_url::DID as BaseDIDUrl; +use did_url_parser::DID as BaseDIDUrl; use identity_core::common::KeyComparable; use identity_core::common::Url; diff --git a/identity_did/src/error.rs b/identity_did/src/error.rs index b2c1ab1469..e9bf4f51b4 100644 --- a/identity_did/src/error.rs +++ b/identity_did/src/error.rs @@ -23,15 +23,15 @@ pub enum Error { Other(&'static str), } -impl From for Error { - fn from(error: did_url::Error) -> Self { +impl From for Error { + fn from(error: did_url_parser::Error) -> Self { match error { - did_url::Error::InvalidFragment => Self::InvalidFragment, - did_url::Error::InvalidMethodId => Self::InvalidMethodId, - did_url::Error::InvalidMethodName => Self::InvalidMethodName, - did_url::Error::InvalidPath => Self::InvalidPath, - did_url::Error::InvalidQuery => Self::InvalidQuery, - did_url::Error::InvalidScheme => Self::InvalidScheme, + did_url_parser::Error::InvalidFragment => Self::InvalidFragment, + did_url_parser::Error::InvalidMethodId => Self::InvalidMethodId, + did_url_parser::Error::InvalidMethodName => Self::InvalidMethodName, + did_url_parser::Error::InvalidPath => Self::InvalidPath, + did_url_parser::Error::InvalidQuery => Self::InvalidQuery, + did_url_parser::Error::InvalidScheme => Self::InvalidScheme, error => Self::Other(error.as_str()), } } diff --git a/identity_did/src/lib.rs b/identity_did/src/lib.rs index f6fb2c368c..9289419211 100644 --- a/identity_did/src/lib.rs +++ b/identity_did/src/lib.rs @@ -23,7 +23,7 @@ mod error; pub use crate::did_url::DIDUrl; pub use crate::did_url::RelativeDIDUrl; -pub use ::did_url::DID as BaseDIDUrl; +pub use ::did_url_parser::DID as BaseDIDUrl; pub use did::CoreDID; pub use did::DID; pub use error::Error; diff --git a/identity_document/Cargo.toml b/identity_document/Cargo.toml index e44b8ce02a..091e1a0ee4 100644 --- a/identity_document/Cargo.toml +++ b/identity_document/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "identity_document" -version = "1.1.0-alpha.1" +version = "1.3.0" authors.workspace = true edition.workspace = true homepage.workspace = true @@ -12,10 +12,10 @@ rust-version.workspace = true description = "Method-agnostic implementation of the Decentralized Identifiers (DID) standard." [dependencies] -did_url = { version = "0.1", default-features = false, features = ["std", "serde"] } -identity_core = { version = "=1.1.0-alpha.1", path = "../identity_core" } -identity_did = { version = "=1.1.0-alpha.1", path = "../identity_did" } -identity_verification = { version = "=1.1.0-alpha.1", path = "../identity_verification", default-features = false } +did_url_parser = { version = "0.2.0", features = ["std", "serde"] } +identity_core = { version = "=1.3.0", path = "../identity_core" } +identity_did = { version = "=1.3.0", path = "../identity_did" } +identity_verification = { version = "=1.3.0", path = "../identity_verification", default-features = false } indexmap = { version = "2.0", default-features = false, features = ["std", "serde"] } serde.workspace = true strum.workspace = true @@ -28,3 +28,6 @@ serde_json.workspace = true [[bench]] name = "deserialize_document" harness = false + +[lints] +workspace = true diff --git a/identity_document/src/document/core_document.rs b/identity_document/src/document/core_document.rs index 87fddd0fed..1b226f9585 100644 --- a/identity_document/src/document/core_document.rs +++ b/identity_document/src/document/core_document.rs @@ -770,7 +770,7 @@ impl CoreDocument { } /// Returns the first [`Service`] with an `id` property matching the provided `service_query`, if present. - // NOTE: This method demonstrates unexpected behaviour in the edge cases where the document contains + // NOTE: This method demonstrates unexpected behavior in the edge cases where the document contains // services whose ids are of the form #. pub fn resolve_service<'query, 'me, Q>(&'me self, service_query: Q) -> Option<&Service> where diff --git a/identity_document/src/verifiable/jwp_verification_options.rs b/identity_document/src/verifiable/jwp_verification_options.rs new file mode 100644 index 0000000000..65667968ea --- /dev/null +++ b/identity_document/src/verifiable/jwp_verification_options.rs @@ -0,0 +1,36 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_did::DIDUrl; +use identity_verification::MethodScope; + +/// Holds additional options for verifying a JWP +#[non_exhaustive] +#[derive(Default, Debug, serde::Serialize, serde::Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct JwpVerificationOptions { + /// Verify the signing verification method relation matches this. + pub method_scope: Option, + /// 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. + pub method_id: Option, +} + +impl JwpVerificationOptions { + /// Creates a new [`JwpVerificationOptions`]. + pub fn new() -> Self { + Self::default() + } + + /// Set the scope of the verification methods that may be used to verify the given JWP. + pub fn method_scope(mut self, value: MethodScope) -> Self { + self.method_scope = Some(value); + self + } + + /// The DID URl of the method, whose JWK should be used to verify the JWP. + pub fn method_id(mut self, value: DIDUrl) -> Self { + self.method_id = Some(value); + self + } +} diff --git a/identity_document/src/verifiable/mod.rs b/identity_document/src/verifiable/mod.rs index da91055ca1..6f0386d3fb 100644 --- a/identity_document/src/verifiable/mod.rs +++ b/identity_document/src/verifiable/mod.rs @@ -3,6 +3,8 @@ //! Additional functionality for DID assisted digital signatures. +pub use self::jwp_verification_options::JwpVerificationOptions; pub use self::jws_verification_options::JwsVerificationOptions; +mod jwp_verification_options; mod jws_verification_options; diff --git a/identity_ecdsa_verifier/Cargo.toml b/identity_ecdsa_verifier/Cargo.toml new file mode 100644 index 0000000000..13ec1edb2a --- /dev/null +++ b/identity_ecdsa_verifier/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "identity_ecdsa_verifier" +version = "1.3.0" +authors = ["IOTA Stiftung", "Filancore GmbH"] +edition.workspace = true +homepage.workspace = true +keywords = ["iota", "identity", "jose", "jwk", "jws"] +license.workspace = true +readme = "./README.md" +repository.workspace = true +rust-version.workspace = true +description = "JWS ECDSA signature verification for IOTA Identity" + +[lints] +workspace = true + +[dependencies] +identity_verification = { version = "=1.3.0", path = "../identity_verification", default-features = false } +k256 = { version = "0.13.3", default-features = false, features = ["std", "ecdsa", "ecdsa-core"], optional = true } +p256 = { version = "0.13.2", default-features = false, features = ["std", "ecdsa", "ecdsa-core"], optional = true } +signature = { version = "2", default-features = false } + +[dev-dependencies] +josekit = "0.8.6" +serde_json.workspace = true + +[features] +default = ["es256", "es256k"] +# Enables the EcDSAJwsVerifier to verify JWS with alg = ES256. +es256 = ["dep:p256"] +# Enables the EcDSAJwsVerifier to verify JWS with alg = ES256K. +es256k = ["dep:k256"] diff --git a/identity_ecdsa_verifier/README.md b/identity_ecdsa_verifier/README.md new file mode 100644 index 0000000000..4ccb0f36b9 --- /dev/null +++ b/identity_ecdsa_verifier/README.md @@ -0,0 +1,3 @@ +# ECDSA Verifier + +This crate implements a `JwsVerifier` capable of verifying EcDSA signatures with algorithms `ES256` and `ES256K`. diff --git a/identity_ecdsa_verifier/src/ecdsa_jws_verifier.rs b/identity_ecdsa_verifier/src/ecdsa_jws_verifier.rs new file mode 100644 index 0000000000..6371b40b78 --- /dev/null +++ b/identity_ecdsa_verifier/src/ecdsa_jws_verifier.rs @@ -0,0 +1,34 @@ +// Copyright 2020-2024 IOTA Stiftung, Filancore GmbH +// SPDX-License-Identifier: Apache-2.0 + +use identity_verification::jws::JwsAlgorithm; +use identity_verification::jws::JwsVerifier; +use identity_verification::jws::SignatureVerificationErrorKind; + +/// An implementor of [`JwsVerifier`](identity_verification::jws::JwsVerifier) +/// that can handle a selection of EcDSA algorithms. +/// +/// The following algorithms are supported, if the respective feature on the +/// crate is activated: +/// +/// - [`JwsAlgorithm::ES256`](identity_verification::jws::JwsAlgorithm::ES256). +/// - [`JwsAlgorithm::ES256K`](identity_verification::jws::JwsAlgorithm::ES256K). +#[derive(Debug, Default)] +#[non_exhaustive] +pub struct EcDSAJwsVerifier {} + +impl JwsVerifier for EcDSAJwsVerifier { + fn verify( + &self, + input: identity_verification::jws::VerificationInput, + public_key: &identity_verification::jwk::Jwk, + ) -> Result<(), identity_verification::jws::SignatureVerificationError> { + match input.alg { + #[cfg(feature = "es256")] + JwsAlgorithm::ES256 => crate::Secp256R1Verifier::verify(&input, public_key), + #[cfg(feature = "es256k")] + JwsAlgorithm::ES256K => crate::Secp256K1Verifier::verify(&input, public_key), + _ => Err(SignatureVerificationErrorKind::UnsupportedAlg.into()), + } + } +} diff --git a/identity_ecdsa_verifier/src/lib.rs b/identity_ecdsa_verifier/src/lib.rs new file mode 100644 index 0000000000..6136a3eae1 --- /dev/null +++ b/identity_ecdsa_verifier/src/lib.rs @@ -0,0 +1,29 @@ +// Copyright 2020-2024 IOTA Stiftung, Filancore GmbH +// SPDX-License-Identifier: Apache-2.0 + +#![doc = include_str!("./../README.md")] +#![warn( + rust_2018_idioms, + unreachable_pub, + missing_docs, + rustdoc::missing_crate_level_docs, + rustdoc::broken_intra_doc_links, + rustdoc::private_intra_doc_links, + rustdoc::private_doc_tests, + clippy::missing_safety_doc +)] + +mod ecdsa_jws_verifier; +#[cfg(feature = "es256k")] +mod secp256k1; +#[cfg(feature = "es256")] +mod secp256r1; + +pub use ecdsa_jws_verifier::*; +#[cfg(feature = "es256k")] +pub use secp256k1::*; +#[cfg(feature = "es256")] +pub use secp256r1::*; + +#[cfg(test)] +mod tests; diff --git a/identity_ecdsa_verifier/src/secp256k1.rs b/identity_ecdsa_verifier/src/secp256k1.rs new file mode 100644 index 0000000000..9c77412cc8 --- /dev/null +++ b/identity_ecdsa_verifier/src/secp256k1.rs @@ -0,0 +1,93 @@ +// Copyright 2020-2024 IOTA Stiftung, Filancore GmbH +// SPDX-License-Identifier: Apache-2.0 + +use std::ops::Deref; + +use identity_verification::jwk::JwkParamsEc; +use identity_verification::jws::SignatureVerificationError; +use identity_verification::jws::SignatureVerificationErrorKind; +use identity_verification::jwu::{self}; +use k256::ecdsa::Signature; +use k256::ecdsa::VerifyingKey; +use k256::elliptic_curve::sec1::FromEncodedPoint; +use k256::elliptic_curve::subtle::CtOption; +use k256::EncodedPoint; +use k256::PublicKey; + +/// A verifier that can handle the +/// [`JwsAlgorithm::ES256K`](identity_verification::jws::JwsAlgorithm::ES256K) +/// algorithm. +#[derive(Debug, Default)] +#[non_exhaustive] +pub struct Secp256K1Verifier {} + +impl Secp256K1Verifier { + /// Verify a JWS signature secured with the + /// [`JwsAlgorithm::ES256K`](identity_verification::jws::JwsAlgorithm::ES256K) + /// algorithm. + /// + /// This function is useful when one is building a + /// [`JwsVerifier`](identity_verification::jws::JwsVerifier) that + /// handles the + /// [`JwsAlgorithm::ES256K`](identity_verification::jws::JwsAlgorithm::ES256K) + /// in the same manner as the [`Secp256K1Verifier`] hence extending its + /// capabilities. + /// + /// # Warning + /// + /// This function does not check whether `alg = ES256K` in the protected + /// header. Callers are expected to assert this prior to calling the + /// function. + pub fn verify( + input: &identity_verification::jws::VerificationInput, + public_key: &identity_verification::jwk::Jwk, + ) -> Result<(), SignatureVerificationError> { + // Obtain a K256 public key. + let params: &JwkParamsEc = public_key + .try_ec_params() + .map_err(|_| SignatureVerificationErrorKind::UnsupportedKeyType)?; + + // Concatenate x and y coordinates as required by + // EncodedPoint::from_untagged_bytes. + let public_key_bytes = jwu::decode_b64(¶ms.x) + .map_err(|err| { + SignatureVerificationError::new(SignatureVerificationErrorKind::KeyDecodingFailure).with_source(err) + })? + .into_iter() + .chain(jwu::decode_b64(¶ms.y).map_err(|err| { + SignatureVerificationError::new(SignatureVerificationErrorKind::KeyDecodingFailure).with_source(err) + })?) + .collect(); + + // The JWK contains the uncompressed x and y coordinates, so we can create the + // encoded point directly without prefixing an SEC1 tag. + let encoded_point: EncodedPoint = EncodedPoint::from_untagged_bytes(&public_key_bytes); + let public_key: PublicKey = { + let opt_public_key: CtOption = PublicKey::from_encoded_point(&encoded_point); + if opt_public_key.is_none().into() { + return Err(SignatureVerificationError::new( + SignatureVerificationErrorKind::KeyDecodingFailure, + )); + } else { + opt_public_key.unwrap() + } + }; + + let verifying_key: VerifyingKey = VerifyingKey::from(public_key); + + let mut signature: Signature = Signature::try_from(input.decoded_signature.deref()).map_err(|err| { + SignatureVerificationError::new(SignatureVerificationErrorKind::InvalidSignature).with_source(err) + })?; + + if let Some(normalized) = signature.normalize_s() { + signature = normalized; + } + + match signature::Verifier::verify(&verifying_key, &input.signing_input, &signature) { + Ok(()) => Ok(()), + Err(err) => { + Err(SignatureVerificationError::new(SignatureVerificationErrorKind::InvalidSignature).with_source(err)) + } + } + } +} diff --git a/identity_ecdsa_verifier/src/secp256r1.rs b/identity_ecdsa_verifier/src/secp256r1.rs new file mode 100644 index 0000000000..09201570d0 --- /dev/null +++ b/identity_ecdsa_verifier/src/secp256r1.rs @@ -0,0 +1,89 @@ +// Copyright 2020-2024 IOTA Stiftung, Filancore GmbH +// SPDX-License-Identifier: Apache-2.0 + +use std::ops::Deref; + +use identity_verification::jwk::JwkParamsEc; +use identity_verification::jws::SignatureVerificationError; +use identity_verification::jws::SignatureVerificationErrorKind; +use identity_verification::jwu::{self}; +use p256::ecdsa::Signature; +use p256::ecdsa::VerifyingKey; +use p256::elliptic_curve::sec1::FromEncodedPoint; +use p256::elliptic_curve::subtle::CtOption; +use p256::EncodedPoint; +use p256::PublicKey; + +/// A verifier that can handle the +/// [`JwsAlgorithm::ES256`](identity_verification::jws::JwsAlgorithm::ES256) +/// algorithm. +#[derive(Debug, Default)] +#[non_exhaustive] +pub struct Secp256R1Verifier {} + +impl Secp256R1Verifier { + /// Verify a JWS signature secured with the + /// [`JwsAlgorithm::ES256`](identity_verification::jws::JwsAlgorithm::ES256) + /// algorithm. + /// + /// This function is useful when one is building a + /// [`JwsVerifier`](identity_verification::jws::JwsVerifier) that + /// handles the + /// [`JwsAlgorithm::ES256`](identity_verification::jws::JwsAlgorithm::ES256) + /// in the same manner as the [`Secp256R1Verifier`] hence extending its + /// capabilities. + /// + /// # Warning + /// + /// This function does not check whether `alg = ES256` in the protected + /// header. Callers are expected to assert this prior to calling the + /// function. + pub fn verify( + input: &identity_verification::jws::VerificationInput, + public_key: &identity_verification::jwk::Jwk, + ) -> Result<(), SignatureVerificationError> { + // Obtain a P256 public key. + let params: &JwkParamsEc = public_key + .try_ec_params() + .map_err(|_| SignatureVerificationErrorKind::UnsupportedKeyType)?; + + // Concatenate x and y coordinates as required by + // EncodedPoint::from_untagged_bytes. + let public_key_bytes = jwu::decode_b64(¶ms.x) + .map_err(|err| { + SignatureVerificationError::new(SignatureVerificationErrorKind::KeyDecodingFailure).with_source(err) + })? + .into_iter() + .chain(jwu::decode_b64(¶ms.y).map_err(|err| { + SignatureVerificationError::new(SignatureVerificationErrorKind::KeyDecodingFailure).with_source(err) + })?) + .collect(); + + // The JWK contains the uncompressed x and y coordinates, so we can create the + // encoded point directly without prefixing an SEC1 tag. + let encoded_point: EncodedPoint = EncodedPoint::from_untagged_bytes(&public_key_bytes); + let public_key: PublicKey = { + let opt_public_key: CtOption = PublicKey::from_encoded_point(&encoded_point); + if opt_public_key.is_none().into() { + return Err(SignatureVerificationError::new( + SignatureVerificationErrorKind::KeyDecodingFailure, + )); + } else { + opt_public_key.unwrap() + } + }; + + let verifying_key: VerifyingKey = VerifyingKey::from(public_key); + + let signature: Signature = Signature::try_from(input.decoded_signature.deref()).map_err(|err| { + SignatureVerificationError::new(SignatureVerificationErrorKind::InvalidSignature).with_source(err) + })?; + + match signature::Verifier::verify(&verifying_key, &input.signing_input, &signature) { + Ok(()) => Ok(()), + Err(err) => { + Err(SignatureVerificationError::new(SignatureVerificationErrorKind::InvalidSignature).with_source(err)) + } + } + } +} diff --git a/identity_ecdsa_verifier/src/tests/mod.rs b/identity_ecdsa_verifier/src/tests/mod.rs new file mode 100644 index 0000000000..63e508fa33 --- /dev/null +++ b/identity_ecdsa_verifier/src/tests/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2020-2024 IOTA Stiftung, Filancore GmbH +// SPDX-License-Identifier: Apache-2.0 + +mod secp256; +mod secp256k; diff --git a/identity_ecdsa_verifier/src/tests/secp256.rs b/identity_ecdsa_verifier/src/tests/secp256.rs new file mode 100644 index 0000000000..c6700a85e4 --- /dev/null +++ b/identity_ecdsa_verifier/src/tests/secp256.rs @@ -0,0 +1,77 @@ +// Copyright 2020-2024 IOTA Stiftung, Filancore GmbH +// SPDX-License-Identifier: Apache-2.0 + +mod es256 { + use identity_verification::jwk::EcCurve; + use identity_verification::jwk::Jwk; + use identity_verification::jwk::JwkParamsEc; + use identity_verification::jwu; + use p256::ecdsa::Signature; + use p256::ecdsa::SigningKey; + use p256::SecretKey; + + pub(crate) fn expand_p256_jwk(jwk: &Jwk) -> SecretKey { + let params: &JwkParamsEc = jwk.try_ec_params().unwrap(); + + if params.try_ec_curve().unwrap() != EcCurve::P256 { + panic!("expected a P256 curve"); + } + + let sk_bytes = params.d.as_ref().map(jwu::decode_b64).unwrap().unwrap(); + SecretKey::from_slice(&sk_bytes).unwrap() + } + + pub(crate) fn sign(message: &[u8], private_key: &Jwk) -> impl AsRef<[u8]> { + let sk: SecretKey = expand_p256_jwk(private_key); + let signing_key: SigningKey = SigningKey::from(sk); + let signature: Signature = signature::Signer::sign(&signing_key, message); + signature.to_bytes() + } +} + +use identity_verification::jwk::Jwk; +use identity_verification::jws; +use identity_verification::jws::JwsHeader; + +use crate::EcDSAJwsVerifier; + +#[test] +fn test_es256_rfc7515() { + // Test Vector taken from https://datatracker.ietf.org/doc/html/rfc7515#appendix-A.3. + let tv_header: &str = r#"{"alg":"ES256"}"#; + let tv_claims: &[u8] = &[ + 123, 34, 105, 115, 115, 34, 58, 34, 106, 111, 101, 34, 44, 13, 10, 32, 34, 101, 120, 112, 34, 58, 49, 51, 48, 48, + 56, 49, 57, 51, 56, 48, 44, 13, 10, 32, 34, 104, 116, 116, 112, 58, 47, 47, 101, 120, 97, 109, 112, 108, 101, 46, + 99, 111, 109, 47, 105, 115, 95, 114, 111, 111, 116, 34, 58, 116, 114, 117, 101, 125, + ]; + let tv_encoded: &[u8] = b"eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.e4ZrhZdbFQ7630Tq51E6RQiJaae9bFNGJszIhtusEwzvO21rzH76Wer6yRn2Zb34VjIm3cVRl0iQctbf4uBY3w"; + let tv_private_key: &str = r#" + { + "kty": "EC", + "crv": "P-256", + "x": "f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU", + "y": "x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0", + "d": "jpsQnnGQmL-YBIffH1136cspYG6-0iY7X1fCE9-E9LI" + } + "#; + + let header: JwsHeader = serde_json::from_str(tv_header).unwrap(); + let jwk: Jwk = serde_json::from_str(tv_private_key).unwrap(); + let encoder: jws::CompactJwsEncoder<'_> = jws::CompactJwsEncoder::new(tv_claims, &header).unwrap(); + let signing_input: &[u8] = encoder.signing_input(); + let encoded: String = { + let signature = es256::sign(signing_input, &jwk); + encoder.into_jws(signature.as_ref()) + }; + assert_eq!(encoded.as_bytes(), tv_encoded); + + let jws_verifier = EcDSAJwsVerifier::default(); + let decoder = jws::Decoder::new(); + let token = decoder + .decode_compact_serialization(tv_encoded, None) + .and_then(|decoded| decoded.verify(&jws_verifier, &jwk)) + .unwrap(); + + assert_eq!(token.protected, header); + assert_eq!(token.claims, tv_claims); +} diff --git a/identity_ecdsa_verifier/src/tests/secp256k.rs b/identity_ecdsa_verifier/src/tests/secp256k.rs new file mode 100644 index 0000000000..49f234a3c7 --- /dev/null +++ b/identity_ecdsa_verifier/src/tests/secp256k.rs @@ -0,0 +1,112 @@ +// Copyright 2020-2024 IOTA Stiftung, Filancore GmbH +// SPDX-License-Identifier: Apache-2.0 + +mod es256k1 { + use identity_verification::jwk::EcCurve; + use identity_verification::jwk::Jwk; + use identity_verification::jwk::JwkParamsEc; + use identity_verification::jwu; + use k256::ecdsa::Signature; + use k256::ecdsa::SigningKey; + use k256::SecretKey; + + pub(crate) fn expand_k256_jwk(jwk: &Jwk) -> SecretKey { + let params: &JwkParamsEc = jwk.try_ec_params().unwrap(); + + if params.try_ec_curve().unwrap() != EcCurve::Secp256K1 { + panic!("expected a Secp256K1 curve"); + } + + let sk_bytes = params.d.as_ref().map(jwu::decode_b64).unwrap().unwrap(); + SecretKey::from_slice(&sk_bytes).unwrap() + } + + pub(crate) fn sign(message: &[u8], private_key: &Jwk) -> impl AsRef<[u8]> { + let sk: SecretKey = expand_k256_jwk(private_key); + let signing_key: SigningKey = SigningKey::from(sk); + let signature: Signature = signature::Signer::sign(&signing_key, message); + signature.to_bytes() + } +} + +use identity_verification::jwk::Jwk; +use identity_verification::jws; +use identity_verification::jws::JwsHeader; + +use crate::EcDSAJwsVerifier; + +#[test] +fn test_es256k_verifier() { + let tv_header: &str = r#"{ + "typ": "JWT", + "alg":"ES256K" + }"#; + let tv_private_key: &str = r#" + { + "kty":"EC", + "crv":"secp256k1", + "d":"y0zUV7bLeUG_kDOvACFHnSmtH7j8MSJek25R2wJbWWg", + "x":"BBobbZkiC8E4C4EYekPNJkcXFCsMNHhh0AV2USy_xSs", + "y":"VQcPHjIQClX0b5TLluFl6jpIf9U-norWC0oEvIQRNyU" + }"#; + let tv_claims: &[u8] = br#"{"key":"value"}"#; + + let header: JwsHeader = serde_json::from_str(tv_header).unwrap(); + let jwk: Jwk = serde_json::from_str(tv_private_key).unwrap(); + let encoder: jws::CompactJwsEncoder<'_> = jws::CompactJwsEncoder::new(tv_claims, &header).unwrap(); + let signing_input: &[u8] = encoder.signing_input(); + let encoded: String = { + let signature = es256k1::sign(signing_input, &jwk); + encoder.into_jws(signature.as_ref()) + }; + + let jws_verifier = EcDSAJwsVerifier::default(); + let jwk: Jwk = serde_json::from_str(tv_private_key).unwrap(); + let decoder = jws::Decoder::new(); + assert!(decoder + .decode_compact_serialization(encoded.as_bytes(), None) + .and_then(|decoded| decoded.verify(&jws_verifier, &jwk)) + .is_ok()); +} + +/// In the absence of official test vectors for secp256k1, +/// this ensures we can verify JWTs created by other libraries. +mod test_es256k_josekit { + use identity_verification::jws; + use josekit::jwk::alg::ec::EcKeyPair; + use josekit::jwk::Jwk; + use josekit::jws::JwsHeader; + use josekit::jwt::JwtPayload; + + use crate::EcDSAJwsVerifier; + + #[test] + fn test_es256k_josekit() { + let alg = josekit::jws::ES256K; + + let private_key: &str = r#" + { + "kty":"EC", + "crv":"secp256k1", + "d":"y0zUV7bLeUG_kDOvACFHnSmtH7j8MSJek25R2wJbWWg", + "x":"BBobbZkiC8E4C4EYekPNJkcXFCsMNHhh0AV2USy_xSs", + "y":"VQcPHjIQClX0b5TLluFl6jpIf9U-norWC0oEvIQRNyU" + }"#; + let josekit_jwk: Jwk = serde_json::from_str(private_key).unwrap(); + let mut src_header = JwsHeader::new(); + src_header.set_token_type("JWT"); + let mut src_payload = JwtPayload::new(); + src_payload.set_claim("key", Some("value".into())).unwrap(); + let eckp = EcKeyPair::from_jwk(&josekit_jwk).unwrap(); + let signer = alg.signer_from_jwk(&eckp.to_jwk_key_pair()).unwrap(); + let jwt_string = josekit::jwt::encode_with_signer(&src_payload, &src_header, &signer).unwrap(); + + let jws_verifier = EcDSAJwsVerifier::default(); + let decoder = jws::Decoder::new(); + let jwk: identity_verification::jwk::Jwk = serde_json::from_str(private_key).unwrap(); + assert!(decoder + .decode_compact_serialization(jwt_string.as_bytes(), None) + .and_then(|decoded| decoded.verify(&jws_verifier, &jwk)) + .is_ok()); + } +} diff --git a/identity_eddsa_verifier/Cargo.toml b/identity_eddsa_verifier/Cargo.toml index d8271444eb..0af8f31dd0 100644 --- a/identity_eddsa_verifier/Cargo.toml +++ b/identity_eddsa_verifier/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "identity_eddsa_verifier" -version = "1.1.0-alpha.1" +version = "1.3.0" authors.workspace = true edition.workspace = true homepage.workspace = true @@ -12,9 +12,12 @@ rust-version.workspace = true description = "JWS EdDSA signature verification for IOTA Identity" [dependencies] -identity_jose = { version = "=1.1.0-alpha.1", path = "../identity_jose", default-features = false } +identity_jose = { version = "=1.3.0", path = "../identity_jose", default-features = false } iota-crypto = { version = "0.23", default-features = false, features = ["std"] } [features] ed25519 = ["iota-crypto/ed25519"] default = ["ed25519"] + +[lints] +workspace = true diff --git a/identity_iota/Cargo.toml b/identity_iota/Cargo.toml index d519fb6917..0e0187801e 100644 --- a/identity_iota/Cargo.toml +++ b/identity_iota/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "identity_iota" -version = "1.1.0-alpha.1" +version = "1.3.0" authors.workspace = true edition.workspace = true homepage.workspace = true @@ -12,18 +12,18 @@ rust-version.workspace = true description = "Framework for Self-Sovereign Identity with IOTA DID." [dependencies] -identity_core = { version = "=1.1.0-alpha.1", path = "../identity_core", default-features = false } -identity_credential = { version = "=1.1.0-alpha.1", path = "../identity_credential", features = ["validator"], 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_iota_core = { version = "=1.1.0-alpha.1", path = "../identity_iota_core", default-features = false } -identity_resolver = { version = "=1.1.0-alpha.1", path = "../identity_resolver", default-features = false, optional = true } -identity_storage = { version = "=1.1.0-alpha.1", path = "../identity_storage", default-features = false, features = ["iota-document"] } -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_credential = { version = "=1.3.0", path = "../identity_credential", features = ["validator"], 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_iota_core = { version = "=1.3.0", path = "../identity_iota_core", default-features = false } +identity_resolver = { version = "=1.3.0", path = "../identity_resolver", default-features = false, optional = true } +identity_storage = { version = "=1.3.0", path = "../identity_storage", default-features = false, features = ["iota-document"] } +identity_verification = { version = "=1.3.0", path = "../identity_verification", default-features = false } [dev-dependencies] anyhow = "1.0.64" -iota-sdk = { version = "1.0", default-features = false, features = ["tls", "client"] } +iota-sdk = { version = "1.1.5", default-features = false, features = ["tls", "client"] } rand = "0.8.5" tokio = { version = "1.29.0", features = ["full"] } @@ -64,8 +64,14 @@ memstore = ["identity_storage/memstore"] # Enables selective disclosure features. sd-jwt = ["identity_credential/sd-jwt"] +# Enables zero knowledge selective disclosurable VCs +jpt-bbs-plus = ["identity_storage/jpt-bbs-plus", "identity_credential/jpt-bbs-plus"] + [package.metadata.docs.rs] # To build locally: # 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_iota/README.md b/identity_iota/README.md index 83254d2152..af03f510af 100644 --- a/identity_iota/README.md +++ b/identity_iota/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" } +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/identity_iota/src/lib.rs b/identity_iota/src/lib.rs index 24a20359eb..9ab2e53805 100644 --- a/identity_iota/src/lib.rs +++ b/identity_iota/src/lib.rs @@ -105,7 +105,22 @@ pub mod verification { pub mod storage { //! Storage traits. - pub use identity_storage::*; + /// KeyIdStorage types and functionalities. + pub mod key_id_storage { + pub use identity_storage::key_id_storage::*; + } + /// KeyStorage types and functionalities. + pub mod key_storage { + pub use identity_storage::key_storage::public_modules::*; + } + /// Storage types and functionalities. + #[allow(clippy::module_inception)] + pub mod storage { + pub use identity_storage::storage::*; + } + pub use identity_storage::key_id_storage::*; + pub use identity_storage::key_storage::*; + pub use identity_storage::storage::*; } #[cfg(feature = "sd-jwt")] diff --git a/identity_iota_core/Cargo.toml b/identity_iota_core/Cargo.toml index 3f7e9fbf12..e2539b28c0 100644 --- a/identity_iota_core/Cargo.toml +++ b/identity_iota_core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "identity_iota_core" -version = "1.1.0-alpha.1" +version = "1.3.0" authors.workspace = true edition.workspace = true homepage.workspace = true @@ -14,12 +14,12 @@ description = "An IOTA Ledger integration for the IOTA DID Method." [dependencies] async-trait = { version = "0.1.56", default-features = false, optional = true } futures = { version = "0.3", default-features = false } -identity_core = { version = "=1.1.0-alpha.1", path = "../identity_core", default-features = false } -identity_credential = { version = "=1.1.0-alpha.1", path = "../identity_credential", default-features = false, features = ["validator"] } -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 } -iota-sdk = { version = "1.0.2", default-features = false, features = ["serde", "std"], optional = true } +identity_core = { version = "=1.3.0", path = "../identity_core", default-features = false } +identity_credential = { version = "=1.3.0", path = "../identity_credential", default-features = false, features = ["validator"] } +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 } +iota-sdk = { version = "1.1.5", default-features = false, features = ["serde", "std"], optional = true } num-derive = { version = "0.4", default-features = false } num-traits = { version = "0.2", default-features = false, features = ["std"] } once_cell = { version = "1.18", default-features = false, features = ["std"] } @@ -45,9 +45,14 @@ rustdoc-args = ["--cfg", "docsrs"] default = ["client", "iota-client", "revocation-bitmap", "send-sync-client-ext"] # Exposes the IotaIdentityClient and IotaIdentityClientExt traits. client = ["dep:async-trait", "iota-sdk"] -# Enbales the implementation of the extension traits on the iota-sdk's Client. +# Enables the implementation of the extension traits on the iota-sdk's Client. iota-client = ["client", "iota-sdk/client", "iota-sdk/tls"] # Enables revocation with `RevocationBitmap2022`. revocation-bitmap = ["identity_credential/revocation-bitmap"] # Adds Send bounds on the futures produces by the client extension traits. send-sync-client-ext = [] +# Disables the blanket implementation of `IotaIdentityClientExt`. +test = ["client"] + +[lints] +workspace = true diff --git a/identity_iota_core/src/client/identity_client.rs b/identity_iota_core/src/client/identity_client.rs index 94b0cf88b2..34df1fd5f0 100644 --- a/identity_iota_core/src/client/identity_client.rs +++ b/identity_iota_core/src/client/identity_client.rs @@ -1,7 +1,8 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use crate::block::protocol::ProtocolParameters; +#[cfg(feature = "test")] +use iota_sdk::client::Client; use crate::block::address::Address; use crate::block::output::feature::SenderFeature; @@ -14,6 +15,7 @@ use crate::block::output::Feature; use crate::block::output::OutputId; use crate::block::output::RentStructure; use crate::block::output::UnlockCondition; +use crate::block::protocol::ProtocolParameters; use crate::Error; use crate::IotaDID; use crate::IotaDocument; @@ -192,7 +194,10 @@ pub trait IotaIdentityClientExt: IotaIdentityClient { } } +#[cfg(not(feature = "test"))] impl IotaIdentityClientExt for T where T: IotaIdentityClient {} +#[cfg(feature = "test")] +impl IotaIdentityClientExt for Client {} pub(super) async fn validate_network(client: &T, did: &IotaDID) -> Result<()> where diff --git a/identity_iota_core/src/document/iota_document.rs b/identity_iota_core/src/document/iota_document.rs index 89abf06cf5..7ae60381d7 100644 --- a/identity_iota_core/src/document/iota_document.rs +++ b/identity_iota_core/src/document/iota_document.rs @@ -5,7 +5,6 @@ use core::fmt; use core::fmt::Debug; use core::fmt::Display; use identity_credential::credential::Jws; -#[cfg(feature = "client")] use identity_did::CoreDID; use identity_did::DIDUrl; use identity_document::verifiable::JwsVerificationOptions; @@ -15,7 +14,6 @@ use serde::Deserialize; use serde::Serialize; use identity_core::common::Object; -#[cfg(feature = "client")] use identity_core::common::OneOrSet; use identity_core::common::OrderedSet; use identity_core::common::Url; @@ -123,9 +121,6 @@ impl IotaDocument { } /// Returns an iterator yielding the DID controllers. - /// - /// NOTE: controllers are determined by the `state_controller` unlock condition of the output - /// during resolution and are omitted when publishing. pub fn controller(&self) -> impl Iterator + '_ { let core_did_controller_iter = self .document @@ -134,11 +129,31 @@ impl IotaDocument { .into_iter() .flatten(); - // CORRECTNESS: These casts are OK because the public API does not expose methods - // enabling unchecked mutation of the controllers. + // CORRECTNESS: These casts are OK because the public API only allows setting IotaDIDs. core_did_controller_iter.map(IotaDID::from_inner_ref_unchecked) } + /// Sets the value of the document controller. + /// + /// Note: + /// * Duplicates in `controller` will be ignored. + /// * Use an empty collection to clear all controllers. + pub fn set_controller(&mut self, controller: T) + where + T: IntoIterator, + { + let controller_core_dids: Option> = { + let controller_set: OrderedSet = controller.into_iter().map(CoreDID::from).collect(); + if controller_set.is_empty() { + None + } else { + Some(OneOrSet::new_set(controller_set).expect("controller is checked to be not empty")) + } + }; + + *self.document.controller_mut() = controller_core_dids; + } + /// Returns a reference to the `alsoKnownAs` set. pub fn also_known_as(&self) -> &OrderedSet { self.document.also_known_as() @@ -442,7 +457,14 @@ mod client_document { _ => None, }; - *self.core_document_mut().controller_mut() = controller_did.map(CoreDID::from).map(OneOrSet::new_one); + if let Some(controller_did) = controller_did { + match self.core_document_mut().controller_mut() { + Some(controllers) => { + controllers.append(CoreDID::from(controller_did)); + } + None => *self.core_document_mut().controller_mut() = Some(OneOrSet::new_one(CoreDID::from(controller_did))), + } + } Ok(()) } @@ -731,6 +753,98 @@ mod tests { assert_eq!(doc1, doc2); } + #[test] + fn test_unpack_no_external_controller() { + let document_did: IotaDID = "did:iota:0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + .parse() + .unwrap(); + let alias_controller: IotaDID = "did:iota:0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB" + .parse() + .unwrap(); + + let mut original_doc: IotaDocument = IotaDocument::new_with_id(document_did.clone()); + original_doc.set_controller([]); + + let alias_output: AliasOutput = AliasOutputBuilder::new_with_amount(1, AliasId::from(&document_did)) + .with_state_metadata(original_doc.pack().unwrap()) + .add_unlock_condition(UnlockCondition::StateControllerAddress( + StateControllerAddressUnlockCondition::new(Address::Alias(AliasAddress::new(AliasId::from(&alias_controller)))), + )) + .add_unlock_condition(UnlockCondition::GovernorAddress(GovernorAddressUnlockCondition::new( + Address::Alias(AliasAddress::new(AliasId::from(&alias_controller))), + ))) + .finish() + .unwrap(); + + let document: IotaDocument = IotaDocument::unpack_from_output(&document_did, &alias_output, true).unwrap(); + let controllers: Vec = document.controller().cloned().collect::>(); + assert_eq!(controllers.first().unwrap(), &alias_controller); + assert_eq!(controllers.len(), 1); + } + + #[test] + fn test_unpack_with_duplicate_controller() { + let document_did: IotaDID = "did:iota:0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + .parse() + .unwrap(); + let alias_controller: IotaDID = "did:iota:0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB" + .parse() + .unwrap(); + + let mut original_doc: IotaDocument = IotaDocument::new_with_id(document_did.clone()); + original_doc.set_controller([alias_controller.clone()]); + + let alias_output: AliasOutput = AliasOutputBuilder::new_with_amount(1, AliasId::from(&document_did)) + .with_state_metadata(original_doc.pack().unwrap()) + .add_unlock_condition(UnlockCondition::StateControllerAddress( + StateControllerAddressUnlockCondition::new(Address::Alias(AliasAddress::new(AliasId::from(&alias_controller)))), + )) + .add_unlock_condition(UnlockCondition::GovernorAddress(GovernorAddressUnlockCondition::new( + Address::Alias(AliasAddress::new(AliasId::from(&alias_controller))), + ))) + .finish() + .unwrap(); + + let document: IotaDocument = IotaDocument::unpack_from_output(&document_did, &alias_output, true).unwrap(); + let controllers: Vec = document.controller().cloned().collect::>(); + assert_eq!(controllers.first().unwrap(), &alias_controller); + assert_eq!(controllers.len(), 1); + } + + #[test] + fn test_unpack_with_external_controller() { + let document_did: IotaDID = "did:iota:0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + .parse() + .unwrap(); + let alias_controller: IotaDID = "did:iota:0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB" + .parse() + .unwrap(); + let external_controller_did: IotaDID = + "did:iota:0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC" + .parse() + .unwrap(); + + let mut original_doc: IotaDocument = IotaDocument::new_with_id(document_did.clone()); + original_doc.set_controller([external_controller_did.clone()]); + + let alias_output: AliasOutput = AliasOutputBuilder::new_with_amount(1, AliasId::from(&document_did)) + .with_state_metadata(original_doc.pack().unwrap()) + .add_unlock_condition(UnlockCondition::StateControllerAddress( + StateControllerAddressUnlockCondition::new(Address::Alias(AliasAddress::new(AliasId::from(&alias_controller)))), + )) + .add_unlock_condition(UnlockCondition::GovernorAddress(GovernorAddressUnlockCondition::new( + Address::Alias(AliasAddress::new(AliasId::from(&alias_controller))), + ))) + .finish() + .unwrap(); + + let document: IotaDocument = IotaDocument::unpack_from_output(&document_did, &alias_output, true).unwrap(); + let controllers: Vec = document.controller().cloned().collect::>(); + assert_eq!(controllers.first().unwrap(), &external_controller_did); + assert_eq!(controllers.get(1).unwrap(), &alias_controller); + assert_eq!(controllers.len(), 2); + } + #[test] fn test_unpack_empty() { let controller_did: IotaDID = valid_did(); @@ -765,7 +879,10 @@ mod tests { let packed: Vec = document.pack_with_encoding(StateMetadataEncoding::Json).unwrap(); let state_metadata_document: StateMetadataDocument = StateMetadataDocument::unpack(&packed).unwrap(); let unpacked_document: IotaDocument = state_metadata_document.into_iota_document(&did).unwrap(); - assert!(unpacked_document.document.controller().is_none()); + assert_eq!( + unpacked_document.document.controller().unwrap().get(0).unwrap().clone(), + CoreDID::from(controller_did) + ); assert!(unpacked_document.metadata.state_controller_address.is_none()); assert!(unpacked_document.metadata.governor_address.is_none()); } diff --git a/identity_iota_core/src/document/test_utils.rs b/identity_iota_core/src/document/test_utils.rs index b8c48cadf4..b45d418751 100644 --- a/identity_iota_core/src/document/test_utils.rs +++ b/identity_iota_core/src/document/test_utils.rs @@ -24,7 +24,7 @@ fn encode_public_ed25519_jwk(public_key: &[u8]) -> Jwk { let mut params = JwkParamsOkp::new(); params.x = x; params.d = None; - params.crv = EdCurve::Ed25519.name().to_owned(); + params.crv = EdCurve::Ed25519.name().to_string(); let mut jwk = Jwk::from_params(params); jwk.set_alg(JwsAlgorithm::EdDSA.name()); jwk diff --git a/identity_iota_core/src/state_metadata/document.rs b/identity_iota_core/src/state_metadata/document.rs index d15f0d8d26..e14e381f5b 100644 --- a/identity_iota_core/src/state_metadata/document.rs +++ b/identity_iota_core/src/state_metadata/document.rs @@ -79,7 +79,6 @@ impl StateMetadataDocument { // Unset Governor and State Controller Addresses to avoid bloating the payload self.metadata.governor_address = None; self.metadata.state_controller_address = None; - *self.document.controller_mut() = None; let encoded_message_data: Vec = match encoding { StateMetadataEncoding::Json => self @@ -410,8 +409,7 @@ mod tests { let TestSetup { document, .. } = test_document(); let mut state_metadata_doc: StateMetadataDocument = StateMetadataDocument::from(document); let packed: Vec = state_metadata_doc.clone().pack(StateMetadataEncoding::Json).unwrap(); - // Controller and State Controller are set to None when packing - *state_metadata_doc.document.controller_mut() = None; + // Governor and State Controller are set to None when packing state_metadata_doc.metadata.governor_address = None; state_metadata_doc.metadata.state_controller_address = None; let expected_payload: String = format!( @@ -434,6 +432,31 @@ mod tests { assert_eq!(&packed[7..], expected_payload.as_bytes()); } + #[test] + fn test_no_controller() { + let TestSetup { + mut document, did_self, .. + } = test_document(); + *document.core_document_mut().controller_mut() = None; + let state_metadata_doc: StateMetadataDocument = StateMetadataDocument::from(document); + let packed: Vec = state_metadata_doc.clone().pack(StateMetadataEncoding::Json).unwrap(); + let expected_payload: String = format!( + "{{\"doc\":{},\"meta\":{}}}", + state_metadata_doc.document, state_metadata_doc.metadata + ); + assert_eq!(&packed[7..], expected_payload.as_bytes()); + let unpacked = StateMetadataDocument::unpack(&packed).unwrap(); + assert_eq!( + unpacked + .into_iota_document(&did_self) + .unwrap() + .controller() + .collect::>() + .len(), + 0 + ); + } + #[test] fn test_unpack_length_prefix() { // Changing the serialization is a breaking change! diff --git a/identity_jose/Cargo.toml b/identity_jose/Cargo.toml index 748b481910..98a802ffbe 100644 --- a/identity_jose/Cargo.toml +++ b/identity_jose/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "identity_jose" -version = "1.1.0-alpha.1" +version = "1.3.0" authors.workspace = true edition.workspace = true homepage.workspace = true @@ -12,8 +12,9 @@ rust-version.workspace = true description = "A library for JOSE (JSON Object Signing and Encryption)" [dependencies] -identity_core = { version = "=1.1.0-alpha.1", path = "../identity_core", default-features = false } +identity_core = { version = "=1.3.0", path = "../identity_core", default-features = false } iota-crypto = { version = "0.23", default-features = false, features = ["std", "sha"] } +json-proof-token.workspace = true serde.workspace = true serde_json = { version = "1.0", default-features = false, features = ["std"] } subtle = { version = "2.5", default-features = false } @@ -29,3 +30,6 @@ signature = { version = "2", default-features = false } [[example]] name = "jws_encoding_decoding" test = true + +[lints] +workspace = true diff --git a/identity_jose/src/jwk/curve/bls.rs b/identity_jose/src/jwk/curve/bls.rs new file mode 100644 index 0000000000..97b68bf678 --- /dev/null +++ b/identity_jose/src/jwk/curve/bls.rs @@ -0,0 +1,43 @@ +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +use core::fmt::Display; +use core::fmt::Formatter; +use core::fmt::Result; + +/// Supported BLS Curves. +/// +/// [More Info](https://datatracker.ietf.org/doc/html/draft-ietf-cose-bls-key-representations-05#name-curve-parameter-registratio) +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub enum BlsCurve { + /// A cryptographic key on the Barreto-Lynn-Scott (BLS) curve featuring an embedding degree 12 with 381-bit p in the + /// subgroup of G1. + BLS12381G1, + /// A cryptographic key on the Barreto-Lynn-Scott (BLS) curve featuring an embedding degree 12 with 381-bit p in the + /// subgroup of G2. + BLS12381G2, + /// A cryptographic key on the Barreto-Lynn-Scott (BLS) curve featuring an embedding degree 48 with 581-bit p in the + /// subgroup of G1. + BLS48581G1, + /// A cryptographic key on the Barreto-Lynn-Scott (BLS) curve featuring an embedding degree 48 with 581-bit p in the + /// subgroup of G2. + BLS48581G2, +} + +impl BlsCurve { + /// Returns the name of the curve as a string slice. + pub const fn name(self) -> &'static str { + match self { + Self::BLS12381G1 => "BLS12381G1", + Self::BLS12381G2 => "BLS12381G2", + Self::BLS48581G1 => "BLS48581G1", + Self::BLS48581G2 => "BLS48581G2", + } + } +} + +impl Display for BlsCurve { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + f.write_str(self.name()) + } +} diff --git a/identity_jose/src/jwk/curve/mod.rs b/identity_jose/src/jwk/curve/mod.rs index 38a1e3bba7..8e1627219f 100644 --- a/identity_jose/src/jwk/curve/mod.rs +++ b/identity_jose/src/jwk/curve/mod.rs @@ -1,10 +1,12 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +mod bls; mod ec; mod ecx; mod ed; +pub use self::bls::*; pub use self::ec::*; pub use self::ecx::*; pub use self::ed::*; diff --git a/identity_jose/src/jwk/jwk_ext.rs b/identity_jose/src/jwk/jwk_ext.rs new file mode 100644 index 0000000000..39fc02fa93 --- /dev/null +++ b/identity_jose/src/jwk/jwk_ext.rs @@ -0,0 +1,162 @@ +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +use super::Jwk; +use super::JwkOperation; +use super::JwkParams; +use super::JwkParamsEc; +use super::JwkType; +use super::JwkUse; +use identity_core::common::Url; +use jsonprooftoken::jpa::algs::ProofAlgorithm; +use jsonprooftoken::jwk::alg_parameters::Algorithm; +use jsonprooftoken::jwk::alg_parameters::JwkAlgorithmParameters; +use jsonprooftoken::jwk::alg_parameters::JwkEllipticCurveKeyParameters; +use jsonprooftoken::jwk::curves::EllipticCurveTypes; +use jsonprooftoken::jwk::key::Jwk as JwkExt; +use jsonprooftoken::jwk::key::KeyOps; +use jsonprooftoken::jwk::key::PKUse; +use jsonprooftoken::jwk::types::KeyType; +use std::str::FromStr; + +impl From for JwkOperation { + fn from(value: KeyOps) -> Self { + match value { + KeyOps::Sign => Self::Sign, + KeyOps::Verify => Self::Verify, + KeyOps::Encrypt => Self::Encrypt, + KeyOps::Decrypt => Self::Decrypt, + KeyOps::WrapKey => Self::WrapKey, + KeyOps::UnwrapKey => Self::UnwrapKey, + KeyOps::DeriveKey => Self::DeriveKey, + KeyOps::DeriveBits => Self::DeriveBits, + KeyOps::ProofGeneration => Self::ProofGeneration, + KeyOps::ProofVerification => Self::ProofVerification, + } + } +} + +impl From for KeyOps { + fn from(value: JwkOperation) -> Self { + match value { + JwkOperation::Sign => Self::Sign, + JwkOperation::Verify => Self::Verify, + JwkOperation::Encrypt => Self::Encrypt, + JwkOperation::Decrypt => Self::Decrypt, + JwkOperation::WrapKey => Self::WrapKey, + JwkOperation::UnwrapKey => Self::UnwrapKey, + JwkOperation::DeriveKey => Self::DeriveKey, + JwkOperation::DeriveBits => Self::DeriveBits, + JwkOperation::ProofGeneration => Self::ProofGeneration, + JwkOperation::ProofVerification => Self::ProofVerification, + } + } +} + +impl From for JwkUse { + fn from(value: PKUse) -> Self { + match value { + PKUse::Signature => Self::Signature, + PKUse::Encryption => Self::Encryption, + PKUse::Proof => Self::Proof, + } + } +} + +impl From for PKUse { + fn from(value: JwkUse) -> Self { + match value { + JwkUse::Signature => Self::Signature, + JwkUse::Encryption => Self::Encryption, + JwkUse::Proof => Self::Proof, + } + } +} + +impl From for JwkParamsEc { + fn from(value: JwkEllipticCurveKeyParameters) -> Self { + Self { + crv: value.crv.to_string(), + x: value.x, + y: value.y, + d: value.d, + } + } +} + +impl TryInto for &JwkParamsEc { + type Error = crate::error::Error; + + fn try_into(self) -> Result { + Ok(JwkEllipticCurveKeyParameters { + kty: KeyType::EllipticCurve, + crv: EllipticCurveTypes::from_str(&self.crv).map_err(|_| Self::Error::KeyError("crv not supported!"))?, + x: self.x.clone(), + y: self.y.clone(), + d: self.d.clone(), + }) + } +} + +impl TryFrom for Jwk { + type Error = crate::error::Error; + + fn try_from(value: JwkExt) -> Result { + let x5u = match value.x5u { + Some(v) => Some(Url::from_str(&v).map_err(|_| Self::Error::InvalidClaim("x5u"))?), + None => None, + }; + + let (kty, params) = match value.key_params { + JwkAlgorithmParameters::EllipticCurve(p) => (JwkType::Ec, JwkParams::Ec(JwkParamsEc::from(p))), + _ => unreachable!(), + }; + + Ok(Self { + kty, + use_: value.pk_use.map(JwkUse::from), + key_ops: value + .key_ops + .map(|vec_key_ops| vec_key_ops.into_iter().map(JwkOperation::from).collect()), + alg: value.alg.map(|a| a.to_string()), + kid: value.kid, + x5u, + x5c: value.x5c, + x5t: value.x5t, + x5t_s256: None, + params, + }) + } +} + +impl TryInto for &Jwk { + type Error = crate::error::Error; + + fn try_into(self) -> Result { + let params = match &self.params { + JwkParams::Ec(p) => JwkAlgorithmParameters::EllipticCurve(p.try_into()?), + _ => return Err(Self::Error::InvalidParam("Parameters not supported!")), + }; + + let alg = match &self.alg { + Some(a) => Some(Algorithm::Proof( + ProofAlgorithm::from_str(a).map_err(|_| Self::Error::KeyError("Invalid alg"))?, + )), + None => None, + }; + + Ok(JwkExt { + kid: self.kid.clone(), + pk_use: self.use_.map(|u| u.into()), + key_ops: self + .key_ops + .as_deref() + .and_then(|vec_key_ops| vec_key_ops.iter().map(|o| Some((*o).into())).collect()), + alg, + x5u: self.x5u.as_ref().map(|v| v.as_str().to_string()), + x5c: self.x5c.clone(), + x5t: self.x5t.clone(), + key_params: params, + }) + } +} diff --git a/identity_jose/src/jwk/key_operation.rs b/identity_jose/src/jwk/key_operation.rs index 8fda0b6a23..ac6b7b0ce8 100644 --- a/identity_jose/src/jwk/key_operation.rs +++ b/identity_jose/src/jwk/key_operation.rs @@ -27,6 +27,10 @@ pub enum JwkOperation { DeriveKey, /// Derive bits not to be used as a key. DeriveBits, + /// Compute proof + ProofGeneration, + /// Verify proof + ProofVerification, } impl JwkOperation { @@ -41,6 +45,8 @@ impl JwkOperation { Self::UnwrapKey => "unwrapKey", Self::DeriveKey => "deriveKey", Self::DeriveBits => "deriveBits", + Self::ProofGeneration => "proofGeneration", + Self::ProofVerification => "proofVerification", } } @@ -55,6 +61,8 @@ impl JwkOperation { Self::UnwrapKey => Self::WrapKey, Self::DeriveKey => Self::DeriveKey, Self::DeriveBits => Self::DeriveBits, + Self::ProofGeneration => Self::ProofVerification, + Self::ProofVerification => Self::ProofGeneration, } } } diff --git a/identity_jose/src/jwk/key_params.rs b/identity_jose/src/jwk/key_params.rs index f60d6d5e66..9d1437637a 100644 --- a/identity_jose/src/jwk/key_params.rs +++ b/identity_jose/src/jwk/key_params.rs @@ -10,6 +10,8 @@ use crate::jwk::EcxCurve; use crate::jwk::EdCurve; use crate::jwk::JwkType; +use super::BlsCurve; + /// Algorithm-specific parameters for JSON Web Keys. /// /// [More Info](https://tools.ietf.org/html/rfc7518#section-6) @@ -103,6 +105,12 @@ pub struct JwkParamsEc { pub d: Option, // ECC Private Key } +impl Default for JwkParamsEc { + fn default() -> Self { + Self::new() + } +} + impl JwkParamsEc { /// Creates new JWK EC Params. pub const fn new() -> Self { @@ -149,6 +157,17 @@ impl JwkParamsEc { _ => Err(Error::KeyError("Ec Curve")), } } + + /// Returns the [`BlsCurve`] if it is of a supported type. + pub fn try_bls_curve(&self) -> Result { + match &*self.crv { + "BLS12381G1" => Ok(BlsCurve::BLS12381G1), + "BLS12381G2" => Ok(BlsCurve::BLS12381G2), + "BLS48581G1" => Ok(BlsCurve::BLS48581G1), + "BLS48581G2" => Ok(BlsCurve::BLS48581G2), + _ => Err(Error::KeyError("BLS Curve")), + } + } } impl From for JwkParams { @@ -238,6 +257,12 @@ pub struct JwkParamsRsaPrime { pub t: String, // Factor CRT Coefficient } +impl Default for JwkParamsRsa { + fn default() -> Self { + Self::new() + } +} + impl JwkParamsRsa { /// Creates new JWK RSA Params. pub const fn new() -> Self { @@ -320,6 +345,12 @@ pub struct JwkParamsOct { pub k: String, // Key Value } +impl Default for JwkParamsOct { + fn default() -> Self { + Self::new() + } +} + impl JwkParamsOct { /// Creates new JWK Oct Params. pub const fn new() -> Self { @@ -369,6 +400,12 @@ pub struct JwkParamsOkp { pub d: Option, // Private Key } +impl Default for JwkParamsOkp { + fn default() -> Self { + Self::new() + } +} + impl JwkParamsOkp { /// Creates new JWK OKP Params. pub const fn new() -> Self { diff --git a/identity_jose/src/jwk/key_use.rs b/identity_jose/src/jwk/key_use.rs index a686ba79cc..edd427c578 100644 --- a/identity_jose/src/jwk/key_use.rs +++ b/identity_jose/src/jwk/key_use.rs @@ -16,6 +16,9 @@ pub enum JwkUse { /// Encryption. #[serde(rename = "enc")] Encryption, + /// Proof + #[serde(rename = "proof")] + Proof, } impl JwkUse { @@ -24,6 +27,7 @@ impl JwkUse { match self { Self::Signature => "sig", Self::Encryption => "enc", + Self::Proof => "proof", } } } diff --git a/identity_jose/src/jwk/mod.rs b/identity_jose/src/jwk/mod.rs index a714cbf5ac..780c7f9861 100644 --- a/identity_jose/src/jwk/mod.rs +++ b/identity_jose/src/jwk/mod.rs @@ -4,6 +4,7 @@ //! JSON Web Keys ([JWK](https://tools.ietf.org/html/rfc7517)) mod curve; +mod jwk_ext; mod key; mod key_operation; mod key_params; diff --git a/identity_jose/src/jwt/header.rs b/identity_jose/src/jwt/header.rs index 631bd151c3..ca87211c84 100644 --- a/identity_jose/src/jwt/header.rs +++ b/identity_jose/src/jwt/header.rs @@ -105,6 +105,12 @@ pub struct JwtHeader { nonce: Option, } +impl Default for JwtHeader { + fn default() -> Self { + Self::new() + } +} + impl JwtHeader { /// Create a new `JwtHeader`. pub const fn new() -> Self { diff --git a/identity_jose/src/jwu/serde.rs b/identity_jose/src/jwu/serde.rs index a5e6c1f84d..cd80a1c949 100644 --- a/identity_jose/src/jwu/serde.rs +++ b/identity_jose/src/jwu/serde.rs @@ -24,10 +24,10 @@ pub(crate) fn parse_utf8(slice: &(impl AsRef<[u8]> + ?Sized)) -> Result<&str> { str::from_utf8(slice.as_ref()).map_err(Error::InvalidUtf8) } -pub(crate) fn filter_non_empty_bytes<'a, T, U: 'a>(value: T) -> Option<&'a [u8]> +pub(crate) fn filter_non_empty_bytes<'a, T, U>(value: T) -> Option<&'a [u8]> where T: Into>, - U: AsRef<[u8]> + ?Sized, + U: AsRef<[u8]> + ?Sized + 'a, { value.into().map(AsRef::as_ref).filter(|value| !value.is_empty()) } diff --git a/identity_resolver/Cargo.toml b/identity_resolver/Cargo.toml index 76679caa5f..e6d93b03f0 100644 --- a/identity_resolver/Cargo.toml +++ b/identity_resolver/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "identity_resolver" -version = "1.1.0-alpha.1" +version = "1.3.0" authors.workspace = true edition.workspace = true homepage.workspace = true @@ -15,22 +15,24 @@ description = "DID Resolution utilities for the identity.rs library." # This is currently necessary for the ResolutionHandler trait. This can be made an optional dependency if alternative ways of attaching handlers are introduced. async-trait = { version = "0.1", default-features = false } futures = { version = "0.3" } -identity_core = { version = "=1.1.0-alpha.1", path = "../identity_core", default-features = false } -identity_credential = { version = "=1.1.0-alpha.1", path = "../identity_credential", default-features = false, features = ["validator"] } -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_core = { version = "=1.3.0", path = "../identity_core", default-features = false } +identity_credential = { version = "=1.3.0", path = "../identity_credential", default-features = false, features = ["validator"] } +identity_did = { version = "=1.3.0", path = "../identity_did", default-features = false } +identity_document = { version = "=1.3.0", path = "../identity_document", default-features = false } serde = { version = "1.0", default-features = false, features = ["std", "derive"] } strum.workspace = true thiserror = { version = "1.0", default-features = false } [dependencies.identity_iota_core] -version = "=1.1.0-alpha.1" +version = "=1.3.0" path = "../identity_iota_core" default-features = false features = ["send-sync-client-ext", "iota-client"] optional = true [dev-dependencies] +identity_iota_core = { path = "../identity_iota_core", features = ["test"] } +iota-sdk = { version = "1.1.5" } tokio = { version = "1.29.0", default-features = false, features = ["rt-multi-thread", "macros"] } [features] @@ -38,3 +40,6 @@ default = ["revocation-bitmap", "iota"] revocation-bitmap = ["identity_credential/revocation-bitmap", "identity_iota_core?/revocation-bitmap"] # Enables the IOTA integration for the resolver. iota = ["dep:identity_iota_core"] + +[lints] +workspace = true diff --git a/identity_resolver/src/error.rs b/identity_resolver/src/error.rs index 5a8c3c63f4..d72a78fd4a 100644 --- a/identity_resolver/src/error.rs +++ b/identity_resolver/src/error.rs @@ -68,4 +68,7 @@ pub enum ErrorCause { /// The method that is unsupported. method: String, }, + /// No client attached to the specific network. + #[error("none of the attached clients support the network {0}")] + UnsupportedNetwork(String), } diff --git a/identity_resolver/src/resolution/resolver.rs b/identity_resolver/src/resolution/resolver.rs index 10d5359db8..b8ceffbc7f 100644 --- a/identity_resolver/src/resolution/resolver.rs +++ b/identity_resolver/src/resolution/resolver.rs @@ -249,12 +249,14 @@ impl Resolver> { #[cfg(feature = "iota")] mod iota_handler { + use crate::ErrorCause; + use super::Resolver; use identity_document::document::CoreDocument; - use identity_iota_core::IotaClientExt; use identity_iota_core::IotaDID; use identity_iota_core::IotaDocument; use identity_iota_core::IotaIdentityClientExt; + use std::collections::HashMap; use std::sync::Arc; impl Resolver @@ -266,7 +268,7 @@ mod iota_handler { /// See also [`attach_handler`](Self::attach_handler). pub fn attach_iota_handler(&mut self, client: CLI) where - CLI: IotaClientExt + Send + Sync + 'static, + CLI: IotaIdentityClientExt + Send + Sync + 'static, { let arc_client: Arc = Arc::new(client); @@ -277,6 +279,58 @@ mod iota_handler { self.attach_handler(IotaDID::METHOD.to_owned(), handler); } + + /// Convenience method for attaching multiple handlers responsible for resolving IOTA DIDs + /// on multiple networks. + /// + /// + /// # Arguments + /// + /// * `clients` - A collection of tuples where each tuple contains the name of the network name and its + /// corresponding client. + /// + /// # Examples + /// + /// ```ignore + /// // Assume `smr_client` and `iota_client` are instances IOTA clients `iota_sdk::client::Client`. + /// attach_multiple_iota_handlers(vec![("smr", smr_client), ("iota", iota_client)]); + /// ``` + /// + /// # See Also + /// - [`attach_handler`](Self::attach_handler). + /// + /// # Note + /// + /// - Using `attach_iota_handler` or `attach_handler` for the IOTA method would override all + /// previously added clients. + /// - This function does not validate the provided configuration. Ensure that the provided + /// network name corresponds with the client, possibly by using `client.network_name()`. + pub fn attach_multiple_iota_handlers(&mut self, clients: I) + where + CLI: IotaIdentityClientExt + Send + Sync + 'static, + I: IntoIterator, + { + let arc_clients = Arc::new(clients.into_iter().collect::>()); + + let handler = move |did: IotaDID| { + let future_client = arc_clients.clone(); + async move { + let did_network = did.network_str(); + let client: &CLI = + future_client + .get(did_network) + .ok_or(crate::Error::new(ErrorCause::UnsupportedNetwork( + did_network.to_string(), + )))?; + client + .resolve_did(&did) + .await + .map_err(|err| crate::Error::new(ErrorCause::HandlerError { source: Box::new(err) })) + } + }; + + self.attach_handler(IotaDID::METHOD.to_owned(), handler); + } } } @@ -301,3 +355,63 @@ where .finish() } } + +#[cfg(test)] +mod tests { + use identity_iota_core::block::output::AliasId; + use identity_iota_core::block::output::AliasOutput; + use identity_iota_core::block::output::OutputId; + use identity_iota_core::block::protocol::ProtocolParameters; + use identity_iota_core::IotaDID; + use identity_iota_core::IotaDocument; + use identity_iota_core::IotaIdentityClient; + use identity_iota_core::IotaIdentityClientExt; + + use super::*; + + struct DummyClient(IotaDocument); + + #[async_trait::async_trait] + impl IotaIdentityClient for DummyClient { + async fn get_alias_output(&self, _id: AliasId) -> identity_iota_core::Result<(OutputId, AliasOutput)> { + unreachable!() + } + async fn get_protocol_parameters(&self) -> identity_iota_core::Result { + unreachable!() + } + } + + #[async_trait::async_trait] + impl IotaIdentityClientExt for DummyClient { + async fn resolve_did(&self, did: &IotaDID) -> identity_iota_core::Result { + if self.0.id().as_str() == did.as_str() { + Ok(self.0.clone()) + } else { + Err(identity_iota_core::Error::DIDResolutionError( + iota_sdk::client::error::Error::NoOutput(did.to_string()), + )) + } + } + } + + #[tokio::test] + async fn test_multiple_handlers() { + let did1 = + IotaDID::parse("did:iota:smr:0x0101010101010101010101010101010101010101010101010101010101010101").unwrap(); + let document = IotaDocument::new_with_id(did1.clone()); + let dummy_smr_client = DummyClient(document); + + let did2 = IotaDID::parse("did:iota:0x0101010101010101010101010101010101010101010101010101010101010101").unwrap(); + let document = IotaDocument::new_with_id(did2.clone()); + let dummy_iota_client = DummyClient(document); + + let mut resolver = Resolver::::new(); + resolver.attach_multiple_iota_handlers(vec![("iota", dummy_iota_client), ("smr", dummy_smr_client)]); + + let doc = resolver.resolve(&did1).await.unwrap(); + assert_eq!(doc.id(), &did1); + + let doc = resolver.resolve(&did2).await.unwrap(); + assert_eq!(doc.id(), &did2); + } +} diff --git a/identity_storage/Cargo.toml b/identity_storage/Cargo.toml index ca2cc6eb8d..0f1e423b7d 100644 --- a/identity_storage/Cargo.toml +++ b/identity_storage/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "identity_storage" -version = "1.1.0-alpha.1" +version = "1.3.0" authors.workspace = true edition.workspace = true homepage.workspace = true @@ -12,25 +12,28 @@ rust-version.workspace = true description = "Abstractions over storage for cryptographic keys used in DID Documents" [dependencies] +anyhow = "1.0.82" async-trait = { version = "0.1.64", default-features = false } futures = { version = "0.3.27", default-features = false, features = ["async-await"] } -identity_core = { version = "=1.1.0-alpha.1", path = "../identity_core", default-features = false } -identity_credential = { version = "=1.1.0-alpha.1", path = "../identity_credential", default-features = false, features = ["credential", "presentation"] } -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_iota_core = { version = "=1.1.0-alpha.1", path = "../identity_iota_core", default-features = false, optional = true } -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_credential = { version = "=1.3.0", path = "../identity_credential", default-features = false, features = ["credential", "presentation", "revocation-bitmap"] } +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_iota_core = { version = "=1.3.0", path = "../identity_iota_core", default-features = false, optional = true } +identity_verification = { version = "=1.3.0", path = "../identity_verification", default_features = false } iota-crypto = { version = "0.23", default-features = false, features = ["ed25519"], optional = true } +json-proof-token = { workspace = true, optional = true } rand = { version = "0.8.5", default-features = false, features = ["std", "std_rng"], optional = true } seahash = { version = "4.1.0", default_features = false } serde.workspace = true serde_json.workspace = true thiserror.workspace = true tokio = { version = "1.29.0", default-features = false, features = ["macros", "sync"], optional = true } +zkryptium = { workspace = true, optional = true } [dev-dependencies] -identity_credential = { version = "=1.1.0-alpha.1", path = "../identity_credential", features = ["revocation-bitmap"] } -identity_eddsa_verifier = { version = "=1.1.0-alpha.1", path = "../identity_eddsa_verifier", default-features = false, features = ["ed25519"] } +identity_credential = { version = "=1.3.0", path = "../identity_credential", features = ["revocation-bitmap"] } +identity_eddsa_verifier = { version = "=1.3.0", path = "../identity_eddsa_verifier", default-features = false, features = ["ed25519"] } once_cell = { version = "1.18", default-features = false } tokio = { version = "1.29.0", default-features = false, features = ["macros", "sync", "rt"] } @@ -42,3 +45,8 @@ memstore = ["dep:tokio", "dep:rand", "dep:iota-crypto"] send-sync-storage = [] # Implements the JwkStorageDocumentExt trait for IotaDocument iota-document = ["dep:identity_iota_core"] +# Enables JSON Proof Token & BBS+ related features +jpt-bbs-plus = ["identity_credential/jpt-bbs-plus", "dep:zkryptium", "dep:json-proof-token"] + +[lints] +workspace = true diff --git a/identity_storage/src/key_storage/bls.rs b/identity_storage/src/key_storage/bls.rs new file mode 100644 index 0000000000..2a3b38a0a7 --- /dev/null +++ b/identity_storage/src/key_storage/bls.rs @@ -0,0 +1,203 @@ +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +use anyhow::Context; +use identity_verification::jose::jwk::Jwk; +use identity_verification::jose::jwu; +use identity_verification::jwk::BlsCurve; +use identity_verification::jwk::JwkParamsEc; +use jsonprooftoken::jpa::algs::ProofAlgorithm; +use zkryptium::bbsplus::ciphersuites::BbsCiphersuite; +use zkryptium::bbsplus::ciphersuites::Bls12381Sha256; +use zkryptium::bbsplus::ciphersuites::Bls12381Shake256; +use zkryptium::bbsplus::keys::BBSplusPublicKey; +use zkryptium::bbsplus::keys::BBSplusSecretKey; +use zkryptium::keys::pair::KeyPair; +use zkryptium::schemes::algorithms::BBSplus; +use zkryptium::schemes::generics::Signature; + +use crate::key_storage::KeyStorageError; +use crate::key_storage::KeyStorageErrorKind; +use crate::key_storage::KeyStorageResult; +use crate::ProofUpdateCtx; + +fn random_bbs_keypair() -> Result<(BBSplusSecretKey, BBSplusPublicKey), zkryptium::errors::Error> +where + S: BbsCiphersuite, +{ + KeyPair::>::random().map(KeyPair::into_parts) +} + +/// Generates a new BBS+ keypair using either `BLS12381-SHA256` or `BLS12381-SHAKE256`. +pub fn generate_bbs_keypair(alg: ProofAlgorithm) -> KeyStorageResult<(BBSplusSecretKey, BBSplusPublicKey)> { + match alg { + ProofAlgorithm::BLS12381_SHA256 => random_bbs_keypair::(), + ProofAlgorithm::BLS12381_SHAKE256 => random_bbs_keypair::(), + _ => return Err(KeyStorageErrorKind::UnsupportedProofAlgorithm.into()), + } + .map_err(|err| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(err)) +} + +/// Encodes a private BBS+ key into JWK. +pub fn encode_bls_jwk( + private_key: &BBSplusSecretKey, + public_key: &BBSplusPublicKey, + alg: ProofAlgorithm, +) -> (Jwk, Jwk) { + let (x, y) = public_key.to_coordinates(); + let x = jwu::encode_b64(x); + let y = jwu::encode_b64(y); + + let d = jwu::encode_b64(private_key.to_bytes()); + let params = JwkParamsEc { + x, + y, + d: Some(d), + crv: BlsCurve::BLS12381G2.name().to_owned(), + }; + + let mut jwk = Jwk::from_params(params); + + jwk.set_alg(alg.to_string()); + jwk.set_kid(jwk.thumbprint_sha256_b64()); + let public_jwk = jwk.to_public().expect("kty != oct"); + + (jwk, public_jwk) +} + +/// Attempts to decode JWK into a BBS+ keypair. +pub fn expand_bls_jwk(jwk: &Jwk) -> KeyStorageResult<(Option, BBSplusPublicKey)> { + // Check the provided JWK represents a BLS12381G2 key. + let params = jwk + .try_ec_params() + .ok() + .filter(|params| { + params + .try_bls_curve() + .map(|curve| curve == BlsCurve::BLS12381G2) + .unwrap_or(false) + }) + .context(format!("not a {} curve key", BlsCurve::BLS12381G2)) + .map_err(|e| KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType).with_source(e))?; + + let sk = params + .d + .as_deref() + .map(|d| { + jwu::decode_b64(d) + .context("`d` parameter is not base64 encoded") + .and_then(|bytes| BBSplusSecretKey::from_bytes(&bytes).context("invalid key size")) + }) + .transpose() + .map_err(|e| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(e))?; + + let x = jwu::decode_b64(¶ms.x) + .context("`x` parameter is not base64 encoded") + .and_then(|bytes| bytes.try_into().ok().context("invalid coordinate size")) + .map_err(|e| KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType).with_source(e))?; + let y = jwu::decode_b64(¶ms.y) + .context("`y` parameter is not base64 encoded") + .and_then(|bytes| bytes.try_into().ok().context("invalid coordinate size")) + .map_err(|e| KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType).with_source(e))?; + + let pk = BBSplusPublicKey::from_coordinates(&x, &y).map_err(|e| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_source(e) + .with_custom_message("invalid BBS+ public key".to_owned()) + })?; + + Ok((sk, pk)) +} + +fn _sign_bbs( + data: &[Vec], + sk: &BBSplusSecretKey, + pk: &BBSplusPublicKey, + header: &[u8], +) -> Result, zkryptium::errors::Error> +where + S: BbsCiphersuite, +{ + Signature::>::sign(Some(data), sk, pk, Some(header)).map(|s| s.to_bytes().to_vec()) +} + +/// Signs data and header using the given keys. +pub fn sign_bbs( + alg: ProofAlgorithm, + data: &[Vec], + sk: &BBSplusSecretKey, + pk: &BBSplusPublicKey, + header: &[u8], +) -> KeyStorageResult> { + match alg { + ProofAlgorithm::BLS12381_SHA256 => _sign_bbs::(data, sk, pk, header), + ProofAlgorithm::BLS12381_SHAKE256 => _sign_bbs::(data, sk, pk, header), + _ => return Err(KeyStorageErrorKind::UnsupportedProofAlgorithm.into()), + } + .map_err(|e| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_source(e) + .with_custom_message("signature failed".to_owned()) + }) +} + +fn _update_bbs_signature( + sig: &[u8; 80], + sk: &BBSplusSecretKey, + update_ctx: &ProofUpdateCtx, +) -> Result<[u8; 80], zkryptium::errors::Error> +where + S: BbsCiphersuite, +{ + let sig = Signature::>::from_bytes(sig)?; + 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, + } = update_ctx; + let half_updated = sig.update_signature( + sk, + old_start_validity_timeframe, + new_start_validity_timeframe, + *index_start_validity_timeframe, + *number_of_signed_messages, + )?; + half_updated + .update_signature( + sk, + old_end_validity_timeframe, + new_end_validity_timeframe, + *index_end_validity_timeframe, + *number_of_signed_messages, + ) + .map(|sig| sig.to_bytes()) +} + +/// Updates BBS+ signature's timeframe data. +pub fn update_bbs_signature( + alg: ProofAlgorithm, + sig: &[u8], + sk: &BBSplusSecretKey, + update_ctx: &ProofUpdateCtx, +) -> KeyStorageResult> { + let exact_size_signature = sig.try_into().map_err(|_| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message("invalid signature size".to_owned()) + })?; + match alg { + ProofAlgorithm::BLS12381_SHA256 => _update_bbs_signature::(exact_size_signature, sk, update_ctx), + ProofAlgorithm::BLS12381_SHAKE256 => { + _update_bbs_signature::(exact_size_signature, sk, update_ctx) + } + _ => return Err(KeyStorageErrorKind::UnsupportedProofAlgorithm.into()), + } + .map(Vec::from) + .map_err(|e| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message("signature failed") + .with_source(e) + }) +} diff --git a/identity_storage/src/key_storage/ed25519.rs b/identity_storage/src/key_storage/ed25519.rs index c8750e1f39..619493c35d 100644 --- a/identity_storage/src/key_storage/ed25519.rs +++ b/identity_storage/src/key_storage/ed25519.rs @@ -53,6 +53,6 @@ pub(crate) fn encode_jwk(private_key: &SecretKey, public_key: &crypto::signature let mut params = JwkParamsOkp::new(); params.x = x; params.d = Some(d); - params.crv = EdCurve::Ed25519.name().to_owned(); + params.crv = EdCurve::Ed25519.name().to_string(); Jwk::from_params(params) } diff --git a/identity_storage/src/key_storage/jwk_storage_bbs_plus_ext.rs b/identity_storage/src/key_storage/jwk_storage_bbs_plus_ext.rs new file mode 100644 index 0000000000..276c39d4cb --- /dev/null +++ b/identity_storage/src/key_storage/jwk_storage_bbs_plus_ext.rs @@ -0,0 +1,40 @@ +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +use async_trait::async_trait; +use identity_verification::jwk::Jwk; +use jsonprooftoken::jpa::algs::ProofAlgorithm; + +use crate::JwkGenOutput; +use crate::JwkStorage; +use crate::KeyId; +use crate::KeyStorageResult; +use crate::KeyType; +use crate::ProofUpdateCtx; + +/// Extension to the JwkStorage to handle BBS+ keys +#[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] +#[cfg_attr(feature = "send-sync-storage", async_trait)] +pub trait JwkStorageBbsPlusExt: JwkStorage { + /// Generates a JWK representing a BBS+ signature + async fn generate_bbs(&self, key_type: KeyType, alg: ProofAlgorithm) -> KeyStorageResult; + + /// Sign the provided `data` and `header` using the private key identified by `key_id` according to the requirements + /// of the corresponding `public_key` (see [`Jwk::alg`](Jwk::alg()) etc.). + async fn sign_bbs( + &self, + key_id: &KeyId, + data: &[Vec], + header: &[u8], + public_key: &Jwk, + ) -> KeyStorageResult>; + + /// Update proof functionality for timeframe revocation mechanism + async fn update_signature( + &self, + key_id: &KeyId, + public_key: &Jwk, + signature: &[u8], + ctx: ProofUpdateCtx, + ) -> KeyStorageResult>; +} diff --git a/identity_storage/src/key_storage/key_storage_error.rs b/identity_storage/src/key_storage/key_storage_error.rs index 59ff10968e..060d0794a6 100644 --- a/identity_storage/src/key_storage/key_storage_error.rs +++ b/identity_storage/src/key_storage/key_storage_error.rs @@ -23,6 +23,9 @@ pub enum KeyStorageErrorKind { /// Indicates an attempt to parse a signature algorithm that is not recognized by the key storage implementation. UnsupportedSignatureAlgorithm, + /// Indicates an attempt to parse a proof algorithm that is not recognized by the key storage implementation. + UnsupportedProofAlgorithm, + /// Indicates that the key storage implementation is not able to find the requested key. KeyNotFound, @@ -59,6 +62,7 @@ impl KeyStorageErrorKind { Self::UnsupportedKeyType => "key generation failed: the provided multikey schema is not supported", Self::KeyAlgorithmMismatch => "the key type cannot be used with the algorithm", Self::UnsupportedSignatureAlgorithm => "signing algorithm parsing failed", + Self::UnsupportedProofAlgorithm => "proof algorithm parsing failed", Self::KeyNotFound => "key not found in storage", Self::Unavailable => "key storage unavailable", Self::Unauthenticated => "authentication with the key storage failed", diff --git a/identity_storage/src/key_storage/memstore.rs b/identity_storage/src/key_storage/memstore.rs index f101af4759..9bf4e6ea9a 100644 --- a/identity_storage/src/key_storage/memstore.rs +++ b/identity_storage/src/key_storage/memstore.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2023 IOTA Stiftung +// Copyright 2020-2023 IOTA Stiftung, Fondazione Links // SPDX-License-Identifier: Apache-2.0 use core::fmt::Debug; @@ -12,6 +12,7 @@ use identity_verification::jose::jwk::EdCurve; use identity_verification::jose::jwk::Jwk; use identity_verification::jose::jwk::JwkType; use identity_verification::jose::jws::JwsAlgorithm; +use identity_verification::jwk::BlsCurve; use rand::distributions::DistString; use shared::Shared; use tokio::sync::RwLockReadGuard; @@ -66,6 +67,12 @@ impl JwkStorage for JwkMemStore { let public_key = private_key.public_key(); (private_key, public_key) } + other => { + return Err( + KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) + .with_custom_message(format!("{other} is not supported")), + ); + } }; let kid: KeyId = random_key_id(); @@ -183,18 +190,24 @@ impl JwkStorage for JwkMemStore { #[derive(Debug, Copy, Clone)] enum MemStoreKeyType { Ed25519, + BLS12381G2, } impl JwkMemStore { const ED25519_KEY_TYPE_STR: &'static str = "Ed25519"; /// The Ed25519 key type. pub const ED25519_KEY_TYPE: KeyType = KeyType::from_static_str(Self::ED25519_KEY_TYPE_STR); + + const BLS12381G2_KEY_TYPE_STR: &'static str = "BLS12381G2"; + /// The BLS12381G2 key type + pub const BLS12381G2_KEY_TYPE: KeyType = KeyType::from_static_str(Self::BLS12381G2_KEY_TYPE_STR); } impl MemStoreKeyType { const fn name(&self) -> &'static str { match self { - MemStoreKeyType::Ed25519 => "Ed25519", + MemStoreKeyType::Ed25519 => JwkMemStore::ED25519_KEY_TYPE_STR, + MemStoreKeyType::BLS12381G2 => JwkMemStore::BLS12381G2_KEY_TYPE_STR, } } } @@ -211,6 +224,7 @@ impl TryFrom<&KeyType> for MemStoreKeyType { fn try_from(value: &KeyType) -> Result { match value.as_str() { JwkMemStore::ED25519_KEY_TYPE_STR => Ok(MemStoreKeyType::Ed25519), + JwkMemStore::BLS12381G2_KEY_TYPE_STR => Ok(MemStoreKeyType::BLS12381G2), _ => Err(KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType)), } } @@ -239,6 +253,24 @@ impl TryFrom<&Jwk> for MemStoreKeyType { ), } } + JwkType::Ec => { + let ec_params = jwk.try_ec_params().map_err(|err| { + KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) + .with_custom_message("expected EC parameters for a JWK with `kty` Ec") + .with_source(err) + })?; + match ec_params.try_bls_curve().map_err(|err| { + KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) + .with_custom_message("only Ed curves are supported for signing") + .with_source(err) + })? { + BlsCurve::BLS12381G2 => Ok(MemStoreKeyType::BLS12381G2), + curve => Err( + KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) + .with_custom_message(format!("{curve} not supported")), + ), + } + } other => Err( KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) .with_custom_message(format!("Jwk `kty` {other} not supported")), @@ -269,6 +301,125 @@ fn check_key_alg_compatibility(key_type: MemStoreKeyType, alg: JwsAlgorithm) -> } } +#[cfg(feature = "jpt-bbs-plus")] +mod bbs_plus_impl { + use std::str::FromStr as _; + + use crate::key_storage::bls::encode_bls_jwk; + use crate::key_storage::bls::expand_bls_jwk; + use crate::key_storage::bls::generate_bbs_keypair; + use crate::key_storage::bls::sign_bbs; + use crate::key_storage::bls::update_bbs_signature; + use crate::JwkGenOutput; + use crate::JwkMemStore; + use crate::JwkStorageBbsPlusExt; + use crate::KeyId; + use crate::KeyStorageError; + use crate::KeyStorageErrorKind; + use crate::KeyStorageResult; + use crate::KeyType; + use crate::ProofUpdateCtx; + use async_trait::async_trait; + use identity_verification::jwk::BlsCurve; + use identity_verification::jwk::Jwk; + use jsonprooftoken::jpa::algs::ProofAlgorithm; + + use super::random_key_id; + + /// JwkStorageBbsPlusExt implementation for JwkMemStore + #[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] + #[cfg_attr(feature = "send-sync-storage", async_trait)] + impl JwkStorageBbsPlusExt for JwkMemStore { + async fn generate_bbs(&self, key_type: KeyType, alg: ProofAlgorithm) -> KeyStorageResult { + if key_type != JwkMemStore::BLS12381G2_KEY_TYPE { + return Err( + KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) + .with_custom_message(format!("unsupported key type {key_type}")), + ); + } + + let (private_key, public_key) = generate_bbs_keypair(alg)?; + let (jwk, public_jwk) = encode_bls_jwk(&private_key, &public_key, alg); + + let kid: KeyId = random_key_id(); + let mut jwk_store = self.jwk_store.write().await; + jwk_store.insert(kid.clone(), jwk); + + Ok(JwkGenOutput::new(kid, public_jwk)) + } + + async fn sign_bbs( + &self, + key_id: &KeyId, + data: &[Vec], + header: &[u8], + public_key: &Jwk, + ) -> KeyStorageResult> { + let jwk_store = self.jwk_store.read().await; + + // Extract the required alg from the given public key + let alg = public_key + .alg() + .and_then(|alg_str| ProofAlgorithm::from_str(alg_str).ok()) + .ok_or(KeyStorageErrorKind::UnsupportedProofAlgorithm)?; + + // Check the provided JWK represents a BLS12381G2 key. + if !public_key + .try_ec_params() + .map(|ec| ec.crv == BlsCurve::BLS12381G2.to_string()) + .unwrap_or(false) + { + return Err( + KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) + .with_custom_message(format!("expected a key from the {} curve", BlsCurve::BLS12381G2)), + ); + } + + // Obtain the corresponding private key. + let jwk: &Jwk = jwk_store.get(key_id).ok_or(KeyStorageErrorKind::KeyNotFound)?; + let (sk, pk) = expand_bls_jwk(jwk)?; + + sign_bbs(alg, data, &sk.expect("jwk is private"), &pk, header) + } + + async fn update_signature( + &self, + key_id: &KeyId, + public_key: &Jwk, + signature: &[u8], + ctx: ProofUpdateCtx, + ) -> KeyStorageResult> { + let jwk_store = self.jwk_store.read().await; + + // 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) + })?; + + // Check the provided JWK represents a BLS12381G2 key. + if !public_key + .try_ec_params() + .map(|ec| ec.crv == BlsCurve::BLS12381G2.to_string()) + .unwrap_or(false) + { + return Err( + KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) + .with_custom_message(format!("expected a key from the {} curve", BlsCurve::BLS12381G2)), + ); + } + + // Obtain the corresponding private key. + let jwk = jwk_store.get(key_id).ok_or(KeyStorageErrorKind::KeyNotFound)?; + let sk = expand_bls_jwk(jwk)?.0.expect("jwk is private"); + + // Update the signature. + update_bbs_signature(alg, signature, &sk, &ctx) + } + } +} pub(crate) mod shared { use core::fmt::Debug; use core::fmt::Formatter; @@ -362,10 +513,10 @@ mod tests { let store: JwkMemStore = JwkMemStore::new(); let mut ec_params = JwkParamsEc::new(); - ec_params.crv = EcCurve::P256.name().to_owned(); - ec_params.x = "".to_owned(); - ec_params.y = "".to_owned(); - ec_params.d = Some("".to_owned()); + ec_params.crv = EcCurve::P256.name().to_string(); + ec_params.x = String::new(); + ec_params.y = String::new(); + ec_params.d = Some(String::new()); let jwk_ec = Jwk::from_params(ec_params); let err = store.insert(jwk_ec).await.unwrap_err(); diff --git a/identity_storage/src/key_storage/mod.rs b/identity_storage/src/key_storage/mod.rs index 70006b0537..f54f9d5233 100644 --- a/identity_storage/src/key_storage/mod.rs +++ b/identity_storage/src/key_storage/mod.rs @@ -6,10 +6,15 @@ //! This module provides the [`JwkStorage`] trait that //! abstracts over storages that store JSON Web Keys. +#[cfg(feature = "jpt-bbs-plus")] +/// BLS12381 utils. +pub mod bls; #[cfg(feature = "memstore")] mod ed25519; mod jwk_gen_output; mod jwk_storage; +#[cfg(feature = "jpt-bbs-plus")] +mod jwk_storage_bbs_plus_ext; mod key_id; mod key_storage_error; mod key_type; @@ -19,10 +24,17 @@ mod memstore; #[cfg(test)] pub(crate) mod tests; -pub use jwk_gen_output::*; -pub use jwk_storage::*; -pub use key_id::*; -pub use key_storage_error::*; -pub use key_type::*; -#[cfg(feature = "memstore")] -pub use memstore::*; +/// All modules that should be made available to end-users. +pub mod public_modules { + pub use super::jwk_gen_output::*; + pub use super::jwk_storage::*; + #[cfg(feature = "jpt-bbs-plus")] + pub use super::jwk_storage_bbs_plus_ext::*; + pub use super::key_id::*; + pub use super::key_storage_error::*; + pub use super::key_type::*; + #[cfg(feature = "memstore")] + pub use super::memstore::*; +} + +pub use public_modules::*; diff --git a/identity_storage/src/key_storage/tests/utils.rs b/identity_storage/src/key_storage/tests/utils.rs index 379df562b4..b5ca210301 100644 --- a/identity_storage/src/key_storage/tests/utils.rs +++ b/identity_storage/src/key_storage/tests/utils.rs @@ -45,10 +45,10 @@ pub(crate) async fn test_incompatible_key_alg(store: impl JwkStorage) { pub(crate) async fn test_incompatible_key_type(store: impl JwkStorage) { let mut ec_params = JwkParamsEc::new(); - ec_params.crv = EcCurve::P256.name().to_owned(); - ec_params.x = "".to_owned(); - ec_params.y = "".to_owned(); - ec_params.d = Some("".to_owned()); + ec_params.crv = EcCurve::P256.name().to_string(); + ec_params.x = String::new(); + ec_params.y = String::new(); + ec_params.d = Some(String::new()); let jwk_ec = Jwk::from_params(ec_params); let err = store.insert(jwk_ec).await.unwrap_err(); diff --git a/identity_storage/src/lib.rs b/identity_storage/src/lib.rs index da1b0b66f4..643e1e7444 100644 --- a/identity_storage/src/lib.rs +++ b/identity_storage/src/lib.rs @@ -19,5 +19,5 @@ pub mod key_storage; pub mod storage; pub use key_id_storage::*; -pub use key_storage::*; +pub use key_storage::public_modules::*; pub use storage::*; diff --git a/identity_storage/src/storage/error.rs b/identity_storage/src/storage/error.rs index 7abac68286..a5d8d11185 100644 --- a/identity_storage/src/storage/error.rs +++ b/identity_storage/src/storage/error.rs @@ -27,6 +27,16 @@ pub enum JwkStorageDocumentError { /// Caused by an invalid JWS algorithm. #[error("invalid JWS algorithm")] InvalidJwsAlgorithm, + /// Caused by an invalid JWP algorithm. + #[error("invalid JWP algorithm")] + InvalidJwpAlgorithm, + /// Cannot cunstruct a valid Jwp (issued or presented form) + #[error("Not able to construct a valid Jwp")] + JwpBuildingError, + /// Credential's proof update internal error + #[error("Credential's proof internal error")] + ProofUpdateError(String), + /// Caused by a failure to construct a verification method. #[error("method generation failed: unable to create a valid verification method")] VerificationMethodConstructionError(#[source] identity_verification::Error), diff --git a/identity_storage/src/storage/jwk_document_ext.rs b/identity_storage/src/storage/jwk_document_ext.rs index 8b412a285a..f9ee100986 100644 --- a/identity_storage/src/storage/jwk_document_ext.rs +++ b/identity_storage/src/storage/jwk_document_ext.rs @@ -153,20 +153,20 @@ mod private { // copious amounts of repetition. // NOTE: If such use of macros becomes very common it is probably better to use the duplicate crate: https://docs.rs/duplicate/latest/duplicate/ macro_rules! generate_method_for_document_type { - ($t:ty, $name:ident) => { + ($t:ty, $a:ty, $k:path, $f:path, $name:ident) => { async fn $name( document: &mut $t, storage: &Storage, key_type: KeyType, - alg: JwsAlgorithm, + alg: $a, fragment: Option<&str>, scope: MethodScope, ) -> StorageResult where - K: JwkStorage, + K: $k, I: KeyIdStorage, { - let JwkGenOutput { key_id, jwk } = ::generate(&storage.key_storage(), key_type, alg) + let JwkGenOutput { key_id, jwk } = $f(storage.key_storage(), key_type, alg) .await .map_err(Error::KeyStorageError)?; @@ -304,7 +304,13 @@ macro_rules! purge_method_for_document_type { // CoreDocument // ==================================================================================================================== -generate_method_for_document_type!(CoreDocument, generate_method_core_document); +generate_method_for_document_type!( + CoreDocument, + JwsAlgorithm, + JwkStorage, + JwkStorage::generate, + generate_method_core_document +); purge_method_for_document_type!(CoreDocument, purge_method_core_document); #[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] @@ -505,7 +511,7 @@ impl JwkDocumentExt for CoreDocument { /// Attempt to revert key generation. If this succeeds the original `source_error` is returned, /// otherwise [`JwkStorageDocumentError::UndoOperationFailed`] is returned with the `source_error` attached as /// `source`. -async fn try_undo_key_generation(storage: &Storage, key_id: &KeyId, source_error: Error) -> Error +pub(crate) async fn try_undo_key_generation(storage: &Storage, key_id: &KeyId, source_error: Error) -> Error where K: JwkStorage, I: KeyIdStorage, @@ -531,7 +537,13 @@ mod iota_document { use identity_credential::credential::Jwt; use identity_iota_core::IotaDocument; - generate_method_for_document_type!(IotaDocument, generate_method_iota_document); + generate_method_for_document_type!( + IotaDocument, + JwsAlgorithm, + JwkStorage, + JwkStorage::generate, + generate_method_iota_document + ); purge_method_for_document_type!(IotaDocument, purge_method_iota_document); #[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] diff --git a/identity_storage/src/storage/jwp_document_ext.rs b/identity_storage/src/storage/jwp_document_ext.rs new file mode 100644 index 0000000000..21ef7fafaa --- /dev/null +++ b/identity_storage/src/storage/jwp_document_ext.rs @@ -0,0 +1,362 @@ +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +use super::JwkStorageDocumentError as Error; +use crate::key_id_storage::MethodDigest; +use crate::try_undo_key_generation; +use crate::JwkGenOutput; +use crate::JwkStorageBbsPlusExt; +use crate::KeyIdStorage; +use crate::KeyType; +use crate::Storage; +use crate::StorageResult; +use async_trait::async_trait; +use identity_core::common::Object; +use identity_core::convert::ToJson; +use identity_credential::credential::Credential; +use identity_credential::credential::Jpt; +use identity_credential::credential::JwpCredentialOptions; +use identity_credential::presentation::JwpPresentationOptions; +use identity_credential::presentation::SelectiveDisclosurePresentation; +use identity_did::DIDUrl; +use identity_document::document::CoreDocument; +use identity_verification::MethodData; +use identity_verification::MethodScope; +use identity_verification::VerificationMethod; +use jsonprooftoken::encoding::SerializationType; +use jsonprooftoken::jpa::algs::ProofAlgorithm; +use jsonprooftoken::jpt::claims::JptClaims; +use jsonprooftoken::jwk::key::Jwk; +use jsonprooftoken::jwp::header::IssuerProtectedHeader; +use jsonprooftoken::jwp::header::PresentationProtectedHeader; +use jsonprooftoken::jwp::issued::JwpIssuedBuilder; +use serde::de::DeserializeOwned; +use serde::Serialize; + +/// Handle JWP-based operations on DID Documents. +#[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] +#[cfg_attr(feature = "send-sync-storage", async_trait)] +pub trait JwpDocumentExt { + /// Generate new key material in the given `storage` and insert a new verification method with the corresponding + /// public key material into the DID document. This supports BBS+ keys. + async fn generate_method_jwp( + &mut self, + storage: &Storage, + key_type: KeyType, + alg: ProofAlgorithm, + fragment: Option<&str>, + scope: MethodScope, + ) -> StorageResult + where + K: JwkStorageBbsPlusExt, + I: KeyIdStorage; + + /// Compute a JWP in the Issued form representing the Verifiable Credential + /// See [JSON Web Proof draft](https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-proof#name-issued-form) + async fn create_issued_jwp( + &self, + storage: &Storage, + fragment: &str, + jpt_claims: &JptClaims, + options: &JwpCredentialOptions, + ) -> StorageResult + where + K: JwkStorageBbsPlusExt, + I: KeyIdStorage; + + /// Compute a JWP in the Presented form representing the presented Verifiable Credential after the Selective + /// Disclosure of attributes See [JSON Web Proof draft](https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-proof#name-presented-form) + async fn create_presented_jwp( + &self, + presentation: &mut SelectiveDisclosurePresentation, + method_id: &str, + options: &JwpPresentationOptions, + ) -> StorageResult; + + /// Produces a JPT where the payload is produced from the given `credential`. + async fn create_credential_jpt( + &self, + credential: &Credential, + storage: &Storage, + fragment: &str, + options: &JwpCredentialOptions, + custom_claims: Option, + ) -> StorageResult + where + K: JwkStorageBbsPlusExt, + I: KeyIdStorage, + T: ToOwned + Serialize + DeserializeOwned + Sync; + + /// Produces a JPT where the payload contains the Selective Disclosed attributes of a `credential`. + async fn create_presentation_jpt( + &self, + presentation: &mut SelectiveDisclosurePresentation, + method_id: &str, + options: &JwpPresentationOptions, + ) -> StorageResult; +} + +// ==================================================================================================================== +// CoreDocument +// ==================================================================================================================== + +generate_method_for_document_type!( + CoreDocument, + ProofAlgorithm, + JwkStorageBbsPlusExt, + JwkStorageBbsPlusExt::generate_bbs, + generate_method_core_document +); + +#[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] +#[cfg_attr(feature = "send-sync-storage", async_trait)] +impl JwpDocumentExt for CoreDocument { + async fn generate_method_jwp( + &mut self, + storage: &Storage, + key_type: KeyType, + alg: ProofAlgorithm, + fragment: Option<&str>, + scope: MethodScope, + ) -> StorageResult + where + K: JwkStorageBbsPlusExt, + I: KeyIdStorage, + { + generate_method_core_document(self, storage, key_type, alg, fragment, scope).await + } + + async fn create_issued_jwp( + &self, + storage: &Storage, + fragment: &str, + jpt_claims: &JptClaims, + options: &JwpCredentialOptions, + ) -> StorageResult + where + K: JwkStorageBbsPlusExt, + I: KeyIdStorage, + { + // Obtain the method corresponding to the given fragment. + let method: &VerificationMethod = self.resolve_method(fragment, None).ok_or(Error::MethodNotFound)?; + let MethodData::PublicKeyJwk(ref jwk) = method.data() else { + return Err(Error::NotPublicKeyJwk); + }; + + // Extract JwsAlgorithm. + let alg: ProofAlgorithm = jwk + .alg() + .unwrap_or("") + .parse() + .map_err(|_| Error::InvalidJwpAlgorithm)?; + + let typ = "JPT".to_string(); + + let kid = if let Some(ref kid) = options.kid { + kid.clone() + } else { + method.id().to_string() + }; + + let mut issuer_header = IssuerProtectedHeader::new(alg); + issuer_header.set_typ(Some(typ)); + issuer_header.set_kid(Some(kid)); + + // Get the key identifier corresponding to the given method from the KeyId storage. + let method_digest: MethodDigest = MethodDigest::new(method).map_err(Error::MethodDigestConstructionError)?; + let key_id = ::get_key_id(storage.key_id_storage(), &method_digest) + .await + .map_err(Error::KeyIdStorageError)?; + + let jwp_builder = JwpIssuedBuilder::new(issuer_header, jpt_claims.clone()); + + let header = jwp_builder.get_issuer_protected_header().map_or_else( + || Err(Error::JwpBuildingError), + |h| h.to_json_vec().map_err(|_| Error::JwpBuildingError), + )?; + + let data = jwp_builder.get_payloads().map_or_else( + || Err(Error::JwpBuildingError), + |p| p.to_bytes().map_err(|_| Error::JwpBuildingError), + )?; + + let signature = ::sign_bbs(storage.key_storage(), &key_id, &data, &header, jwk) + .await + .map_err(Error::KeyStorageError)?; + + jwp_builder + .build_with_proof(signature) + .map_err(|_| Error::JwpBuildingError)? + .encode(SerializationType::COMPACT) + .map_err(|err| Error::EncodingError(Box::new(err))) + } + + async fn create_presented_jwp( + &self, + presentation: &mut SelectiveDisclosurePresentation, + method_id: &str, + options: &JwpPresentationOptions, + ) -> StorageResult { + // Obtain the method corresponding to the given fragment. + let method: &VerificationMethod = self.resolve_method(method_id, None).ok_or(Error::MethodNotFound)?; + let MethodData::PublicKeyJwk(ref jwk) = method.data() else { + return Err(Error::NotPublicKeyJwk); + }; + + // Extract JwsAlgorithm. + let alg: ProofAlgorithm = jwk + .alg() + .unwrap_or("") + .parse() + .map_err(|_| Error::InvalidJwpAlgorithm)?; + + let public_key: Jwk = jwk.try_into().map_err(|_| Error::NotPublicKeyJwk)?; + + let mut presentation_header = PresentationProtectedHeader::new(alg.into()); + presentation_header.set_nonce(options.nonce.clone()); + presentation_header.set_aud(options.audience.as_ref().map(|u| u.to_string())); + + presentation.set_presentation_header(presentation_header); + + let jwp_builder = presentation.builder(); + + let presented_jwp = jwp_builder.build(&public_key).map_err(|_| Error::JwpBuildingError)?; + + Ok( + presented_jwp + .encode(SerializationType::COMPACT) + .map_err(|e| Error::EncodingError(Box::new(e)))?, + ) + } + + async fn create_credential_jpt( + &self, + credential: &Credential, + storage: &Storage, + fragment: &str, + options: &JwpCredentialOptions, + custom_claims: Option, + ) -> StorageResult + where + K: JwkStorageBbsPlusExt, + I: KeyIdStorage, + T: ToOwned + Serialize + DeserializeOwned + Sync, + { + let jpt_claims = credential + .serialize_jpt(custom_claims) + .map_err(Error::ClaimsSerializationError)?; + + self + .create_issued_jwp(storage, fragment, &jpt_claims, options) + .await + .map(Jpt::new) + } + + async fn create_presentation_jpt( + &self, + presentation: &mut SelectiveDisclosurePresentation, + method_id: &str, + options: &JwpPresentationOptions, + ) -> StorageResult { + self + .create_presented_jwp(presentation, method_id, options) + .await + .map(Jpt::new) + } +} + +// ==================================================================================================================== +// IotaDocument +// ==================================================================================================================== +#[cfg(feature = "iota-document")] +mod iota_document { + use super::*; + use identity_iota_core::IotaDocument; + + generate_method_for_document_type!( + IotaDocument, + ProofAlgorithm, + JwkStorageBbsPlusExt, + JwkStorageBbsPlusExt::generate_bbs, + generate_method_iota_document + ); + + #[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] + #[cfg_attr(feature = "send-sync-storage", async_trait)] + impl JwpDocumentExt for IotaDocument { + async fn generate_method_jwp( + &mut self, + storage: &Storage, + key_type: KeyType, + alg: ProofAlgorithm, + fragment: Option<&str>, + scope: MethodScope, + ) -> StorageResult + where + K: JwkStorageBbsPlusExt, + I: KeyIdStorage, + { + generate_method_iota_document(self, storage, key_type, alg, fragment, scope).await + } + + async fn create_issued_jwp( + &self, + storage: &Storage, + fragment: &str, + jpt_claims: &JptClaims, + options: &JwpCredentialOptions, + ) -> StorageResult + where + K: JwkStorageBbsPlusExt, + I: KeyIdStorage, + { + self + .core_document() + .create_issued_jwp(storage, fragment, jpt_claims, options) + .await + } + + async fn create_presented_jwp( + &self, + presentation: &mut SelectiveDisclosurePresentation, + method_id: &str, + options: &JwpPresentationOptions, + ) -> StorageResult { + self + .core_document() + .create_presented_jwp(presentation, method_id, options) + .await + } + + async fn create_credential_jpt( + &self, + credential: &Credential, + storage: &Storage, + fragment: &str, + options: &JwpCredentialOptions, + custom_claims: Option, + ) -> StorageResult + where + K: JwkStorageBbsPlusExt, + I: KeyIdStorage, + T: ToOwned + Serialize + DeserializeOwned + Sync, + { + self + .core_document() + .create_credential_jpt(credential, storage, fragment, options, custom_claims) + .await + } + + async fn create_presentation_jpt( + &self, + presentation: &mut SelectiveDisclosurePresentation, + method_id: &str, + options: &JwpPresentationOptions, + ) -> StorageResult { + self + .core_document() + .create_presentation_jpt(presentation, method_id, options) + .await + } + } +} diff --git a/identity_storage/src/storage/mod.rs b/identity_storage/src/storage/mod.rs index efbdc28cbb..7643c41a95 100644 --- a/identity_storage/src/storage/mod.rs +++ b/identity_storage/src/storage/mod.rs @@ -4,14 +4,25 @@ //! This module provides a type wrapping a key and key id storage. mod error; +#[macro_use] mod jwk_document_ext; +#[cfg(feature = "jpt-bbs-plus")] +mod jwp_document_ext; mod signature_options; +#[cfg(feature = "jpt-bbs-plus")] +mod timeframe_revocation_ext; + #[cfg(all(test, feature = "memstore"))] pub(crate) mod tests; pub use error::*; + pub use jwk_document_ext::*; +#[cfg(feature = "jpt-bbs-plus")] +pub use jwp_document_ext::*; pub use signature_options::*; +#[cfg(feature = "jpt-bbs-plus")] +pub use timeframe_revocation_ext::*; /// A type wrapping a key and key id storage, typically used with [`JwkStorage`](crate::key_storage::JwkStorage) and /// [`KeyIdStorage`](crate::key_id_storage::KeyIdStorage) that should always be used together when calling methods from diff --git a/identity_storage/src/storage/tests/kb_jwt.rs b/identity_storage/src/storage/tests/kb_jwt.rs index 5ea27ed5b9..e7451050fd 100644 --- a/identity_storage/src/storage/tests/kb_jwt.rs +++ b/identity_storage/src/storage/tests/kb_jwt.rs @@ -57,12 +57,8 @@ async fn setup_test() -> (Setup, Credential, SdJwt) let mut encoder = SdObjectEncoder::new(&payload).unwrap(); let disclosures = vec![ - encoder - .conceal(&["vc", "credentialSubject", "degree", "type"], None) - .unwrap(), - encoder - .conceal(&["vc", "credentialSubject", "degree", "name"], None) - .unwrap(), + encoder.conceal("/vc/credentialSubject/degree/type", None).unwrap(), + encoder.conceal("/vc/credentialSubject/degree/name", None).unwrap(), ]; encoder.add_sd_alg_property(); let encoded_payload = encoder.try_to_string().unwrap(); diff --git a/identity_storage/src/storage/tests/test_utils.rs b/identity_storage/src/storage/tests/test_utils.rs index ebc0660147..77b1a92072 100644 --- a/identity_storage/src/storage/tests/test_utils.rs +++ b/identity_storage/src/storage/tests/test_utils.rs @@ -192,7 +192,7 @@ pub(crate) fn encode_public_ed25519_jwk(public_key: &PublicKey) -> Jwk { let mut params = JwkParamsOkp::new(); params.x = x; params.d = None; - params.crv = EdCurve::Ed25519.name().to_owned(); + params.crv = EdCurve::Ed25519.name().to_string(); let mut jwk = Jwk::from_params(params); jwk.set_alg(JwsAlgorithm::EdDSA.name()); jwk diff --git a/identity_storage/src/storage/timeframe_revocation_ext.rs b/identity_storage/src/storage/timeframe_revocation_ext.rs new file mode 100644 index 0000000000..f53f2a9639 --- /dev/null +++ b/identity_storage/src/storage/timeframe_revocation_ext.rs @@ -0,0 +1,198 @@ +// Copyright 2020-2024 IOTA Stiftung, Fondazione Links +// SPDX-License-Identifier: Apache-2.0 + +use super::JwkStorageDocumentError as Error; +use crate::JwkStorageBbsPlusExt; +use crate::KeyIdStorage; +use crate::MethodDigest; +use crate::Storage; +use crate::StorageResult; +use async_trait::async_trait; +use identity_core::common::Duration; +use identity_core::common::Timestamp; +use identity_credential::credential::Jpt; +use identity_credential::revocation::RevocationTimeframeStatus; +use identity_document::document::CoreDocument; +use identity_verification::MethodData; +use identity_verification::VerificationMethod; +use jsonprooftoken::encoding::SerializationType; +use jsonprooftoken::jpt::payloads::Payloads; +use jsonprooftoken::jwp::issued::JwpIssued; +use serde_json::Value; +use zkryptium::bbsplus::signature::BBSplusSignature; + +/// Contains information needed to update the signature in the RevocationTimeframe2024 revocation mechanism. +pub struct ProofUpdateCtx { + /// 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, +} + +/// CoreDocument and IotaDocument extension to handle Credential' signature update for RevocationTimeframe2024 +#[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] +#[cfg_attr(feature = "send-sync-storage", async_trait)] +pub trait TimeframeRevocationExtension { + /// Update Credential' signature considering the Timeframe interval + async fn update( + &self, + storage: &Storage, + fragment: &str, + start_validity: Option, + duration: Duration, + credential_jwp: &mut JwpIssued, + ) -> StorageResult + where + K: JwkStorageBbsPlusExt, + I: KeyIdStorage; +} + +// ==================================================================================================================== +// CoreDocument +// ==================================================================================================================== + +#[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] +#[cfg_attr(feature = "send-sync-storage", async_trait)] +impl TimeframeRevocationExtension for CoreDocument { + async fn update( + &self, + storage: &Storage, + fragment: &str, + start_validity: Option, + duration: Duration, + credential_jwp: &mut JwpIssued, + ) -> StorageResult + where + K: JwkStorageBbsPlusExt, + I: KeyIdStorage, + { + // Obtain the method corresponding to the given fragment. + let method: &VerificationMethod = self.resolve_method(fragment, None).ok_or(Error::MethodNotFound)?; + let MethodData::PublicKeyJwk(ref jwk) = method.data() else { + return Err(Error::NotPublicKeyJwk); + }; + + // Get the key identifier corresponding to the given method from the KeyId storage. + let method_digest: MethodDigest = MethodDigest::new(method).map_err(Error::MethodDigestConstructionError)?; + let key_id = ::get_key_id(storage.key_id_storage(), &method_digest) + .await + .map_err(Error::KeyIdStorageError)?; + + let new_start_validity_timeframe = start_validity.unwrap_or(Timestamp::now_utc()); + let new_end_validity_timeframe = new_start_validity_timeframe + .checked_add(duration) + .ok_or(Error::ProofUpdateError("Invalid granularity".to_owned()))?; + let new_start_validity_timeframe = new_start_validity_timeframe.to_rfc3339(); + let new_end_validity_timeframe = new_end_validity_timeframe.to_rfc3339(); + + let proof = credential_jwp.get_proof(); + let claims = credential_jwp + .get_claims() + .ok_or(Error::ProofUpdateError("Should not happen".to_owned()))?; + let mut payloads: Payloads = credential_jwp.get_payloads().clone(); + + let index_start_validity_timeframe = claims + .get_claim_index(format!( + "vc.credentialStatus.{}", + RevocationTimeframeStatus::START_TIMEFRAME_PROPERTY + )) + .ok_or(Error::ProofUpdateError( + "'startValidityTimeframe' property NOT found".to_owned(), + ))?; + let index_end_validity_timeframe = claims + .get_claim_index(format!( + "vc.credentialStatus.{}", + RevocationTimeframeStatus::END_TIMEFRAME_PROPERTY + )) + .ok_or(Error::ProofUpdateError( + "'endValidityTimeframe' property NOT found".to_owned(), + ))?; + + let old_start_validity_timeframe = payloads + .replace_payload_at_index( + index_start_validity_timeframe, + Value::String(new_start_validity_timeframe.clone()), + ) + .map(serde_json::from_value::) + .map_err(|_| Error::ProofUpdateError("'startValidityTimeframe' value NOT found".to_owned()))? + .map_err(|_| Error::ProofUpdateError("'startValidityTimeframe' value NOT a JSON String".to_owned()))?; + + let old_end_validity_timeframe = payloads + .replace_payload_at_index( + index_end_validity_timeframe, + Value::String(new_end_validity_timeframe.clone()), + ) + .map(serde_json::from_value::) + .map_err(|_| Error::ProofUpdateError("'endValidityTimeframe' value NOT found".to_owned()))? + .map_err(|_| Error::ProofUpdateError("'endValidityTimeframe' value NOT a JSON String".to_owned()))?; + + let proof: [u8; BBSplusSignature::BYTES] = proof + .try_into() + .map_err(|_| Error::ProofUpdateError("Invalid bytes length of JWP proof".to_owned()))?; + + let proof_update_ctx = ProofUpdateCtx { + old_start_validity_timeframe: serde_json::to_vec(&old_start_validity_timeframe).unwrap(), + new_start_validity_timeframe: serde_json::to_vec(&new_start_validity_timeframe).unwrap(), + old_end_validity_timeframe: serde_json::to_vec(&old_end_validity_timeframe).unwrap(), + new_end_validity_timeframe: serde_json::to_vec(&new_end_validity_timeframe).unwrap(), + index_start_validity_timeframe, + index_end_validity_timeframe, + number_of_signed_messages: payloads.0.len(), + }; + + let new_proof = + ::update_signature(storage.key_storage(), &key_id, jwk, &proof, proof_update_ctx) + .await + .map_err(Error::KeyStorageError)?; + + credential_jwp.set_proof(&new_proof); + credential_jwp.set_payloads(payloads); + + let jpt = credential_jwp + .encode(SerializationType::COMPACT) + .map_err(|e| Error::EncodingError(Box::new(e)))?; + + Ok(Jpt::new(jpt)) + } +} + +// ==================================================================================================================== +// IotaDocument +// ==================================================================================================================== +#[cfg(feature = "iota-document")] +mod iota_document { + use super::*; + use identity_iota_core::IotaDocument; + + #[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] + #[cfg_attr(feature = "send-sync-storage", async_trait)] + impl TimeframeRevocationExtension for IotaDocument { + async fn update( + &self, + storage: &Storage, + fragment: &str, + start_validity: Option, + duration: Duration, + credential_jwp: &mut JwpIssued, + ) -> StorageResult + where + K: JwkStorageBbsPlusExt, + I: KeyIdStorage, + { + self + .core_document() + .update(storage, fragment, start_validity, duration, credential_jwp) + .await + } + } +} diff --git a/identity_stronghold/Cargo.toml b/identity_stronghold/Cargo.toml index 02342b6f51..6fb1b82b07 100644 --- a/identity_stronghold/Cargo.toml +++ b/identity_stronghold/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "identity_stronghold" -version = "1.1.0-alpha.1" +version = "1.3.0" authors.workspace = true edition.workspace = true homepage.workspace = true @@ -13,20 +13,30 @@ description = "Secure JWK storage with Stronghold for IOTA Identity" [dependencies] async-trait = { version = "0.1.64", default-features = false } -identity_storage = { version = "=1.1.0-alpha.1", path = "../identity_storage", default_features = false } -identity_verification = { version = "=1.1.0-alpha.1", path = "../identity_verification", default_features = false } +identity_storage = { version = "=1.3.0", path = "../identity_storage", default_features = false } +identity_verification = { version = "=1.3.0", path = "../identity_verification", default_features = false } iota-crypto = { version = "0.23", default-features = false, features = ["ed25519"] } -iota-sdk = { version = "1.0.2", default-features = false, features = ["client", "stronghold"] } -iota_stronghold = { version = "2.0", default-features = false } +iota-sdk = { version = "1.1.5", default-features = false, features = ["client", "stronghold"] } +iota_stronghold = { version = "2.1.0", default-features = false } +json-proof-token = { workspace = true, optional = true } rand = { version = "0.8.5", default-features = false, features = ["std", "std_rng"] } tokio = { version = "1.29.0", default-features = false, features = ["macros", "sync"] } zeroize = { version = "1.6.0", default_features = false } +zkryptium = { workspace = true, optional = true } [dev-dependencies] -identity_did = { version = "=1.1.0-alpha.1", path = "../identity_did", default_features = false } +anyhow = "1.0.82" +identity_did = { version = "=1.3.0", path = "../identity_did", default_features = false } +identity_storage = { version = "=1.3.0", path = "../identity_storage", default_features = false, features = ["jpt-bbs-plus"] } +json-proof-token = { workspace = true } tokio = { version = "1.29.0", default-features = false, features = ["macros", "sync", "rt"] } +zkryptium = { workspace = true } [features] default = [] # Enables `Send` + `Sync` bounds for the trait implementations on `StrongholdStorage`. send-sync-storage = ["identity_storage/send-sync-storage"] +bbs-plus = ["identity_storage/jpt-bbs-plus", "dep:zkryptium", "dep:json-proof-token"] + +[lints] +workspace = true diff --git a/identity_stronghold/src/ed25519.rs b/identity_stronghold/src/ed25519.rs index 13c3135bb0..933983cdfc 100644 --- a/identity_stronghold/src/ed25519.rs +++ b/identity_stronghold/src/ed25519.rs @@ -53,6 +53,6 @@ pub(crate) fn encode_jwk(private_key: &SecretKey, public_key: &crypto::signature let mut params = JwkParamsOkp::new(); params.x = x; params.d = Some(d); - params.crv = EdCurve::Ed25519.name().to_owned(); + params.crv = EdCurve::Ed25519.name().to_string(); Jwk::from_params(params) } diff --git a/identity_stronghold/src/lib.rs b/identity_stronghold/src/lib.rs index decb2c4c00..ae8f8aef5b 100644 --- a/identity_stronghold/src/lib.rs +++ b/identity_stronghold/src/lib.rs @@ -2,9 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 pub(crate) mod ed25519; -mod stronghold_jwk_storage; -mod stronghold_key_id; +mod storage; +pub(crate) mod stronghold_key_type; #[cfg(test)] mod tests; +pub(crate) mod utils; -pub use stronghold_jwk_storage::*; +pub use storage::*; +pub use stronghold_key_type::*; diff --git a/identity_stronghold/src/storage/mod.rs b/identity_stronghold/src/storage/mod.rs new file mode 100644 index 0000000000..cb02b9274b --- /dev/null +++ b/identity_stronghold/src/storage/mod.rs @@ -0,0 +1,163 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +mod stronghold_jwk_storage; +#[cfg(any(feature = "bbs-plus", test))] +mod stronghold_jwk_storage_bbs_plus_ext; +mod stronghold_key_id; + +use std::sync::Arc; + +#[cfg(feature = "bbs-plus")] +use identity_storage::key_storage::bls::encode_bls_jwk; +use identity_storage::KeyId; +use identity_storage::KeyStorageError; +use identity_storage::KeyStorageErrorKind; +use identity_storage::KeyStorageResult; +use identity_verification::jwk::EdCurve; +use identity_verification::jwk::Jwk; +use identity_verification::jwk::JwkParamsOkp; +use identity_verification::jws::JwsAlgorithm; +use identity_verification::jwu; +use iota_sdk::client::secret::stronghold::StrongholdSecretManager; +use iota_sdk::client::secret::SecretManager; +#[cfg(feature = "bbs-plus")] +use iota_stronghold::procedures::FatalProcedureError; +use iota_stronghold::procedures::KeyType as ProceduresKeyType; +#[cfg(feature = "bbs-plus")] +use iota_stronghold::procedures::Runner as _; +use iota_stronghold::procedures::StrongholdProcedure; +use iota_stronghold::Location; +use iota_stronghold::Stronghold; +#[cfg(feature = "bbs-plus")] +use jsonprooftoken::jpa::algs::ProofAlgorithm; +use tokio::sync::MutexGuard; +#[cfg(feature = "bbs-plus")] +use zeroize::Zeroizing; +#[cfg(feature = "bbs-plus")] +use zkryptium::bbsplus::keys::BBSplusSecretKey; + +use crate::stronghold_key_type::StrongholdKeyType; +use crate::utils::get_client; +use crate::utils::IDENTITY_VAULT_PATH; + +/// Wrapper around a [`StrongholdSecretManager`] that implements the [`KeyIdStorage`](crate::KeyIdStorage) +/// and [`JwkStorage`](crate::JwkStorage) interfaces. +#[derive(Clone, Debug)] +pub struct StrongholdStorage(Arc); + +impl StrongholdStorage { + /// Creates a new [`StrongholdStorage`]. + pub fn new(stronghold_secret_manager: StrongholdSecretManager) -> Self { + Self(Arc::new(SecretManager::Stronghold(stronghold_secret_manager))) + } + + /// Shared reference to the inner [`SecretManager`]. + pub fn as_secret_manager(&self) -> &SecretManager { + self.0.as_ref() + } + + /// Acquire lock of the inner [`Stronghold`]. + pub(crate) async fn get_stronghold(&self) -> MutexGuard<'_, Stronghold> { + match *self.0 { + SecretManager::Stronghold(ref stronghold) => stronghold.inner().await, + _ => unreachable!("secret manager can be only constructed from stronghold"), + } + } + + async fn get_ed25519_public_key(&self, key_id: &KeyId) -> KeyStorageResult { + let stronghold = self.get_stronghold().await; + let client = get_client(&stronghold)?; + + let location = Location::generic( + IDENTITY_VAULT_PATH.as_bytes().to_vec(), + key_id.to_string().as_bytes().to_vec(), + ); + + let public_key_procedure = iota_stronghold::procedures::PublicKey { + ty: ProceduresKeyType::Ed25519, + private_key: location, + }; + + let procedure_result = client + .execute_procedure(StrongholdProcedure::PublicKey(public_key_procedure)) + .map_err(|err| KeyStorageError::new(KeyStorageErrorKind::KeyNotFound).with_source(err))?; + + let public_key: Vec = procedure_result.into(); + + let mut params = JwkParamsOkp::new(); + params.x = jwu::encode_b64(public_key); + EdCurve::Ed25519.name().clone_into(&mut params.crv); + let mut jwk: Jwk = Jwk::from_params(params); + jwk.set_alg(JwsAlgorithm::EdDSA.name()); + jwk.set_kid(jwk.thumbprint_sha256_b64()); + + Ok(jwk) + } + + #[cfg(feature = "bbs-plus")] + async fn get_bls12381g2_public_key(&self, key_id: &KeyId) -> KeyStorageResult { + let stronghold = self.get_stronghold().await; + let client = get_client(&stronghold)?; + + let location = Location::generic( + IDENTITY_VAULT_PATH.as_bytes().to_vec(), + key_id.to_string().as_bytes().to_vec(), + ); + + client + .get_guards([location], |[sk]| { + let sk = BBSplusSecretKey::from_bytes(&sk.borrow()).map_err(|e| FatalProcedureError::from(e.to_string()))?; + let pk = sk.public_key(); + let public_jwk = encode_bls_jwk(&sk, &pk, ProofAlgorithm::BLS12381_SHA256).1; + + drop(Zeroizing::new(sk.to_bytes())); + Ok(public_jwk) + }) + .map_err(|e| KeyStorageError::new(KeyStorageErrorKind::KeyNotFound).with_source(e)) + } + + /// Attepts to retrieve the public key corresponding to the key of id `key_id`, + /// returning it as a `key_type` encoded public JWK. + pub async fn get_public_key_with_type(&self, key_id: &KeyId, key_type: StrongholdKeyType) -> KeyStorageResult { + match key_type { + StrongholdKeyType::Ed25519 => self.get_ed25519_public_key(key_id).await, + #[cfg(feature = "bbs-plus")] + StrongholdKeyType::Bls12381G2 => self.get_bls12381g2_public_key(key_id).await, + #[allow(unreachable_patterns)] + _ => Err(KeyStorageErrorKind::UnsupportedKeyType.into()), + } + } + + /// Retrieve the public key corresponding to `key_id`. + #[deprecated(since = "1.3.0", note = "use `get_public_key_with_type` instead")] + pub async fn get_public_key(&self, key_id: &KeyId) -> KeyStorageResult { + let stronghold = self.get_stronghold().await; + let client = get_client(&stronghold)?; + + let location = Location::generic( + IDENTITY_VAULT_PATH.as_bytes().to_vec(), + key_id.to_string().as_bytes().to_vec(), + ); + + let public_key_procedure = iota_stronghold::procedures::PublicKey { + ty: ProceduresKeyType::Ed25519, + private_key: location, + }; + + let procedure_result = client + .execute_procedure(StrongholdProcedure::PublicKey(public_key_procedure)) + .map_err(|err| KeyStorageError::new(KeyStorageErrorKind::KeyNotFound).with_source(err))?; + + let public_key: Vec = procedure_result.into(); + + let mut params = JwkParamsOkp::new(); + params.x = jwu::encode_b64(public_key); + EdCurve::Ed25519.name().clone_into(&mut params.crv); + let mut jwk: Jwk = Jwk::from_params(params); + jwk.set_alg(JwsAlgorithm::EdDSA.name()); + jwk.set_kid(jwk.thumbprint_sha256_b64()); + + Ok(jwk) + } +} diff --git a/identity_stronghold/src/stronghold_jwk_storage.rs b/identity_stronghold/src/storage/stronghold_jwk_storage.rs similarity index 54% rename from identity_stronghold/src/stronghold_jwk_storage.rs rename to identity_stronghold/src/storage/stronghold_jwk_storage.rs index eb1b186f86..b0400c8f65 100644 --- a/identity_stronghold/src/stronghold_jwk_storage.rs +++ b/identity_stronghold/src/storage/stronghold_jwk_storage.rs @@ -14,58 +14,19 @@ use identity_storage::KeyType; use identity_verification::jwk::EdCurve; use identity_verification::jwk::Jwk; use identity_verification::jwk::JwkParamsOkp; -use identity_verification::jwk::JwkType; use identity_verification::jws::JwsAlgorithm; use identity_verification::jwu; -use iota_sdk::client::secret::stronghold::StrongholdSecretManager; -use iota_sdk::client::secret::SecretManager; use iota_stronghold::procedures::Ed25519Sign; use iota_stronghold::procedures::GenerateKey; use iota_stronghold::procedures::KeyType as ProceduresKeyType; use iota_stronghold::procedures::StrongholdProcedure; -use iota_stronghold::Client; -use iota_stronghold::ClientError; use iota_stronghold::Location; -use iota_stronghold::Stronghold; -use rand::distributions::DistString; -use std::fmt::Display; use std::str::FromStr; -use std::sync::Arc; -use tokio::sync::MutexGuard; use crate::ed25519; - -const ED25519_KEY_TYPE_STR: &str = "Ed25519"; -static IDENTITY_VAULT_PATH: &str = "iota_identity_vault"; -pub(crate) static IDENTITY_CLIENT_PATH: &[u8] = b"iota_identity_client"; - -/// The Ed25519 key type. -pub const ED25519_KEY_TYPE: &KeyType = &KeyType::from_static_str(ED25519_KEY_TYPE_STR); - -/// Wrapper around a [`StrongholdSecretManager`] that implements the [`KeyIdStorage`](crate::KeyIdStorage) -/// and [`JwkStorage`](crate::JwkStorage) interfaces. -#[derive(Clone, Debug)] -pub struct StrongholdStorage(Arc); - -impl StrongholdStorage { - /// Creates a new [`StrongholdStorage`]. - pub fn new(stronghold_secret_manager: StrongholdSecretManager) -> Self { - Self(Arc::new(SecretManager::Stronghold(stronghold_secret_manager))) - } - - /// Shared reference to the inner [`SecretManager`]. - pub fn as_secret_manager(&self) -> &SecretManager { - self.0.as_ref() - } - - /// Acquire lock of the inner [`Stronghold`]. - pub(crate) async fn get_stronghold(&self) -> MutexGuard<'_, Stronghold> { - match *self.0 { - SecretManager::Stronghold(ref stronghold) => stronghold.inner().await, - _ => unreachable!("secret manager can be only constrcuted from stronghold"), - } - } -} +use crate::stronghold_key_type::StrongholdKeyType; +use crate::utils::*; +use crate::StrongholdStorage; #[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] #[cfg_attr(feature = "send-sync-storage", async_trait)] @@ -79,6 +40,13 @@ impl JwkStorage for StrongholdStorage { let keytype: ProceduresKeyType = match key_type { StrongholdKeyType::Ed25519 => ProceduresKeyType::Ed25519, + StrongholdKeyType::Bls12381G2 => { + return Err( + KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_custom_message(format!( + "`{key_type}` is supported but `JwkStorageBbsPlusExt::generate_bbs` should be called instead." + )), + ) + } }; let key_id: KeyId = random_key_id(); @@ -112,12 +80,12 @@ impl JwkStorage for StrongholdStorage { .with_custom_message("stronghold public key procedure failed") .with_source(err) })?; - persist_changes(self, stronghold).await?; let public_key: Vec = procedure_result.into(); + persist_changes(self.as_secret_manager(), stronghold).await?; let mut params = JwkParamsOkp::new(); params.x = jwu::encode_b64(public_key); - params.crv = EdCurve::Ed25519.name().to_owned(); + params.crv = EdCurve::Ed25519.name().to_string(); let mut jwk: Jwk = Jwk::from_params(params); jwk.set_alg(alg.name()); jwk.set_kid(jwk.thumbprint_sha256_b64()); @@ -126,7 +94,7 @@ impl JwkStorage for StrongholdStorage { } async fn insert(&self, jwk: Jwk) -> KeyStorageResult { - let key_type: StrongholdKeyType = StrongholdKeyType::try_from(&jwk)?; + let key_type = StrongholdKeyType::try_from(&jwk)?; if !jwk.is_private() { return Err( KeyStorageError::new(KeyStorageErrorKind::Unspecified) @@ -164,7 +132,8 @@ impl JwkStorage for StrongholdStorage { .with_custom_message("stronghold write secret failed") .with_source(err) })?; - persist_changes(self, stronghold).await?; + + persist_changes(self.as_secret_manager(), stronghold).await?; Ok(key_id) } @@ -239,7 +208,8 @@ impl JwkStorage for StrongholdStorage { if !deleted { return Err(KeyStorageError::new(KeyStorageErrorKind::KeyNotFound)); } - persist_changes(self, stronghold).await?; + + persist_changes(self.as_secret_manager(), stronghold).await?; Ok(()) } @@ -259,134 +229,3 @@ impl JwkStorage for StrongholdStorage { Ok(exists) } } - -/// Generate a random alphanumeric string of len 32. -fn random_key_id() -> KeyId { - KeyId::new(rand::distributions::Alphanumeric.sample_string(&mut rand::thread_rng(), 32)) -} - -/// Check that the key type can be used with the algorithm. -fn check_key_alg_compatibility(key_type: StrongholdKeyType, alg: JwsAlgorithm) -> KeyStorageResult<()> { - match (key_type, alg) { - (StrongholdKeyType::Ed25519, JwsAlgorithm::EdDSA) => Ok(()), - (key_type, alg) => Err( - KeyStorageError::new(identity_storage::KeyStorageErrorKind::KeyAlgorithmMismatch) - .with_custom_message(format!("cannot use key type `{key_type}` with algorithm `{alg}`")), - ), - } -} - -fn get_client(stronghold: &Stronghold) -> KeyStorageResult { - let client = stronghold.get_client(IDENTITY_CLIENT_PATH); - match client { - Ok(client) => Ok(client), - Err(ClientError::ClientDataNotPresent) => load_or_create_client(stronghold), - Err(err) => Err(KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(err)), - } -} - -fn load_or_create_client(stronghold: &Stronghold) -> KeyStorageResult { - match stronghold.load_client(IDENTITY_CLIENT_PATH) { - Ok(client) => Ok(client), - Err(ClientError::ClientDataNotPresent) => stronghold - .create_client(IDENTITY_CLIENT_PATH) - .map_err(|err| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(err)), - Err(err) => Err(KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(err)), - } -} - -async fn persist_changes( - secret_manager: &StrongholdStorage, - stronghold: MutexGuard<'_, Stronghold>, -) -> KeyStorageResult<()> { - stronghold.write_client(IDENTITY_CLIENT_PATH).map_err(|err| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified) - .with_custom_message("stronghold write client error") - .with_source(err) - })?; - // Must be dropped since `write_stronghold_snapshot` needs to acquire the stronghold lock. - drop(stronghold); - - match secret_manager.as_secret_manager() { - iota_sdk::client::secret::SecretManager::Stronghold(stronghold_manager) => { - stronghold_manager - .write_stronghold_snapshot(None) - .await - .map_err(|err| { - KeyStorageError::new(KeyStorageErrorKind::Unspecified) - .with_custom_message("writing to stronghold snapshot failed") - .with_source(err) - })?; - } - _ => { - return Err( - KeyStorageError::new(KeyStorageErrorKind::Unspecified) - .with_custom_message("secret manager is not of type stronghold"), - ) - } - }; - Ok(()) -} - -/// Key Types supported by the stronghold storage implementation. -#[derive(Debug, Copy, Clone)] -enum StrongholdKeyType { - Ed25519, -} - -impl StrongholdKeyType { - /// String representation of the key type. - const fn name(&self) -> &'static str { - match self { - StrongholdKeyType::Ed25519 => "Ed25519", - } - } -} - -impl Display for StrongholdKeyType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(self.name()) - } -} - -impl TryFrom<&KeyType> for StrongholdKeyType { - type Error = KeyStorageError; - - fn try_from(value: &KeyType) -> Result { - match value.as_str() { - ED25519_KEY_TYPE_STR => Ok(StrongholdKeyType::Ed25519), - _ => Err(KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType)), - } - } -} - -impl TryFrom<&Jwk> for StrongholdKeyType { - type Error = KeyStorageError; - - fn try_from(jwk: &Jwk) -> Result { - match jwk.kty() { - JwkType::Okp => { - let okp_params = jwk.try_okp_params().map_err(|err| { - KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) - .with_custom_message("expected Okp parameters for a JWK with `kty` Okp") - .with_source(err) - })?; - match okp_params.try_ed_curve().map_err(|err| { - KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) - .with_custom_message("only Ed curves are supported for signing") - .with_source(err) - })? { - EdCurve::Ed25519 => Ok(StrongholdKeyType::Ed25519), - curve => Err( - KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) - .with_custom_message(format!("{curve} not supported")), - ), - } - } - other => Err( - KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) - .with_custom_message(format!("Jwk `kty` {other} not supported")), - ), - } - } -} diff --git a/identity_stronghold/src/storage/stronghold_jwk_storage_bbs_plus_ext.rs b/identity_stronghold/src/storage/stronghold_jwk_storage_bbs_plus_ext.rs new file mode 100644 index 0000000000..10fbe7faa0 --- /dev/null +++ b/identity_stronghold/src/storage/stronghold_jwk_storage_bbs_plus_ext.rs @@ -0,0 +1,174 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use async_trait::async_trait; +use identity_storage::key_storage::bls::*; +use identity_storage::key_storage::JwkStorage; +use identity_storage::JwkGenOutput; +use identity_storage::JwkStorageBbsPlusExt; +use identity_storage::KeyId; +use identity_storage::KeyStorageError; +use identity_storage::KeyStorageErrorKind; +use identity_storage::KeyStorageResult; +use identity_storage::KeyType; +use identity_storage::ProofUpdateCtx; +use identity_verification::jwk::Jwk; +use iota_stronghold::procedures::FatalProcedureError; +use iota_stronghold::procedures::Products; +use iota_stronghold::procedures::Runner as _; +use iota_stronghold::Location; +use jsonprooftoken::jpa::algs::ProofAlgorithm; +use std::str::FromStr; +use zeroize::Zeroizing; +use zkryptium::bbsplus::keys::BBSplusSecretKey; + +use crate::stronghold_key_type::*; +use crate::utils::*; +use crate::StrongholdStorage; + +#[cfg_attr(not(feature = "send-sync-storage"), async_trait(?Send))] +#[cfg_attr(feature = "send-sync-storage", async_trait)] +impl JwkStorageBbsPlusExt for StrongholdStorage { + async fn generate_bbs(&self, key_type: KeyType, alg: ProofAlgorithm) -> KeyStorageResult { + let key_type = StrongholdKeyType::try_from(&key_type)?; + + if !matches!(key_type, StrongholdKeyType::Bls12381G2) { + return Err( + KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) + .with_custom_message(format!("{key_type} is not supported")), + ); + } + + if !matches!(alg, ProofAlgorithm::BLS12381_SHA256 | ProofAlgorithm::BLS12381_SHAKE256) { + return Err(KeyStorageErrorKind::UnsupportedProofAlgorithm.into()); + } + + // Get a key id that's not already used. + let mut kid = random_key_id(); + while self.exists(&kid).await? { + kid = random_key_id(); + } + + let stronghold = self.get_stronghold().await; + let client = get_client(&stronghold)?; + let target_key_location = Location::generic( + IDENTITY_VAULT_PATH.as_bytes().to_vec(), + kid.to_string().as_bytes().to_vec(), + ); + let jwk = client + .exec_proc([], &target_key_location, |_| { + let (sk, pk) = generate_bbs_keypair(alg).map_err(|e| FatalProcedureError::from(e.to_string()))?; + let public_jwk = encode_bls_jwk(&sk, &pk, alg).1; + + Ok(Products { + output: public_jwk, + secret: Zeroizing::new(sk.to_bytes().to_vec()), + }) + }) + .map_err(|e| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message("Failed to execute stronghold procedure") + .with_source(e) + })?; + + persist_changes(self.as_secret_manager(), stronghold).await?; + + Ok(JwkGenOutput::new(kid, jwk)) + } + + async fn sign_bbs( + &self, + key_id: &KeyId, + data: &[Vec], + header: &[u8], + public_key: &Jwk, + ) -> 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) + })?; + + // Check `key_id` exists in store. + if !self.exists(key_id).await? { + return Err(KeyStorageError::new(KeyStorageErrorKind::KeyNotFound)); + } + + let pk = expand_bls_jwk(public_key) + .map_err(|e| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(e))? + .1; + + let sk_location = Location::Generic { + vault_path: IDENTITY_VAULT_PATH.as_bytes().to_vec(), + record_path: key_id.to_string().as_bytes().to_vec(), + }; + + let stronghold = self.get_stronghold().await; + let client = get_client(&stronghold)?; + client + .get_guards([sk_location], |[sk]| { + let sk = BBSplusSecretKey::from_bytes(&sk.borrow()).map_err(|e| FatalProcedureError::from(e.to_string()))?; + // Ensure `sk` and `pk` matches. + if sk.public_key() != pk { + return Err(FatalProcedureError::from( + "`public_key` is not the public key of key with id `key_id`".to_owned(), + )); + } + let signature_result = + sign_bbs(alg, data, &sk, &pk, header).map_err(|e| FatalProcedureError::from(e.to_string())); + // clean up `sk` to avoid leaking. + drop(Zeroizing::new(sk.to_bytes())); + signature_result + }) + .map(|sig| sig.to_vec()) + .map_err(|e| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message("Signature failed") + .with_source(e) + }) + } + + 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) + })?; + + // Check `key_id` exists in store. + if !self.exists(key_id).await? { + return Err(KeyStorageError::new(KeyStorageErrorKind::KeyNotFound)); + } + + let sk_location = Location::Generic { + vault_path: IDENTITY_VAULT_PATH.as_bytes().to_vec(), + record_path: key_id.to_string().as_bytes().to_vec(), + }; + let stronghold = self.get_stronghold().await; + let client = get_client(&stronghold)?; + + client + .get_guards([sk_location], |[sk]| { + let sk = BBSplusSecretKey::from_bytes(&sk.borrow()).map_err(|e| FatalProcedureError::from(e.to_string()))?; + let signature_update_result = + update_bbs_signature(alg, signature, &sk, &ctx).map_err(|e| FatalProcedureError::from(e.to_string())); + drop(Zeroizing::new(sk.to_bytes())); + signature_update_result + }) + .map_err(|e| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message("Signature update failed") + .with_source(e) + }) + } +} diff --git a/identity_stronghold/src/stronghold_key_id.rs b/identity_stronghold/src/storage/stronghold_key_id.rs similarity index 98% rename from identity_stronghold/src/stronghold_key_id.rs rename to identity_stronghold/src/storage/stronghold_key_id.rs index dcd3755cab..f7b7aa6436 100644 --- a/identity_stronghold/src/stronghold_key_id.rs +++ b/identity_stronghold/src/storage/stronghold_key_id.rs @@ -1,7 +1,7 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use crate::stronghold_jwk_storage::IDENTITY_CLIENT_PATH; +use crate::utils::IDENTITY_CLIENT_PATH; use crate::StrongholdStorage; use async_trait::async_trait; use identity_storage::key_id_storage::KeyIdStorage; diff --git a/identity_stronghold/src/stronghold_key_type.rs b/identity_stronghold/src/stronghold_key_type.rs new file mode 100644 index 0000000000..c78deb4d3a --- /dev/null +++ b/identity_stronghold/src/stronghold_key_type.rs @@ -0,0 +1,109 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::fmt::Display; + +use identity_storage::KeyStorageError; +use identity_storage::KeyStorageErrorKind; +use identity_storage::KeyType; +use identity_verification::jwk::BlsCurve; +use identity_verification::jwk::EdCurve; +use identity_verification::jwk::Jwk; +use identity_verification::jwk::JwkType; + +pub const ED25519_KEY_TYPE_STR: &str = "Ed25519"; +/// The Ed25519 key type. +pub const ED25519_KEY_TYPE: KeyType = KeyType::from_static_str(ED25519_KEY_TYPE_STR); +pub const BLS12381G2_KEY_TYPE_STR: &str = "BLS12381G2"; +/// The BLS12381G2 key type +pub const BLS12381G2_KEY_TYPE: KeyType = KeyType::from_static_str(BLS12381G2_KEY_TYPE_STR); + +/// Key Types supported by the stronghold storage implementation. +#[derive(Debug, Copy, Clone)] +pub enum StrongholdKeyType { + Ed25519, + Bls12381G2, +} + +impl StrongholdKeyType { + /// String representation of the key type. + const fn name(&self) -> &'static str { + match self { + StrongholdKeyType::Ed25519 => ED25519_KEY_TYPE_STR, + StrongholdKeyType::Bls12381G2 => BLS12381G2_KEY_TYPE_STR, + } + } +} + +impl Display for StrongholdKeyType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.name()) + } +} + +impl TryFrom<&KeyType> for StrongholdKeyType { + type Error = KeyStorageError; + + fn try_from(value: &KeyType) -> Result { + match value.as_str() { + ED25519_KEY_TYPE_STR => Ok(StrongholdKeyType::Ed25519), + BLS12381G2_KEY_TYPE_STR => Ok(StrongholdKeyType::Bls12381G2), + _ => Err(KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType)), + } + } +} + +impl From for KeyType { + fn from(key_type: StrongholdKeyType) -> KeyType { + KeyType::from_static_str(key_type.name()) + } +} + +impl TryFrom<&Jwk> for StrongholdKeyType { + type Error = KeyStorageError; + + fn try_from(jwk: &Jwk) -> Result { + match jwk.kty() { + JwkType::Okp => { + let okp_params = jwk.try_okp_params().map_err(|err| { + KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) + .with_custom_message("expected Okp parameters for a JWK with `kty` Okp") + .with_source(err) + })?; + match okp_params.try_ed_curve().map_err(|err| { + KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) + .with_custom_message("only Ed curves are supported for signing") + .with_source(err) + })? { + EdCurve::Ed25519 => Ok(StrongholdKeyType::Ed25519), + curve => Err( + KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) + .with_custom_message(format!("{curve} not supported")), + ), + } + } + JwkType::Ec => { + let ec_params = jwk.try_ec_params().map_err(|err| { + KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) + .with_custom_message("expected EC parameters for a JWK with `kty` Ec") + .with_source(err) + })?; + match ec_params.try_bls_curve().map_err(|err| { + KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) + .with_custom_message("only Ed curves are supported for signing") + .with_source(err) + })? { + BlsCurve::BLS12381G2 => Ok(StrongholdKeyType::Bls12381G2), + curve => Err( + KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) + .with_custom_message(format!("{curve} not supported")), + ), + } + } + other => Err( + KeyStorageError::new(KeyStorageErrorKind::UnsupportedKeyType) + .with_custom_message(format!("Jwk `kty` {other} not supported")), + ), + } + } +} diff --git a/identity_stronghold/src/tests/mod.rs b/identity_stronghold/src/tests/mod.rs index 96db03f0aa..54c5488c05 100644 --- a/identity_stronghold/src/tests/mod.rs +++ b/identity_stronghold/src/tests/mod.rs @@ -1,6 +1,7 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +mod test_bbs_ext; mod test_jwk_storage; mod test_key_id_storage; pub(crate) mod utils; diff --git a/identity_stronghold/src/tests/test_bbs_ext.rs b/identity_stronghold/src/tests/test_bbs_ext.rs new file mode 100644 index 0000000000..efa71f3cc2 --- /dev/null +++ b/identity_stronghold/src/tests/test_bbs_ext.rs @@ -0,0 +1,93 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_storage::key_storage::bls::expand_bls_jwk; +use identity_storage::key_storage::bls::sign_bbs; +use identity_storage::JwkGenOutput; +use identity_storage::JwkStorage; +use identity_storage::JwkStorageBbsPlusExt; +use identity_storage::KeyStorageErrorKind; +use iota_stronghold::procedures::Runner; +use iota_stronghold::Location; +use jsonprooftoken::jpa::algs::ProofAlgorithm; +use rand::RngCore; +use zkryptium::bbsplus::keys::BBSplusSecretKey; + +use crate::stronghold_key_type::StrongholdKeyType; +use crate::tests::utils::create_stronghold_secret_manager; +use crate::utils::get_client; +use crate::utils::IDENTITY_VAULT_PATH; +use crate::StrongholdStorage; + +#[tokio::test] +async fn stronghold_bbs_keypair_gen_works() -> anyhow::Result<()> { + let stronghold_storage = StrongholdStorage::new(create_stronghold_secret_manager()); + let JwkGenOutput { key_id, jwk, .. } = stronghold_storage + .generate_bbs(StrongholdKeyType::Bls12381G2.into(), ProofAlgorithm::BLS12381_SHA256) + .await?; + + assert!(jwk.is_public()); + assert!(stronghold_storage.exists(&key_id).await?); + + Ok(()) +} + +#[tokio::test] +async fn stronghold_bbs_keypair_gen_fails_with_wrong_key_type() -> anyhow::Result<()> { + let stronghold_storage = StrongholdStorage::new(create_stronghold_secret_manager()); + let error = stronghold_storage + .generate_bbs(StrongholdKeyType::Ed25519.into(), ProofAlgorithm::BLS12381_SHA256) + .await + .unwrap_err(); + assert!(matches!(error.kind(), KeyStorageErrorKind::UnsupportedKeyType)); + + Ok(()) +} + +#[tokio::test] +async fn stronghold_bbs_keypair_gen_fails_with_wrong_alg() -> anyhow::Result<()> { + let stronghold_storage = StrongholdStorage::new(create_stronghold_secret_manager()); + let error = stronghold_storage + .generate_bbs(StrongholdKeyType::Bls12381G2.into(), ProofAlgorithm::MAC_H256) + .await + .unwrap_err(); + + assert!(matches!(error.kind(), KeyStorageErrorKind::UnsupportedProofAlgorithm)); + + Ok(()) +} + +#[tokio::test] +async fn stronghold_sign_bbs_works() -> anyhow::Result<()> { + let stronghold_storage = StrongholdStorage::new(create_stronghold_secret_manager()); + let JwkGenOutput { key_id, jwk, .. } = stronghold_storage + .generate_bbs(StrongholdKeyType::Bls12381G2.into(), ProofAlgorithm::BLS12381_SHA256) + .await?; + let pk = expand_bls_jwk(&jwk)?.1; + let sk = { + let stronghold = stronghold_storage.get_stronghold().await; + let client = get_client(&stronghold)?; + let sk_location = Location::Generic { + vault_path: IDENTITY_VAULT_PATH.as_bytes().to_owned(), + record_path: key_id.as_str().as_bytes().to_owned(), + }; + client + .get_guards([sk_location], |[sk]| Ok(BBSplusSecretKey::from_bytes(&sk.borrow()))) + .unwrap() + }?; + + let mut data = vec![0; 1024]; + rand::thread_rng().fill_bytes(&mut data); + let expected_signature = sign_bbs( + ProofAlgorithm::BLS12381_SHA256, + std::slice::from_ref(&data), + &sk, + &pk, + &[], + )?; + + let signature = stronghold_storage.sign_bbs(&key_id, &[data], &[], &jwk).await?; + assert_eq!(signature, expected_signature); + + Ok(()) +} diff --git a/identity_stronghold/src/tests/test_jwk_storage.rs b/identity_stronghold/src/tests/test_jwk_storage.rs index 73f6a41a1e..e7ccbb2a05 100644 --- a/identity_stronghold/src/tests/test_jwk_storage.rs +++ b/identity_stronghold/src/tests/test_jwk_storage.rs @@ -22,6 +22,24 @@ async fn insert() { jwk_storage_tests::test_insertion(stronghold_storage).await; } +#[tokio::test] +async fn retrieve() { + let stronghold_secret_manager = create_stronghold_secret_manager(); + let stronghold_storage = StrongholdStorage::new(stronghold_secret_manager); + + let generate = stronghold_storage + .generate(KeyType::new("Ed25519"), JwsAlgorithm::EdDSA) + .await + .unwrap(); + let key_id = &generate.key_id; + + let pub_key: Jwk = stronghold_storage + .get_public_key_with_type(key_id, crate::stronghold_key_type::StrongholdKeyType::Ed25519) + .await + .unwrap(); + assert_eq!(generate.jwk, pub_key); +} + #[tokio::test] async fn incompatible_key_alg() { let stronghold_secret_manager = create_stronghold_secret_manager(); @@ -53,6 +71,7 @@ async fn key_exists() { // Tests the cases that require persisting to disk, generate, insert and delete. #[tokio::test] async fn write_to_disk() { + iota_stronghold::engine::snapshot::try_set_encrypt_work_factor(0).unwrap(); const PASS: &str = "secure_password"; let file: PathBuf = create_temp_file(); let secret_manager = StrongholdSecretManager::builder() @@ -152,10 +171,10 @@ mod jwk_storage_tests { pub(crate) async fn test_incompatible_key_type(store: impl JwkStorage) { let mut ec_params = JwkParamsEc::new(); - ec_params.crv = EcCurve::P256.name().to_owned(); - ec_params.x = "".to_owned(); - ec_params.y = "".to_owned(); - ec_params.d = Some("".to_owned()); + ec_params.crv = EcCurve::P256.name().to_string(); + ec_params.x = String::new(); + ec_params.y = String::new(); + ec_params.d = Some(String::new()); let jwk_ec = Jwk::from_params(ec_params); let err = store.insert(jwk_ec).await.unwrap_err(); diff --git a/identity_stronghold/src/tests/utils.rs b/identity_stronghold/src/tests/utils.rs index 9fec954f0f..5113c95f28 100644 --- a/identity_stronghold/src/tests/utils.rs +++ b/identity_stronghold/src/tests/utils.rs @@ -28,7 +28,7 @@ pub(crate) fn encode_public_ed25519_jwk(public_key: &PublicKey) -> Jwk { let mut params = JwkParamsOkp::new(); params.x = x; params.d = None; - params.crv = EdCurve::Ed25519.name().to_owned(); + params.crv = EdCurve::Ed25519.name().to_string(); let mut jwk = Jwk::from_params(params); jwk.set_alg(JwsAlgorithm::EdDSA.name()); jwk diff --git a/identity_stronghold/src/utils.rs b/identity_stronghold/src/utils.rs new file mode 100644 index 0000000000..3a9ae72842 --- /dev/null +++ b/identity_stronghold/src/utils.rs @@ -0,0 +1,87 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_storage::KeyId; +use identity_storage::KeyStorageError; +use identity_storage::KeyStorageErrorKind; +use identity_storage::KeyStorageResult; +use identity_verification::jws::JwsAlgorithm; +use iota_sdk::client::secret::SecretManager; +use iota_stronghold::Client; +use iota_stronghold::ClientError; +use iota_stronghold::Stronghold; +use rand::distributions::DistString as _; +use tokio::sync::MutexGuard; + +use crate::stronghold_key_type::StrongholdKeyType; + +pub static IDENTITY_VAULT_PATH: &str = "iota_identity_vault"; +pub static IDENTITY_CLIENT_PATH: &[u8] = b"iota_identity_client"; + +/// Generate a random alphanumeric string of len 32. +pub fn random_key_id() -> KeyId { + KeyId::new(rand::distributions::Alphanumeric.sample_string(&mut rand::thread_rng(), 32)) +} + +/// Check that the key type can be used with the algorithm. +pub fn check_key_alg_compatibility(key_type: StrongholdKeyType, alg: JwsAlgorithm) -> KeyStorageResult<()> { + match (key_type, alg) { + (StrongholdKeyType::Ed25519, JwsAlgorithm::EdDSA) => Ok(()), + (key_type, alg) => Err( + KeyStorageError::new(identity_storage::KeyStorageErrorKind::KeyAlgorithmMismatch) + .with_custom_message(format!("cannot use key type `{key_type}` with algorithm `{alg}`")), + ), + } +} + +pub fn get_client(stronghold: &Stronghold) -> KeyStorageResult { + let client = stronghold.get_client(IDENTITY_CLIENT_PATH); + match client { + Ok(client) => Ok(client), + Err(ClientError::ClientDataNotPresent) => load_or_create_client(stronghold), + Err(err) => Err(KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(err)), + } +} + +fn load_or_create_client(stronghold: &Stronghold) -> KeyStorageResult { + match stronghold.load_client(IDENTITY_CLIENT_PATH) { + Ok(client) => Ok(client), + Err(ClientError::ClientDataNotPresent) => stronghold + .create_client(IDENTITY_CLIENT_PATH) + .map_err(|err| KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(err)), + Err(err) => Err(KeyStorageError::new(KeyStorageErrorKind::Unspecified).with_source(err)), + } +} + +pub async fn persist_changes( + secret_manager: &SecretManager, + stronghold: MutexGuard<'_, Stronghold>, +) -> KeyStorageResult<()> { + stronghold.write_client(IDENTITY_CLIENT_PATH).map_err(|err| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message("stronghold write client error") + .with_source(err) + })?; + // Must be dropped since `write_stronghold_snapshot` needs to acquire the stronghold lock. + drop(stronghold); + + match secret_manager { + iota_sdk::client::secret::SecretManager::Stronghold(stronghold_manager) => { + stronghold_manager + .write_stronghold_snapshot(None) + .await + .map_err(|err| { + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message("writing to stronghold snapshot failed") + .with_source(err) + })?; + } + _ => { + return Err( + KeyStorageError::new(KeyStorageErrorKind::Unspecified) + .with_custom_message("secret manager is not of type stronghold"), + ) + } + }; + Ok(()) +} diff --git a/identity_verification/Cargo.toml b/identity_verification/Cargo.toml index 2367aeed14..41ba1f2aa3 100644 --- a/identity_verification/Cargo.toml +++ b/identity_verification/Cargo.toml @@ -1,20 +1,24 @@ [package] name = "identity_verification" -version = "1.1.0-alpha.1" +version = "1.3.0" authors.workspace = true edition.workspace = true homepage.workspace = true license.workspace = true +repository.workspace = true rust-version.workspace = true description = "Verification data types and functionality for identity.rs" [dependencies] -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_jose = { version = "=1.1.0-alpha.1", path = "./../identity_jose", 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_jose = { version = "=1.3.0", path = "./../identity_jose", default-features = false } serde.workspace = true +serde_json.workspace = true strum.workspace = true thiserror.workspace = true [dev-dependencies] -serde_json.workspace = true + +[lints] +workspace = true diff --git a/identity_verification/src/verification_method/material.rs b/identity_verification/src/verification_method/material.rs index d8553a4368..8e881253c5 100644 --- a/identity_verification/src/verification_method/material.rs +++ b/identity_verification/src/verification_method/material.rs @@ -5,6 +5,12 @@ use crate::jose::jwk::Jwk; use core::fmt::Debug; use core::fmt::Formatter; use identity_core::convert::BaseEncoding; +use serde::de::Visitor; +use serde::ser::SerializeMap; +use serde::Deserialize; +use serde::Serialize; +use serde::Serializer; +use serde_json::Value; use crate::error::Error; use crate::error::Result; @@ -21,6 +27,9 @@ pub enum MethodData { PublicKeyBase58(String), /// Verification Material in the JSON Web Key format. PublicKeyJwk(Jwk), + /// Arbitrary verification material. + #[serde(untagged)] + Custom(CustomMethodData), } impl MethodData { @@ -36,6 +45,11 @@ impl MethodData { Self::PublicKeyMultibase(BaseEncoding::encode_multibase(&data, None)) } + /// Creates a new `MethodData` variant from custom data. + pub fn new_custom(data: impl Into) -> Self { + Self::Custom(data.into()) + } + /// Returns a `Vec` containing the decoded bytes of the `MethodData`. /// /// This is generally a public key identified by a `MethodType` value. @@ -45,7 +59,7 @@ impl MethodData { /// represented as a vector of bytes. pub fn try_decode(&self) -> Result> { match self { - Self::PublicKeyJwk(_) => Err(Error::InvalidMethodDataTransformation( + Self::PublicKeyJwk(_) | Self::Custom(_) => Err(Error::InvalidMethodDataTransformation( "method data is not base encoded", )), Self::PublicKeyMultibase(input) => { @@ -68,6 +82,15 @@ impl MethodData { pub fn try_public_key_jwk(&self) -> Result<&Jwk> { self.public_key_jwk().ok_or(Error::NotPublicKeyJwk) } + + /// Returns the custom method data, if any. + pub fn custom(&self) -> Option<&CustomMethodData> { + if let Self::Custom(method_data) = self { + Some(method_data) + } else { + None + } + } } impl Debug for MethodData { @@ -76,6 +99,94 @@ impl Debug for MethodData { Self::PublicKeyJwk(inner) => f.write_fmt(format_args!("PublicKeyJwk({inner:#?})")), Self::PublicKeyMultibase(inner) => f.write_fmt(format_args!("PublicKeyMultibase({inner})")), Self::PublicKeyBase58(inner) => f.write_fmt(format_args!("PublicKeyBase58({inner})")), + Self::Custom(CustomMethodData { name, data }) => f.write_fmt(format_args!("{name}({data})")), } } } + +#[derive(Clone, Debug, PartialEq, Eq)] +/// Custom verification method. +pub struct CustomMethodData { + /// Verification method's name. + pub name: String, + /// Verification method's data. + pub data: Value, +} + +impl Serialize for CustomMethodData { + fn serialize(&self, serializer: S) -> std::prelude::v1::Result + where + S: Serializer, + { + let mut map = serializer.serialize_map(Some(1))?; + map.serialize_entry(&self.name, &self.data)?; + map.end() + } +} + +impl<'de> Deserialize<'de> for CustomMethodData { + fn deserialize(deserializer: D) -> std::prelude::v1::Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_map(CustomMethodDataVisitor) + } +} + +struct CustomMethodDataVisitor; + +impl<'de> Visitor<'de> for CustomMethodDataVisitor { + type Value = CustomMethodData; + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("\"\": ") + } + fn visit_map(self, mut map: A) -> std::prelude::v1::Result + where + A: serde::de::MapAccess<'de>, + { + let mut custom_method_data = CustomMethodData { + name: String::default(), + data: Value::Null, + }; + while let Some((name, data)) = map.next_entry::()? { + custom_method_data = CustomMethodData { name, data }; + } + + Ok(custom_method_data) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn serialize_custom_method_data() { + let custom = MethodData::Custom(CustomMethodData { + name: "anArbitraryMethod".to_owned(), + data: json!({"a": 1, "b": 2}), + }); + let target_str = json!({ + "anArbitraryMethod": {"a": 1, "b": 2}, + }) + .to_string(); + assert_eq!(serde_json::to_string(&custom).unwrap(), target_str); + } + #[test] + fn deserialize_custom_method_data() { + let inner_data = json!({ + "firstCustomField": "a random string", + "secondCustomField": 420, + }); + let json_method_data = json!({ + "myCustomVerificationMethod": &inner_data, + }); + let custom = serde_json::from_value::(json_method_data.clone()).unwrap(); + let target_method_data = MethodData::Custom(CustomMethodData { + name: "myCustomVerificationMethod".to_owned(), + data: inner_data, + }); + assert_eq!(custom, target_method_data); + } +} diff --git a/identity_verification/src/verification_method/method.rs b/identity_verification/src/verification_method/method.rs index 360f2efe55..65c838639f 100644 --- a/identity_verification/src/verification_method/method.rs +++ b/identity_verification/src/verification_method/method.rs @@ -20,6 +20,7 @@ use crate::verification_method::MethodBuilder; use crate::verification_method::MethodData; use crate::verification_method::MethodRef; use crate::verification_method::MethodType; +use crate::CustomMethodData; use identity_did::CoreDID; use identity_did::DIDUrl; use identity_did::DID; @@ -28,8 +29,8 @@ use identity_did::DID; /// /// [Specification](https://www.w3.org/TR/did-core/#verification-method-properties) #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +#[serde(from = "_VerificationMethod")] pub struct VerificationMethod { - #[serde(deserialize_with = "deserialize_id_with_fragment")] pub(crate) id: DIDUrl, pub(crate) controller: CoreDID, #[serde(rename = "type")] @@ -219,7 +220,7 @@ impl VerificationMethod { MethodBuilder::default() .id(id) .controller(did.into()) - .type_(MethodType::JSON_WEB_KEY) + .type_(MethodType::JSON_WEB_KEY_2020) .data(MethodData::PublicKeyJwk(key)) .build() } @@ -245,3 +246,46 @@ impl KeyComparable for VerificationMethod { self.id() } } + +// Horrible workaround for a tracked serde issue https://github.com/serde-rs/serde/issues/2200. Serde doesn't "consume" +// the input when deserializing flattened enums (MethodData in this case) causing duplication of data (in this case +// it ends up in the properties object). This workaround simply removes the duplication. +#[derive(Deserialize)] +struct _VerificationMethod { + #[serde(deserialize_with = "deserialize_id_with_fragment")] + pub(crate) id: DIDUrl, + pub(crate) controller: CoreDID, + #[serde(rename = "type")] + pub(crate) type_: MethodType, + #[serde(flatten)] + pub(crate) data: MethodData, + #[serde(flatten)] + pub(crate) properties: Object, +} + +impl From<_VerificationMethod> for VerificationMethod { + fn from(value: _VerificationMethod) -> Self { + let _VerificationMethod { + id, + controller, + type_, + data, + mut properties, + } = value; + let key = match &data { + MethodData::PublicKeyBase58(_) => "publicKeyBase58", + MethodData::PublicKeyJwk(_) => "publicKeyJwk", + MethodData::PublicKeyMultibase(_) => "publicKeyMultibase", + MethodData::Custom(CustomMethodData { name, .. }) => name.as_str(), + }; + properties.remove(key); + + VerificationMethod { + id, + controller, + type_, + data, + properties, + } + } +} diff --git a/identity_verification/src/verification_method/method_type.rs b/identity_verification/src/verification_method/method_type.rs index e387db14de..5a3eadd4f1 100644 --- a/identity_verification/src/verification_method/method_type.rs +++ b/identity_verification/src/verification_method/method_type.rs @@ -12,6 +12,7 @@ use crate::error::Result; const ED25519_VERIFICATION_KEY_2018_STR: &str = "Ed25519VerificationKey2018"; const X25519_KEY_AGREEMENT_KEY_2019_STR: &str = "X25519KeyAgreementKey2019"; const JSON_WEB_KEY_METHOD_TYPE: &str = "JsonWebKey"; +const JSON_WEB_KEY_2020_STR: &str = "JsonWebKey2020"; /// verification method types. #[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)] @@ -24,7 +25,15 @@ impl MethodType { pub const X25519_KEY_AGREEMENT_KEY_2019: Self = Self(Cow::Borrowed(X25519_KEY_AGREEMENT_KEY_2019_STR)); /// A verification method for use with JWT verification as prescribed by the [`Jwk`](::identity_jose::jwk::Jwk) /// in the [`publicKeyJwk`](crate::MethodData::PublicKeyJwk) entry. + #[deprecated(since = "1.3.0", note = "use JSON_WEB_KEY_2020 instead")] pub const JSON_WEB_KEY: Self = Self(Cow::Borrowed(JSON_WEB_KEY_METHOD_TYPE)); + /// A verification method for use with JWT verification as prescribed by the [`Jwk`](::identity_jose::jwk::Jwk) + /// in the [`publicKeyJwk`](crate::MethodData::PublicKeyJwk) entry. + pub const JSON_WEB_KEY_2020: Self = Self(Cow::Borrowed(JSON_WEB_KEY_2020_STR)); + /// Construct a custom method type. + pub fn custom(type_: impl AsRef) -> Self { + Self(Cow::Owned(type_.as_ref().to_owned())) + } } impl MethodType { @@ -53,7 +62,11 @@ impl FromStr for MethodType { match string { ED25519_VERIFICATION_KEY_2018_STR => Ok(Self::ED25519_VERIFICATION_KEY_2018), X25519_KEY_AGREEMENT_KEY_2019_STR => Ok(Self::X25519_KEY_AGREEMENT_KEY_2019), - JSON_WEB_KEY_METHOD_TYPE => Ok(Self::JSON_WEB_KEY), + JSON_WEB_KEY_METHOD_TYPE => Ok( + #[allow(deprecated)] + Self::JSON_WEB_KEY, + ), + JSON_WEB_KEY_2020_STR => Ok(Self::JSON_WEB_KEY_2020), _ => Ok(Self(Cow::Owned(string.to_owned()))), } } @@ -70,6 +83,7 @@ mod tests { for method_type in [ MethodType::ED25519_VERIFICATION_KEY_2018, MethodType::X25519_KEY_AGREEMENT_KEY_2019, + MethodType::JSON_WEB_KEY_2020, ] { let ser: Value = serde_json::to_value(method_type.clone()).unwrap(); assert_eq!(ser.as_str().unwrap(), method_type.as_str()); diff --git a/identity_verification/src/verification_method/mod.rs b/identity_verification/src/verification_method/mod.rs index af6da98529..585b58639c 100644 --- a/identity_verification/src/verification_method/mod.rs +++ b/identity_verification/src/verification_method/mod.rs @@ -15,6 +15,7 @@ mod method_scope; mod method_type; pub use self::builder::MethodBuilder; +pub use self::material::CustomMethodData; pub use self::material::MethodData; pub use self::method::VerificationMethod; pub use self::method_ref::MethodRef;