From 2f00b22b11fcfde84b62aeae1cf32f77cee28cff Mon Sep 17 00:00:00 2001
From: Enrico Marconi
Date: Wed, 27 Mar 2024 09:00:54 +0100
Subject: [PATCH] merge main
---
.dockerignore | 3 +
.github/workflows/build-and-test-grpc.yml | 41 ++
.github/workflows/build-and-test.yml | 4 +-
.../workflows/grpc-publish-to-dockerhub.yml | 52 +++
Cargo.toml | 5 +-
README.md | 11 +-
bindings/grpc/Cargo.toml | 43 ++
bindings/grpc/Dockerfile | 20 +
bindings/grpc/README.md | 130 ++++++
bindings/grpc/build.rs | 14 +
bindings/grpc/proto/credentials.proto | 61 +++
bindings/grpc/proto/document.proto | 24 ++
bindings/grpc/proto/domain_linkage.proto | 63 +++
bindings/grpc/proto/health_check.proto | 15 +
bindings/grpc/proto/sd_jwt.proto | 30 ++
bindings/grpc/proto/status_list_2021.proto | 50 +++
bindings/grpc/src/lib.rs | 7 +
bindings/grpc/src/main.rs | 47 +++
bindings/grpc/src/server.rs | 33 ++
bindings/grpc/src/services/credential/jwt.rs | 85 ++++
bindings/grpc/src/services/credential/mod.rs | 16 +
.../src/services/credential/revocation.rs | 161 ++++++++
.../src/services/credential/validation.rs | 135 +++++++
bindings/grpc/src/services/document.rs | 115 ++++++
bindings/grpc/src/services/domain_linkage.rs | 377 ++++++++++++++++++
bindings/grpc/src/services/health_check.rs | 36 ++
bindings/grpc/src/services/mod.rs | 26 ++
bindings/grpc/src/services/sd_jwt.rs | 164 ++++++++
.../grpc/src/services/status_list_2021.rs | 170 ++++++++
.../tests/api/credential_revocation_check.rs | 99 +++++
.../grpc/tests/api/credential_validation.rs | 151 +++++++
.../grpc/tests/api/did_document_creation.rs | 43 ++
bindings/grpc/tests/api/domain_linkage.rs | 174 ++++++++
bindings/grpc/tests/api/health_check.rs | 24 ++
bindings/grpc/tests/api/helpers.rs | 336 ++++++++++++++++
bindings/grpc/tests/api/jwt.rs | 54 +++
bindings/grpc/tests/api/main.rs | 12 +
bindings/grpc/tests/api/sd_jwt_validation.rs | 165 ++++++++
bindings/grpc/tests/api/status_list_2021.rs | 94 +++++
.../.well-known/did-configuration.json | 6 +
bindings/grpc/tooling/start-http-server.sh | 4 +
bindings/grpc/tooling/start-rpc-server.sh | 7 +
bindings/wasm/docs/api-reference.md | 228 +++++++----
bindings/wasm/src/iota/iota_document.rs | 20 +
bindings/wasm/src/sd_jwt/wasm_sd_jwt.rs | 1 -
.../wasm/src/verification/wasm_method_data.rs | 50 +++
.../wasm/src/verification/wasm_method_type.rs | 5 +
.../verification/wasm_verification_method.rs | 19 +
examples/Cargo.toml | 4 +-
identity_core/Cargo.toml | 3 +
identity_credential/Cargo.toml | 7 +-
.../src/credential/linked_domain_service.rs | 5 +
identity_credential/src/error.rs | 2 +-
.../revocation/status_list_2021/credential.rs | 52 ++-
.../jwt_credential_validator_utils.rs | 2 +-
identity_did/Cargo.toml | 3 +
identity_document/Cargo.toml | 3 +
identity_eddsa_verifier/Cargo.toml | 3 +
identity_iota/Cargo.toml | 3 +
identity_iota/README.md | 10 +-
identity_iota_core/Cargo.toml | 3 +
.../src/document/iota_document.rs | 135 ++++++-
.../src/state_metadata/document.rs | 29 +-
identity_jose/Cargo.toml | 3 +
identity_resolver/Cargo.toml | 3 +
identity_storage/Cargo.toml | 3 +
identity_stronghold/Cargo.toml | 3 +
identity_verification/Cargo.toml | 5 +-
.../src/verification_method/material.rs | 113 +++++-
.../src/verification_method/method.rs | 46 ++-
.../src/verification_method/method_type.rs | 4 +
.../src/verification_method/mod.rs | 1 +
72 files changed, 3767 insertions(+), 108 deletions(-)
create mode 100644 .dockerignore
create mode 100644 .github/workflows/build-and-test-grpc.yml
create mode 100644 .github/workflows/grpc-publish-to-dockerhub.yml
create mode 100644 bindings/grpc/Cargo.toml
create mode 100644 bindings/grpc/Dockerfile
create mode 100644 bindings/grpc/README.md
create mode 100644 bindings/grpc/build.rs
create mode 100644 bindings/grpc/proto/credentials.proto
create mode 100644 bindings/grpc/proto/document.proto
create mode 100644 bindings/grpc/proto/domain_linkage.proto
create mode 100644 bindings/grpc/proto/health_check.proto
create mode 100644 bindings/grpc/proto/sd_jwt.proto
create mode 100644 bindings/grpc/proto/status_list_2021.proto
create mode 100644 bindings/grpc/src/lib.rs
create mode 100644 bindings/grpc/src/main.rs
create mode 100644 bindings/grpc/src/server.rs
create mode 100644 bindings/grpc/src/services/credential/jwt.rs
create mode 100644 bindings/grpc/src/services/credential/mod.rs
create mode 100644 bindings/grpc/src/services/credential/revocation.rs
create mode 100644 bindings/grpc/src/services/credential/validation.rs
create mode 100644 bindings/grpc/src/services/document.rs
create mode 100644 bindings/grpc/src/services/domain_linkage.rs
create mode 100644 bindings/grpc/src/services/health_check.rs
create mode 100644 bindings/grpc/src/services/mod.rs
create mode 100644 bindings/grpc/src/services/sd_jwt.rs
create mode 100644 bindings/grpc/src/services/status_list_2021.rs
create mode 100644 bindings/grpc/tests/api/credential_revocation_check.rs
create mode 100644 bindings/grpc/tests/api/credential_validation.rs
create mode 100644 bindings/grpc/tests/api/did_document_creation.rs
create mode 100644 bindings/grpc/tests/api/domain_linkage.rs
create mode 100644 bindings/grpc/tests/api/health_check.rs
create mode 100644 bindings/grpc/tests/api/helpers.rs
create mode 100644 bindings/grpc/tests/api/jwt.rs
create mode 100644 bindings/grpc/tests/api/main.rs
create mode 100644 bindings/grpc/tests/api/sd_jwt_validation.rs
create mode 100644 bindings/grpc/tests/api/status_list_2021.rs
create mode 100644 bindings/grpc/tooling/domain-linkage-test-server/.well-known/did-configuration.json
create mode 100644 bindings/grpc/tooling/start-http-server.sh
create mode 100755 bindings/grpc/tooling/start-rpc-server.sh
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/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 d60c351e6c..a58316790f 100644
--- a/.github/workflows/build-and-test.yml
+++ b/.github/workflows/build-and-test.yml
@@ -101,7 +101,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 +109,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 {}
diff --git a/.github/workflows/grpc-publish-to-dockerhub.yml b/.github/workflows/grpc-publish-to-dockerhub.yml
new file mode 100644
index 0000000000..d72fe20702
--- /dev/null
+++ b/.github/workflows/grpc-publish-to-dockerhub.yml
@@ -0,0 +1,52 @@
+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 }}
+ labels: iotaledger/identity-grpc:${{ inputs.tag }}
+
+ - name: Docker Hub Description
+ uses: peter-evans/dockerhub-description@e98e4d1628a5f3be2be7c231e50981aee98723ae
+ with:
+ username: ${{ secrets.IOTALEDGER_DOCKER_USERNAME }}
+ password: ${{ secrets.IOTALEDGER_DOCKER_PASSWORD }}
+ repository: iotaledger/identity-grpc
+ readme-filepath: ./bindigns/grpc/README.md
+ short-description: ${{ github.event.repository.description }}
+
diff --git a/Cargo.toml b/Cargo.toml
index d4726e0d4c..0799f08ca1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -16,7 +16,7 @@ members = [
"examples",
]
-exclude = ["bindings/wasm"]
+exclude = ["bindings/wasm", "bindings/grpc"]
[workspace.dependencies]
serde = { version = "1.0", default-features = false, features = ["alloc", "derive"] }
@@ -31,3 +31,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 658479444f..3b6a4d6305 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
@@ -238,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..f594dc56d4
--- /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.2", 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..814e82a7f8
--- /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/grpc-bindings/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/grpc-bindings/bindings/grpc/proto/credentials.proto) |
+| SD-JWT Validation | `sd_jwt/Verification.verify` | [sd_jwt.proto](https://github.com/iotaledger/identity.rs/blob/grpc-bindings/bindings/grpc/proto/sd_jwt.proto) |
+| Credential JWT creation | `credentials/Jwt.create` | [credentials.proto](https://github.com/iotaledger/identity.rs/blob/grpc-bindings/bindings/grpc/proto/credentials.proto) |
+| Credential JWT validation | `credentials/VcValidation.validate` | [credentials.proto](https://github.com/iotaledger/identity.rs/blob/grpc-bindings/bindings/grpc/proto/credentials.proto) |
+| DID Document Creation | `document/DocumentService.create` | [document.proto](https://github.com/iotaledger/identity.rs/blob/grpc-bindings/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/grpc-bindings/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/grpc-bindings/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/grpc-bindings/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/grpc-bindings/bindings/grpc/proto/domain_linkage.proto) |
+| `StatusList2021Credential` creation | `status_list_2021/StatusList2021Svc.create` | [status_list_2021.proto](https://github.com/iotaledger/identity.rs/blob/grpc-bindings/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/grpc-bindings/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](../../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/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..4e6e3e11fa
--- /dev/null
+++ b/bindings/grpc/src/main.rs
@@ -0,0 +1,47 @@
+// Copyright 2020-2024 IOTA Stiftung
+// SPDX-License-Identifier: Apache-2.0
+
+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 {
+ let stronghold_password = std::env::var("STRONGHOLD_PWD")?;
+ let snapshot_path = std::env::var("SNAPSHOT_PATH")?;
+
+ // Check for snapshot file at specified path
+ let metadata = std::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)?,
+ )
+}
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::
Credential
+CustomMethodData
+A custom verification method data format.
+
DIDUrl
A method agnostic DID Url.
@@ -187,12 +190,9 @@ working with storage backed DID documents.
## Members
-- StateMetadataEncoding
-
-- MethodRelationship
-
-- CredentialStatus
-
+- StatusPurpose
+Purpose of a StatusList2021.
+
- SubjectHolderRelationship
Declares how credential subjects must relate to the presentation holder.
See also the Subject-Holder Relationship section of the specification.
@@ -207,6 +207,8 @@ This variant is the default.
- Any
The holder is not required to have any kind of relationship to any credential subject.
+- StateMetadataEncoding
+
- FailFast
Declares when validation should return if an error occurs.
@@ -216,9 +218,10 @@ This variant is the default.
- FirstError
Return after the first error occurs.
-- StatusPurpose
-Purpose of a StatusList2021.
-
+- MethodRelationship
+
+- CredentialStatus
+
- StatusCheck
Controls validation behaviour when checking whether or not a credential has been revoked by its
credentialStatus
.
@@ -241,12 +244,6 @@ This variant is the default.
## Functions
-- encodeB64(data) ⇒
string
-Encode the given bytes in url-safe base64.
-
-- decodeB64(data) ⇒
Uint8Array
-Decode the given url-safe base64-encoded slice into its raw bytes.
-
- verifyEd25519(alg, signingInput, decodedSignature, publicKey)
Verify a JWS signature secured with the EdDSA
algorithm and curve Ed25519
.
This function is useful when one is composing a IJwsVerifier
that delegates
@@ -255,6 +252,12 @@ 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.
+- encodeB64(data) ⇒
string
+Encode the given bytes in url-safe base64.
+
+- decodeB64(data) ⇒
Uint8Array
+Decode the given url-safe base64-encoded slice into its raw bytes.
+
- start()
Initializes the console error panic hook for better error messages
@@ -1138,6 +1141,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
@@ -1967,6 +2017,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>
@@ -2039,6 +2090,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>
@@ -4328,6 +4393,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 +4402,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 +4477,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 +4640,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 +4676,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)
@@ -4991,11 +5088,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 +5133,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 +5154,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
@@ -5952,6 +6030,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)
@@ -5969,6 +6048,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)
@@ -6104,17 +6196,11 @@ Deserializes an instance from a JSON object.
| --- | --- |
| json | any
|
-
-
-## StateMetadataEncoding
-**Kind**: global variable
-
+
-## MethodRelationship
-**Kind**: global variable
-
+## StatusPurpose
+Purpose of a [StatusList2021](#StatusList2021).
-## CredentialStatus
**Kind**: global variable
@@ -6142,6 +6228,10 @@ 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.
+**Kind**: global variable
+
+
+## StateMetadataEncoding
**Kind**: global variable
@@ -6161,11 +6251,13 @@ Return all errors that occur during validation.
Return after the first error occurs.
**Kind**: global variable
-
+
-## StatusPurpose
-Purpose of a [StatusList2021](#StatusList2021).
+## MethodRelationship
+**Kind**: global variable
+
+## CredentialStatus
**Kind**: global variable
@@ -6198,28 +6290,6 @@ Validate the status if supported, skip any unsupported
Skip all status checks.
**Kind**: global variable
-
-
-## encodeB64(data) ⇒ string
-Encode the given bytes in url-safe base64.
-
-**Kind**: global function
-
-| Param | Type |
-| --- | --- |
-| data | Uint8Array
|
-
-
-
-## decodeB64(data) ⇒ Uint8Array
-Decode the given url-safe base64-encoded slice into its raw bytes.
-
-**Kind**: global function
-
-| Param | Type |
-| --- | --- |
-| data | Uint8Array
|
-
## verifyEd25519(alg, signingInput, decodedSignature, publicKey)
@@ -6242,6 +6312,28 @@ prior to calling the function.
| decodedSignature | Uint8Array
|
| publicKey | [Jwk
](#Jwk) |
+
+
+## encodeB64(data) ⇒ string
+Encode the given bytes in url-safe base64.
+
+**Kind**: global function
+
+| Param | Type |
+| --- | --- |
+| data | Uint8Array
|
+
+
+
+## decodeB64(data) ⇒ Uint8Array
+Decode the given url-safe base64-encoded slice into its raw bytes.
+
+**Kind**: global function
+
+| Param | Type |
+| --- | --- |
+| data | Uint8Array
|
+
## start()
diff --git a/bindings/wasm/src/iota/iota_document.rs b/bindings/wasm/src/iota/iota_document.rs
index 8f8cbe6823..8d004422ad 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;
@@ -48,6 +50,7 @@ use crate::credential::WasmJws;
use crate::credential::WasmJwt;
use crate::credential::WasmPresentation;
use crate::did::CoreDocumentLock;
+
use crate::did::PromiseJws;
use crate::did::PromiseJwt;
use crate::did::WasmCoreDocument;
@@ -156,6 +159,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 {
@@ -845,6 +862,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/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/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..4b7d297a62 100644
--- a/bindings/wasm/src/verification/wasm_method_type.rs
+++ b/bindings/wasm/src/verification/wasm_method_type.rs
@@ -27,6 +27,11 @@ impl WasmMethodType {
WasmMethodType(MethodType::JSON_WEB_KEY)
}
+ /// 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/examples/Cargo.toml b/examples/Cargo.toml
index a9337d6a33..3b837244dc 100644
--- a/examples/Cargo.toml
+++ b/examples/Cargo.toml
@@ -8,12 +8,12 @@ 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_iota = { path = "../identity_iota", default-features = false, features = ["iota-client", "client", "memstore", "domain-linkage", "revocation-bitmap", "status-list-2021"] }
identity_stronghold = { path = "../identity_stronghold", default-features = false }
iota-sdk = { version = "1.0", default-features = false, features = ["tls", "client", "stronghold"] }
primitive-types = "0.12.1"
rand = "0.8.5"
-sd-jwt-payload = { version = "0.2.0", 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"] }
diff --git a/identity_core/Cargo.toml b/identity_core/Cargo.toml
index 120d6dc9be..7fcba9777c 100644
--- a/identity_core/Cargo.toml
+++ b/identity_core/Cargo.toml
@@ -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 876d5577d5..38c65045e5 100644
--- a/identity_credential/Cargo.toml
+++ b/identity_credential/Cargo.toml
@@ -22,8 +22,8 @@ indexmap = { version = "2.0", default-features = false, features = ["std", "serd
itertools = { version = "0.11", default-features = false, features = ["use_std"], 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, features = ["std"], optional = true }
-sd-jwt-payload = { version = "0.2.0", 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_json.workspace = true
@@ -55,3 +55,6 @@ 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"]
+
+[lints]
+workspace = true
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/error.rs b/identity_credential/src/error.rs
index 356d89d3d2..62964415e4 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")]
diff --git a/identity_credential/src/revocation/status_list_2021/credential.rs b/identity_credential/src/revocation/status_list_2021/credential.rs
index e3f0875216..cc52916967 100644
--- a/identity_credential/src/revocation/status_list_2021/credential.rs
+++ b/identity_credential/src/revocation/status_list_2021/credential.rs
@@ -143,6 +143,21 @@ 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()?;
@@ -167,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 {
@@ -241,7 +275,7 @@ 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") {
@@ -283,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,
})
@@ -363,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,
+ }
})
}
}
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_did/Cargo.toml b/identity_did/Cargo.toml
index 5d23782c8b..9b32efb13b 100644
--- a/identity_did/Cargo.toml
+++ b/identity_did/Cargo.toml
@@ -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_document/Cargo.toml b/identity_document/Cargo.toml
index c25671873d..c2fceac707 100644
--- a/identity_document/Cargo.toml
+++ b/identity_document/Cargo.toml
@@ -28,3 +28,6 @@ serde_json.workspace = true
[[bench]]
name = "deserialize_document"
harness = false
+
+[lints]
+workspace = true
diff --git a/identity_eddsa_verifier/Cargo.toml b/identity_eddsa_verifier/Cargo.toml
index 257fa5d5a4..69ba2ff005 100644
--- a/identity_eddsa_verifier/Cargo.toml
+++ b/identity_eddsa_verifier/Cargo.toml
@@ -18,3 +18,6 @@ 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 bd5aa25125..9910b03c5e 100644
--- a/identity_iota/Cargo.toml
+++ b/identity_iota/Cargo.toml
@@ -69,3 +69,6 @@ sd-jwt = ["identity_credential/sd-jwt"]
# 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 e210d6e10d..69d68defd8 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
@@ -36,8 +36,8 @@ IOTA Identity is a [Rust](https://www.rust-lang.org/) implementation of decentra
- 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
@@ -74,7 +74,7 @@ version = "1.0.0"
edition = "2021"
[dependencies]
-identity_iota = {version = "1.1.1", features = ["memstore"]}
+identity_iota = { version = "1.1.1", features = ["memstore"] }
iota-sdk = { version = "1.0.2", default-features = true, features = ["tls", "client", "stronghold"] }
tokio = { version = "1", features = ["full"] }
anyhow = "1.0.62"
@@ -214,7 +214,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_core/Cargo.toml b/identity_iota_core/Cargo.toml
index c5fbfa1f89..6c55db6b12 100644
--- a/identity_iota_core/Cargo.toml
+++ b/identity_iota_core/Cargo.toml
@@ -53,3 +53,6 @@ revocation-bitmap = ["identity_credential/revocation-bitmap"]
send-sync-client-ext = []
# Disables the blanket implementation of `IotaIdentityClientExt`.
test = ["client"]
+
+[lints]
+workspace = true
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/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 aa2a53f13a..5465f93473 100644
--- a/identity_jose/Cargo.toml
+++ b/identity_jose/Cargo.toml
@@ -29,3 +29,6 @@ signature = { version = "2", default-features = false }
[[example]]
name = "jws_encoding_decoding"
test = true
+
+[lints]
+workspace = true
diff --git a/identity_resolver/Cargo.toml b/identity_resolver/Cargo.toml
index bd28b248ad..76c88ca6c1 100644
--- a/identity_resolver/Cargo.toml
+++ b/identity_resolver/Cargo.toml
@@ -40,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_storage/Cargo.toml b/identity_storage/Cargo.toml
index 75086ccab9..d0f62b08c2 100644
--- a/identity_storage/Cargo.toml
+++ b/identity_storage/Cargo.toml
@@ -42,3 +42,6 @@ memstore = ["dep:tokio", "dep:rand", "dep:iota-crypto"]
send-sync-storage = []
# Implements the JwkStorageDocumentExt trait for IotaDocument
iota-document = ["dep:identity_iota_core"]
+
+[lints]
+workspace = true
diff --git a/identity_stronghold/Cargo.toml b/identity_stronghold/Cargo.toml
index e45a6d4eb7..dccb20b695 100644
--- a/identity_stronghold/Cargo.toml
+++ b/identity_stronghold/Cargo.toml
@@ -30,3 +30,6 @@ tokio = { version = "1.29.0", default-features = false, features = ["macros", "s
default = []
# Enables `Send` + `Sync` bounds for the trait implementations on `StrongholdStorage`.
send-sync-storage = ["identity_storage/send-sync-storage"]
+
+[lints]
+workspace = true
diff --git a/identity_verification/Cargo.toml b/identity_verification/Cargo.toml
index 1b6bb11d77..0e19f1fadf 100644
--- a/identity_verification/Cargo.toml
+++ b/identity_verification/Cargo.toml
@@ -13,8 +13,11 @@ identity_core = { version = "=1.1.1", path = "./../identity_core", default-featu
identity_did = { version = "=1.1.1", path = "./../identity_did", default-features = false }
identity_jose = { version = "=1.1.1", 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..8c48e06893 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")]
@@ -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..ae3877948d 100644
--- a/identity_verification/src/verification_method/method_type.rs
+++ b/identity_verification/src/verification_method/method_type.rs
@@ -25,6 +25,10 @@ impl MethodType {
/// 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: Self = Self(Cow::Borrowed(JSON_WEB_KEY_METHOD_TYPE));
+ /// Construct a custom method type.
+ pub fn custom(type_: impl AsRef) -> Self {
+ Self(Cow::Owned(type_.as_ref().to_owned()))
+ }
}
impl MethodType {
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;