Skip to content

Commit

Permalink
gRPC bindings (#1264)
Browse files Browse the repository at this point in the history
  • Loading branch information
UMR1352 authored Mar 26, 2024
1 parent e53561e commit e68538f
Show file tree
Hide file tree
Showing 45 changed files with 3,144 additions and 9 deletions.
3 changes: 3 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
target/
bindings/wasm/
bindings/grpc/target/
41 changes: 41 additions & 0 deletions .github/workflows/build-and-test-grpc.yml
Original file line number Diff line number Diff line change
@@ -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
52 changes: 52 additions & 0 deletions .github/workflows/grpc-publish-to-dockerhub.yml
Original file line number Diff line number Diff line change
@@ -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 }}

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ 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:
Expand Down
43 changes: 43 additions & 0 deletions bindings/grpc/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
20 changes: 20 additions & 0 deletions bindings/grpc/Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
130 changes: 130 additions & 0 deletions bindings/grpc/README.md
Original file line number Diff line number Diff line change
@@ -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.
14 changes: 14 additions & 0 deletions bindings/grpc/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright 2020-2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

fn main() -> Result<(), Box<dyn std::error::Error>> {
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(())
}
61 changes: 61 additions & 0 deletions bindings/grpc/proto/credentials.proto
Original file line number Diff line number Diff line change
@@ -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<string, string> 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);
}
Loading

0 comments on commit e68538f

Please sign in to comment.