29 Commits

Author SHA1 Message Date
hdbg
088fa6fe72 feat(evm): add grant management for EVM wallets
Some checks failed
ci/woodpecker/pr/server-audit Pipeline was successful
ci/woodpecker/pr/server-vet Pipeline failed
ci/woodpecker/pr/server-lint Pipeline failed
ci/woodpecker/pr/server-test Pipeline was successful
2026-03-16 18:53:10 +01:00
hdbg
6ed8150e48 feat(useragent::evm): basic list & creation 2026-03-16 04:19:43 +01:00
hdbg
fac312d860 refactor(server): move connection-related handlers into separate module 2026-03-16 03:41:06 +01:00
hdbg
549a0f5f52 refactor(server): removed grpc adapter and replaced with concrete implementations 2026-03-16 03:12:29 +01:00
hdbg
4db102b3d1 feat(useragent): bootstrap / unseal flow implementattion 2026-03-15 23:08:10 +01:00
hdbg
c61a9e30ac feat(useragent): initial connection impl 2026-03-15 22:10:24 +01:00
hdbg
27836beb75 fix(server::user_agent::auth): not sending AuthOk on succesful auth 2026-03-15 22:09:59 +01:00
hdbg
ec0e8a980c feat(useragent): added connection info setup screen 2026-03-15 16:48:03 +01:00
hdbg
16d5b9a233 feat(useragent): settled on routing architecture 2026-03-15 16:46:58 +01:00
hdbg
62c4bc5ade feat(useragent): initial impl 2026-03-15 16:46:58 +01:00
hdbg
ccd657c9ec fix(server): enabled crypto provider for rustls
Some checks failed
ci/woodpecker/push/server-audit Pipeline was successful
ci/woodpecker/push/server-vet Pipeline failed
ci/woodpecker/push/server-lint Pipeline was successful
ci/woodpecker/push/server-test Pipeline was successful
2026-03-15 16:46:39 +01:00
hdbg
013af7e65f fix(server): remove useless vendored protoc 2026-03-15 16:43:30 +01:00
84978afd58 fix(clippy): forbidden methods
Some checks failed
ci/woodpecker/push/server-audit Pipeline was successful
ci/woodpecker/push/server-lint Pipeline was successful
ci/woodpecker/push/server-vet Pipeline failed
ci/woodpecker/push/server-test Pipeline was successful
2026-03-14 17:08:59 +00:00
CleverWild
4cb5b303dc security: audit some crates
Some checks failed
ci/woodpecker/pr/server-audit Pipeline was successful
ci/woodpecker/pr/server-lint Pipeline was successful
ci/woodpecker/pr/server-vet Pipeline failed
ci/woodpecker/pr/server-test Pipeline was successful
ci/woodpecker/push/server-test Pipeline failed
ci/woodpecker/push/server-audit Pipeline failed
ci/woodpecker/push/server-vet Pipeline failed
ci/woodpecker/push/server-lint Pipeline failed
2026-03-14 17:58:36 +01:00
8fde3cec41 Merge pull request 'feat(user-agent-auth): add RSA and ECDSA auth key types' (#29) from feat-min-RSA-&-ECDSA-auth-pipeline into main
Some checks failed
ci/woodpecker/push/server-audit Pipeline was successful
ci/woodpecker/push/server-lint Pipeline was successful
ci/woodpecker/push/server-vet Pipeline failed
ci/woodpecker/push/server-test Pipeline was successful
Reviewed-on: #29
Reviewed-by: Stas <business@jexter.tech>
2026-03-14 14:41:46 +00:00
17ac195c5d clippy: fix
Some checks failed
ci/woodpecker/pr/server-audit Pipeline was successful
ci/woodpecker/pr/server-vet Pipeline failed
ci/woodpecker/pr/server-lint Pipeline was successful
ci/woodpecker/pr/server-test Pipeline was successful
2026-03-14 14:30:46 +01:00
c1c5d14133 fix(rustc): config toolchaing mismatch
Some checks failed
ci/woodpecker/pr/server-audit Pipeline was successful
ci/woodpecker/pr/server-vet Pipeline failed
ci/woodpecker/pr/server-lint Pipeline failed
ci/woodpecker/pr/server-test Pipeline was successful
2026-03-14 14:13:15 +01:00
47144bdf81 feat(auth): limited RSA support for signing
Some checks failed
ci/woodpecker/pr/server-lint Pipeline failed
ci/woodpecker/pr/server-audit Pipeline was successful
ci/woodpecker/pr/server-test Pipeline failed
ci/woodpecker/pr/server-vet Pipeline failed
see server/clippy.toml
2026-03-14 13:57:13 +01:00
42760bbd79 revert(auth): remove RSA support from authentication and related components
Some checks failed
ci/woodpecker/pr/server-audit Pipeline was successful
ci/woodpecker/pr/server-vet Pipeline failed
ci/woodpecker/pr/server-lint Pipeline failed
ci/woodpecker/pr/server-test Pipeline was successful
2026-03-14 13:23:06 +01:00
d29bca853b chore: squash migrations 2026-03-14 13:22:47 +01:00
f8d27a1454 refactor(config): specify target for Windows in profile.dev settings
Some checks failed
ci/woodpecker/pr/server-audit Pipeline failed
ci/woodpecker/pr/server-vet Pipeline failed
ci/woodpecker/pr/server-lint Pipeline failed
ci/woodpecker/pr/server-test Pipeline was successful
2026-03-14 12:40:37 +01:00
6030f30901 feat(user-agent-auth): add RSA and ECDSA auth key types
Some checks failed
ci/woodpecker/pr/server-lint Pipeline failed
ci/woodpecker/pr/server-audit Pipeline failed
ci/woodpecker/pr/server-test Pipeline failed
ci/woodpecker/pr/server-vet Pipeline failed
Extend user-agent authentication to support Ed25519, ECDSA (secp256k1), and RSA (PSS+SHA-256) with minimal protocol and storage changes. Add key_type to auth requests and useragent_client, update key parsing/signature verification paths, and keep backward compatibility by treating UNSPECIFIED as Ed25519.
2026-03-14 12:14:30 +01:00
a3c401194f fix: my having come back 2026-03-13 16:59:37 +01:00
hdbg
6386510f52 merge: evm into main
Some checks failed
ci/woodpecker/push/server-audit Pipeline was successful
ci/woodpecker/push/server-lint Pipeline failed
ci/woodpecker/push/server-vet Pipeline failed
ci/woodpecker/push/server-test Pipeline was successful
2026-03-12 16:29:00 +01:00
ec36e5c2ea Merge pull request 'refactor(server::client::auth): Approval flow for first-time connections and simplified to keep state on stack' (#26) from push-xxmwpvvwnllx into main
Some checks failed
ci/woodpecker/push/server-lint Pipeline failed
ci/woodpecker/push/server-audit Pipeline was successful
ci/woodpecker/push/server-vet Pipeline failed
ci/woodpecker/push/server-test Pipeline was successful
Reviewed-on: #26
2026-03-12 15:22:09 +00:00
hdbg
ba86d18250 refactor(server::client::auth): removed state machine and added approval flow coordination
Some checks failed
ci/woodpecker/pr/server-audit Pipeline was successful
ci/woodpecker/pr/server-vet Pipeline failed
ci/woodpecker/pr/server-lint Pipeline failed
ci/woodpecker/pr/server-test Pipeline was successful
2026-03-12 16:12:19 +01:00
hdbg
606a1f3774 feat(server::{router, useragent}): inter-actor approval coordination 2026-03-11 20:07:06 +01:00
hdbg
b3a67ffc00 feat(server::client): proper connect error 2026-03-11 17:58:44 +01:00
hdbg
168290040c feat(server::client): approval flow through user-agent on first-time client connects 2026-03-11 16:31:58 +01:00
162 changed files with 19213 additions and 2827 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
target/
scripts/__pycache__/
.DS_Store
.cargo/config.toml

View File

@@ -1,3 +1,2 @@
{
"git.enabled": false
}

128
AGENTS.md Normal file
View File

@@ -0,0 +1,128 @@
# AGENTS.md
This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.
## Project Overview
Arbiter is a **permissioned signing service** for cryptocurrency wallets. It consists of:
- **`server/`** — Rust gRPC daemon that holds encrypted keys and enforces policies
- **`useragent/`** — Flutter desktop app (macOS/Windows) with a Rust backend via Rinf
- **`protobufs/`** — Protocol Buffer definitions shared between server and client
The vault never exposes key material; it only produces signatures when requests satisfy configured policies.
## Toolchain Setup
Tools are managed via [mise](https://mise.jdx.dev/). Install all required tools:
```sh
mise install
```
Key versions: Rust 1.93.0 (with clippy), Flutter 3.38.9-stable, protoc 29.6, diesel_cli 2.3.6 (sqlite).
## Server (Rust workspace at `server/`)
### Crates
| Crate | Purpose |
|---|---|
| `arbiter-proto` | Generated gRPC stubs + protobuf types; compiled from `protobufs/*.proto` via `tonic-prost-build` |
| `arbiter-server` | Main daemon — actors, DB, EVM policy engine, gRPC service implementation |
| `arbiter-useragent` | Rust client library for the user agent side of the gRPC protocol |
| `arbiter-client` | Rust client library for SDK clients |
### Common Commands
```sh
cd server
# Build
cargo build
# Run the server daemon
cargo run -p arbiter-server
# Run all tests (preferred over cargo test)
cargo nextest run
# Run a single test
cargo nextest run <test_name>
# Lint
cargo clippy
# Security audit
cargo audit
# Check unused dependencies
cargo shear
# Run snapshot tests and update snapshots
cargo insta review
```
### Architecture
The server is actor-based using the **kameo** crate. All long-lived state lives in `GlobalActors`:
- **`Bootstrapper`** — Manages the one-time bootstrap token written to `~/.arbiter/bootstrap_token` on first run.
- **`KeyHolder`** — Holds the encrypted root key and manages the Sealed/Unsealed vault state machine. On unseal, decrypts the root key into a `memsafe` hardened memory cell.
- **`MessageRouter`** — Coordinates streaming messages between user agents and SDK clients.
- **`EvmActor`** — Handles EVM transaction policy enforcement and signing.
Per-connection actors live under `actors/user_agent/` and `actors/client/`, each with `auth` (challenge-response authentication) and `session` (post-auth operations) sub-modules.
**Database:** SQLite via `diesel-async` + `bb8` connection pool. Schema managed by embedded Diesel migrations in `crates/arbiter-server/migrations/`. DB file lives at `~/.arbiter/arbiter.sqlite`. Tests use a temp-file DB via `db::create_test_pool()`.
**Cryptography:**
- Authentication: ed25519 (challenge-response, nonce-tracked per peer)
- Encryption at rest: XChaCha20-Poly1305 (versioned via `scheme` field for transparent migration on unseal)
- Password KDF: Argon2
- Unseal transport: X25519 ephemeral key exchange
- TLS: self-signed certificate (aws-lc-rs backend), fingerprint distributed via `ArbiterUrl`
**Protocol:** gRPC with Protocol Buffers. The `ArbiterUrl` type encodes host, port, CA cert, and bootstrap token into a single shareable string (printed to console on first run).
### Proto Regeneration
When `.proto` files in `protobufs/` change, rebuild to regenerate:
```sh
cd server && cargo build -p arbiter-proto
```
### Database Migrations
```sh
# Create a new migration
diesel migration generate <name> --migration-dir crates/arbiter-server/migrations
# Run migrations manually (server also runs them on startup)
diesel migration run --migration-dir crates/arbiter-server/migrations
```
## User Agent (Flutter + Rinf at `useragent/`)
The Flutter app uses [Rinf](https://rinf.cunarist.org) to call Rust code. The Rust logic lives in `useragent/native/hub/` as a separate crate that uses `arbiter-useragent` for the gRPC client.
Communication between Dart and Rust uses typed **signals** defined in `useragent/native/hub/src/signals/`. After modifying signal structs, regenerate Dart bindings:
```sh
cd useragent && rinf gen
```
### Common Commands
```sh
cd useragent
# Run the app (macOS or Windows)
flutter run
# Regenerate Rust↔Dart signal bindings
rinf gen
# Analyze Dart code
flutter analyze
```
The Rinf Rust entry point is `useragent/native/hub/src/lib.rs`. It spawns actors defined in `useragent/native/hub/src/actors/` which handle Dart↔server communication via signals.

128
CLAUDE.md Normal file
View File

@@ -0,0 +1,128 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Arbiter is a **permissioned signing service** for cryptocurrency wallets. It consists of:
- **`server/`** — Rust gRPC daemon that holds encrypted keys and enforces policies
- **`useragent/`** — Flutter desktop app (macOS/Windows) with a Rust backend via Rinf
- **`protobufs/`** — Protocol Buffer definitions shared between server and client
The vault never exposes key material; it only produces signatures when requests satisfy configured policies.
## Toolchain Setup
Tools are managed via [mise](https://mise.jdx.dev/). Install all required tools:
```sh
mise install
```
Key versions: Rust 1.93.0 (with clippy), Flutter 3.38.9-stable, protoc 29.6, diesel_cli 2.3.6 (sqlite).
## Server (Rust workspace at `server/`)
### Crates
| Crate | Purpose |
|---|---|
| `arbiter-proto` | Generated gRPC stubs + protobuf types; compiled from `protobufs/*.proto` via `tonic-prost-build` |
| `arbiter-server` | Main daemon — actors, DB, EVM policy engine, gRPC service implementation |
| `arbiter-useragent` | Rust client library for the user agent side of the gRPC protocol |
| `arbiter-client` | Rust client library for SDK clients |
### Common Commands
```sh
cd server
# Build
cargo build
# Run the server daemon
cargo run -p arbiter-server
# Run all tests (preferred over cargo test)
cargo nextest run
# Run a single test
cargo nextest run <test_name>
# Lint
cargo clippy
# Security audit
cargo audit
# Check unused dependencies
cargo shear
# Run snapshot tests and update snapshots
cargo insta review
```
### Architecture
The server is actor-based using the **kameo** crate. All long-lived state lives in `GlobalActors`:
- **`Bootstrapper`** — Manages the one-time bootstrap token written to `~/.arbiter/bootstrap_token` on first run.
- **`KeyHolder`** — Holds the encrypted root key and manages the Sealed/Unsealed vault state machine. On unseal, decrypts the root key into a `memsafe` hardened memory cell.
- **`MessageRouter`** — Coordinates streaming messages between user agents and SDK clients.
- **`EvmActor`** — Handles EVM transaction policy enforcement and signing.
Per-connection actors live under `actors/user_agent/` and `actors/client/`, each with `auth` (challenge-response authentication) and `session` (post-auth operations) sub-modules.
**Database:** SQLite via `diesel-async` + `bb8` connection pool. Schema managed by embedded Diesel migrations in `crates/arbiter-server/migrations/`. DB file lives at `~/.arbiter/arbiter.sqlite`. Tests use a temp-file DB via `db::create_test_pool()`.
**Cryptography:**
- Authentication: ed25519 (challenge-response, nonce-tracked per peer)
- Encryption at rest: XChaCha20-Poly1305 (versioned via `scheme` field for transparent migration on unseal)
- Password KDF: Argon2
- Unseal transport: X25519 ephemeral key exchange
- TLS: self-signed certificate (aws-lc-rs backend), fingerprint distributed via `ArbiterUrl`
**Protocol:** gRPC with Protocol Buffers. The `ArbiterUrl` type encodes host, port, CA cert, and bootstrap token into a single shareable string (printed to console on first run).
### Proto Regeneration
When `.proto` files in `protobufs/` change, rebuild to regenerate:
```sh
cd server && cargo build -p arbiter-proto
```
### Database Migrations
```sh
# Create a new migration
diesel migration generate <name> --migration-dir crates/arbiter-server/migrations
# Run migrations manually (server also runs them on startup)
diesel migration run --migration-dir crates/arbiter-server/migrations
```
## User Agent (Flutter + Rinf at `useragent/`)
The Flutter app uses [Rinf](https://rinf.cunarist.org) to call Rust code. The Rust logic lives in `useragent/native/hub/` as a separate crate that uses `arbiter-useragent` for the gRPC client.
Communication between Dart and Rust uses typed **signals** defined in `useragent/native/hub/src/signals/`. After modifying signal structs, regenerate Dart bindings:
```sh
cd useragent && rinf gen
```
### Common Commands
```sh
cd useragent
# Run the app (macOS or Windows)
flutter run
# Regenerate Rust↔Dart signal bindings
rinf gen
# Analyze Dart code
flutter analyze
```
The Rinf Rust entry point is `useragent/native/hub/src/lib.rs`. It spawns actors defined in `useragent/native/hub/src/actors/` which handle Dart↔server communication via signals.

View File

@@ -4,6 +4,52 @@ This document covers concrete technology choices and dependencies. For the archi
---
## Client Connection Flow
### New Client Approval
When a client whose public key is not yet in the database connects, all connected user agents are asked to approve the connection. The first agent to respond determines the outcome; remaining requests are cancelled via a watch channel.
```mermaid
flowchart TD
A([Client connects]) --> B[Receive AuthChallengeRequest]
B --> C{pubkey in DB?}
C -- yes --> D[Read nonce\nIncrement nonce in DB]
D --> G
C -- no --> E[Ask all UserAgents:\nClientConnectionRequest]
E --> F{First response}
F -- denied --> Z([Reject connection])
F -- approved --> F2[Cancel remaining\nUserAgent requests]
F2 --> F3[INSERT client\nnonce = 1]
F3 --> G[Send AuthChallenge\nwith nonce]
G --> H[Receive AuthChallengeSolution]
H --> I{Signature valid?}
I -- no --> Z
I -- yes --> J([Session started])
```
### Known Issue: Concurrent Registration Race (TOCTOU)
Two connections presenting the same previously-unknown public key can race through the approval flow simultaneously:
1. Both check the DB → neither is registered.
2. Both request approval from user agents → both receive approval.
3. Both `INSERT` the client record → the second insert silently overwrites the first, resetting the nonce.
This means the first connection's nonce is invalidated by the second, causing its challenge verification to fail. A fix requires either serialising new-client registration (e.g. an in-memory lock keyed on pubkey) or replacing the separate check + insert with an `INSERT OR IGNORE` / upsert guarded by a unique constraint on `public_key`.
### Nonce Semantics
The `program_client.nonce` column stores the **next usable nonce** — i.e. it is always one ahead of the nonce last issued in a challenge.
- **New client:** inserted with `nonce = 1`; the first challenge is issued with `nonce = 0`.
- **Existing client:** the current DB value is read and used as the challenge nonce, then immediately incremented within the same exclusive transaction, preventing replay.
---
## Cryptography
### Authentication

View File

@@ -42,6 +42,10 @@ backend = "cargo:diesel_cli"
default-features = "false"
features = "sqlite,sqlite-bundled"
[[tools."cargo:rinf_cli"]]
version = "8.9.1"
backend = "cargo:rinf_cli"
[[tools.flutter]]
version = "3.38.9-stable"
backend = "asdf:flutter"

View File

@@ -10,3 +10,11 @@ protoc = "29.6"
"cargo:cargo-shear" = "latest"
"cargo:cargo-insta" = "1.46.3"
python = "3.14.3"
[tasks.codegen]
sources = ['protobufs/*.proto']
outputs = ['useragent/lib/proto/*']
run = '''
dart pub global activate protoc_plugin && \
protoc --dart_out=grpc:useragent/lib/proto --proto_path=protobufs/ protobufs/*.proto
'''

View File

@@ -23,15 +23,23 @@ message ClientRequest {
oneof payload {
AuthChallengeRequest auth_challenge_request = 1;
AuthChallengeSolution auth_challenge_solution = 2;
arbiter.evm.EvmSignTransactionRequest evm_sign_transaction = 3;
arbiter.evm.EvmAnalyzeTransactionRequest evm_analyze_transaction = 4;
}
}
message ClientConnectError {
enum Code {
UNKNOWN = 0;
APPROVAL_DENIED = 1;
NO_USER_AGENTS_ONLINE = 2;
}
Code code = 1;
}
message ClientResponse {
oneof payload {
AuthChallenge auth_challenge = 1;
AuthOk auth_ok = 2;
ClientConnectError client_connect_error = 5;
arbiter.evm.EvmSignTransactionResponse evm_sign_transaction = 3;
arbiter.evm.EvmAnalyzeTransactionResponse evm_analyze_transaction = 4;
}

View File

@@ -5,9 +5,17 @@ package arbiter.user_agent;
import "google/protobuf/empty.proto";
import "evm.proto";
enum KeyType {
KEY_TYPE_UNSPECIFIED = 0;
KEY_TYPE_ED25519 = 1;
KEY_TYPE_ECDSA_SECP256K1 = 2;
KEY_TYPE_RSA = 3;
}
message AuthChallengeRequest {
bytes pubkey = 1;
optional string bootstrap_token = 2;
KeyType key_type = 3;
}
message AuthChallenge {
@@ -34,6 +42,12 @@ message UnsealEncryptedKey {
bytes associated_data = 3;
}
message BootstrapEncryptedKey {
bytes nonce = 1;
bytes ciphertext = 2;
bytes associated_data = 3;
}
enum UnsealResult {
UNSEAL_RESULT_UNSPECIFIED = 0;
UNSEAL_RESULT_SUCCESS = 1;
@@ -41,6 +55,13 @@ enum UnsealResult {
UNSEAL_RESULT_UNBOOTSTRAPPED = 3;
}
enum BootstrapResult {
BOOTSTRAP_RESULT_UNSPECIFIED = 0;
BOOTSTRAP_RESULT_SUCCESS = 1;
BOOTSTRAP_RESULT_ALREADY_BOOTSTRAPPED = 2;
BOOTSTRAP_RESULT_INVALID_KEY = 3;
}
enum VaultState {
VAULT_STATE_UNSPECIFIED = 0;
VAULT_STATE_UNBOOTSTRAPPED = 1;
@@ -49,6 +70,16 @@ enum VaultState {
VAULT_STATE_ERROR = 4;
}
message ClientConnectionRequest {
bytes pubkey = 1;
}
message ClientConnectionResponse {
bool approved = 1;
}
message ClientConnectionCancel {}
message UserAgentRequest {
oneof payload {
AuthChallengeRequest auth_challenge_request = 1;
@@ -61,6 +92,8 @@ message UserAgentRequest {
arbiter.evm.EvmGrantCreateRequest evm_grant_create = 8;
arbiter.evm.EvmGrantDeleteRequest evm_grant_delete = 9;
arbiter.evm.EvmGrantListRequest evm_grant_list = 10;
ClientConnectionResponse client_connection_response = 11;
BootstrapEncryptedKey bootstrap_encrypted_key = 12;
}
}
message UserAgentResponse {
@@ -75,5 +108,8 @@ message UserAgentResponse {
arbiter.evm.EvmGrantCreateResponse evm_grant_create = 8;
arbiter.evm.EvmGrantDeleteResponse evm_grant_delete = 9;
arbiter.evm.EvmGrantListResponse evm_grant_list = 10;
ClientConnectionRequest client_connection_request = 11;
ClientConnectionCancel client_connection_cancel = 12;
BootstrapResult bootstrap_result = 13;
}
}

13
server/.cargo/audit.toml Normal file
View File

@@ -0,0 +1,13 @@
[advisories]
# RUSTSEC-2023-0071: Marvin Attack timing side-channel in rsa crate.
# No fixed version is available upstream.
# RSA support is required for Windows Hello / KeyCredentialManager
# (https://learn.microsoft.com/en-us/uwp/api/windows.security.credentials.keycredentialmanager.requestcreateasync),
# which only issues RSA-2048 keys.
# Mitigations in place:
# - Signing uses BlindedSigningKey (PSS+SHA-256), which applies blinding to
# protect the private key from timing recovery during signing.
# - RSA decryption is never performed; we only verify public-key signatures.
# - The attack requires local, high-resolution timing access against the
# signing process, which is not exposed in our threat model.
ignore = ["RUSTSEC-2023-0071"]

191
server/Cargo.lock generated
View File

@@ -654,7 +654,7 @@ version = "1.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fa0c53e8c1e1ef4d01066b01c737fb62fc9397ab52c6e7bb5669f97d281b9bc"
dependencies = [
"darling",
"darling 0.21.3",
"proc-macro2",
"quote",
"syn 2.0.117",
@@ -727,12 +727,16 @@ dependencies = [
"memsafe",
"miette",
"pem",
"prost-types",
"rand 0.10.0",
"rcgen",
"restructed",
"rsa",
"rustls",
"secrecy",
"sha2 0.10.9",
"smlang",
"spki",
"strum",
"test-log",
"thiserror",
@@ -752,25 +756,6 @@ dependencies = [
"alloy",
]
[[package]]
name = "arbiter-useragent"
version = "0.1.0"
dependencies = [
"arbiter-proto",
"async-trait",
"ed25519-dalek",
"http",
"kameo",
"rustls-webpki",
"smlang",
"thiserror",
"tokio",
"tokio-stream",
"tonic",
"tracing",
"x25519-dalek",
]
[[package]]
name = "argon2"
version = "0.5.3"
@@ -1334,9 +1319,9 @@ dependencies = [
[[package]]
name = "c-kzg"
version = "2.1.6"
version = "2.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a0f582957c24870b7bfd12bf562c40b4734b533cafbaf8ded31d6d85f462c01"
checksum = "6648ed1e4ea8e8a1a4a2c78e1cda29a3fd500bc622899c340d8525ea9a76b24a"
dependencies = [
"blst",
"cc",
@@ -1349,9 +1334,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.56"
version = "1.2.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423"
dependencies = [
"find-msvc-tools",
"jobserver",
@@ -1615,7 +1600,7 @@ dependencies = [
"cfg-if",
"cpufeatures 0.2.17",
"curve25519-dalek-derive",
"digest 0.11.1",
"digest 0.11.2",
"fiat-crypto 0.3.0",
"rustc_version 0.4.1",
"subtle",
@@ -1639,8 +1624,18 @@ version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0"
dependencies = [
"darling_core",
"darling_macro",
"darling_core 0.21.3",
"darling_macro 0.21.3",
]
[[package]]
name = "darling"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
dependencies = [
"darling_core 0.23.0",
"darling_macro 0.23.0",
]
[[package]]
@@ -1658,13 +1653,37 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "darling_core"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0"
dependencies = [
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn 2.0.117",
]
[[package]]
name = "darling_macro"
version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81"
dependencies = [
"darling_core",
"darling_core 0.21.3",
"quote",
"syn 2.0.117",
]
[[package]]
name = "darling_macro"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
dependencies = [
"darling_core 0.23.0",
"quote",
"syn 2.0.117",
]
@@ -1696,6 +1715,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
dependencies = [
"const-oid",
"pem-rfc7468",
"zeroize",
]
@@ -1759,9 +1779,9 @@ dependencies = [
[[package]]
name = "diesel"
version = "2.3.6"
version = "2.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b6c2fc184a6fb6ebcf5f9a5e3bbfa84d8fd268cdfcce4ed508979a6259494d"
checksum = "f4ae09a41a4b89f94ec1e053623da8340d996bc32c6517d325a9daad9b239358"
dependencies = [
"chrono",
"diesel_derives",
@@ -1844,9 +1864,9 @@ dependencies = [
[[package]]
name = "digest"
version = "0.11.1"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "285743a676ccb6b3e116bc14cc69319b957867930ae9c4822f8e0f54509d7243"
checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c"
dependencies = [
"block-buffer 0.12.0",
"crypto-common 0.2.1",
@@ -1875,7 +1895,7 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd122633e4bef06db27737f21d3738fb89c8f6d5360d6d9d7635dda142a7757e"
dependencies = [
"darling",
"darling 0.21.3",
"either",
"heck",
"proc-macro2",
@@ -2876,6 +2896,9 @@ name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
dependencies = [
"spin",
]
[[package]]
name = "leb128fmt"
@@ -2897,9 +2920,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
[[package]]
name = "libsqlite3-sys"
version = "0.35.0"
version = "0.36.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f"
checksum = "95b4103cffefa72eb8428cb6b47d6627161e51c2739fc5e3b734584157bc642a"
dependencies = [
"pkg-config",
"vcpkg",
@@ -3109,6 +3132,22 @@ dependencies = [
"num-traits",
]
[[package]]
name = "num-bigint-dig"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7"
dependencies = [
"lazy_static",
"libm",
"num-integer",
"num-iter",
"num-traits",
"rand 0.8.5",
"smallvec",
"zeroize",
]
[[package]]
name = "num-conv"
version = "0.2.0"
@@ -3124,6 +3163,17 @@ dependencies = [
"num-traits",
]
[[package]]
name = "num-iter"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.19"
@@ -3199,9 +3249,9 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.21.3"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "opaque-debug"
@@ -3293,6 +3343,15 @@ dependencies = [
"serde_core",
]
[[package]]
name = "pem-rfc7468"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
dependencies = [
"base64ct",
]
[[package]]
name = "percent-encoding"
version = "2.3.2"
@@ -3352,6 +3411,17 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkcs1"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
dependencies = [
"der",
"pkcs8",
"spki",
]
[[package]]
name = "pkcs8"
version = "0.10.2"
@@ -3935,6 +4005,27 @@ dependencies = [
"rustc-hex",
]
[[package]]
name = "rsa"
version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d"
dependencies = [
"const-oid",
"digest 0.10.7",
"num-bigint-dig",
"num-integer",
"num-traits",
"pkcs1",
"pkcs8",
"rand_core 0.6.4",
"sha2 0.10.9",
"signature 2.2.0",
"spki",
"subtle",
"zeroize",
]
[[package]]
name = "rsqlite-vfs"
version = "0.1.0"
@@ -4302,9 +4393,9 @@ dependencies = [
[[package]]
name = "serde_with"
version = "3.17.0"
version = "3.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "381b283ce7bc6b476d903296fb59d0d36633652b633b27f64db4fb46dcbfc3b9"
checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f"
dependencies = [
"base64",
"chrono",
@@ -4321,11 +4412,11 @@ dependencies = [
[[package]]
name = "serde_with_macros"
version = "3.17.0"
version = "3.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6d4e30573c8cb306ed6ab1dca8423eec9a463ea0e155f45399455e0368b27e0"
checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65"
dependencies = [
"darling",
"darling 0.23.0",
"proc-macro2",
"quote",
"syn 2.0.117",
@@ -4360,7 +4451,7 @@ checksum = "7c5f3b1e2dc8aad28310d8410bd4d7e180eca65fca176c52ab00d364475d0024"
dependencies = [
"cfg-if",
"cpufeatures 0.2.17",
"digest 0.11.1",
"digest 0.11.2",
]
[[package]]
@@ -4482,6 +4573,12 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "spin"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
[[package]]
name = "spki"
version = "0.7.3"
@@ -4771,9 +4868,9 @@ dependencies = [
[[package]]
name = "tinyvec"
version = "1.10.0"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa"
checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
dependencies = [
"tinyvec_macros",
]
@@ -5065,9 +5162,9 @@ dependencies = [
[[package]]
name = "tracing-subscriber"
version = "0.3.22"
version = "0.3.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
dependencies = [
"matchers",
"nu-ansi-term",

View File

@@ -4,6 +4,9 @@ members = [
]
resolver = "3"
[workspace.lints.clippy]
disallowed-methods = "deny"
[workspace.dependencies]
tonic = { version = "0.14.3", features = [
@@ -17,7 +20,7 @@ tokio = { version = "1.49.0", features = ["full"] }
ed25519-dalek = { version = "3.0.0-pre.6", features = ["rand_core"] }
chrono = { version = "0.4.43", features = ["serde"] }
rand = "0.10.0"
rustls = "0.23.36"
rustls = { version = "0.23.36", features = ["aws-lc-rs"] }
smlang = "0.8.0"
miette = { version = "7.6.0", features = ["fancy", "serde"] }
thiserror = "2.0.18"
@@ -36,3 +39,7 @@ rcgen = { version = "0.14.7", features = [
"x509-parser",
"zeroize",
], default-features = false }
k256 = { version = "0.13.4", features = ["ecdsa", "pkcs8"] }
rsa = { version = "0.9", features = ["sha2"] }
sha2 = "0.10"
spki = "0.7"

9
server/clippy.toml Normal file
View File

@@ -0,0 +1,9 @@
disallowed-methods = [
# RSA decryption is forbidden: the rsa crate has RUSTSEC-2023-0071 (Marvin Attack).
# We only use RSA for Windows Hello (KeyCredentialManager) public-key verification — decryption
# is never required and must not be introduced.
{ path = "rsa::RsaPrivateKey::decrypt", reason = "RSA decryption is forbidden (RUSTSEC-2023-0071 Marvin Attack). Only PSS signing/verification is permitted." },
{ path = "rsa::RsaPrivateKey::decrypt_blinded", reason = "RSA decryption is forbidden (RUSTSEC-2023-0071 Marvin Attack). Only PSS signing/verification is permitted." },
{ path = "rsa::traits::Decryptor::decrypt", reason = "RSA decryption is forbidden (RUSTSEC-2023-0071 Marvin Attack). This blocks decrypt() on rsa::{pkcs1v15,oaep}::DecryptingKey." },
{ path = "rsa::traits::RandomizedDecryptor::decrypt_with_rng", reason = "RSA decryption is forbidden (RUSTSEC-2023-0071 Marvin Attack). This blocks decrypt_with_rng() on rsa::{pkcs1v15,oaep}::DecryptingKey." },
]

View File

@@ -3,7 +3,6 @@ use tonic_prost_build::configure;
static PROTOBUF_DIR: &str = "../../../protobufs";
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("cargo::rerun-if-changed={PROTOBUF_DIR}");
configure()
@@ -17,7 +16,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
],
&[PROTOBUF_DIR.to_string()],
)
.unwrap();
Ok(())
}

View File

@@ -1,78 +1,39 @@
//! Transport-facing abstractions for protocol/session code.
//! Transport-facing abstractions shared by protocol/session code.
//!
//! This module separates three concerns:
//!
//! - protocol/session logic wants a small duplex interface ([`Bi`])
//! - transport adapters push concrete stream items to an underlying IO layer
//! - transport boundaries translate between protocol-facing and transport-facing
//! item types via direction-specific converters
//! This module defines a small duplex interface, [`Bi`], that actors and other
//! protocol code can depend on without knowing anything about the concrete
//! transport underneath.
//!
//! [`Bi`] is intentionally minimal and transport-agnostic:
//! - [`Bi::recv`] yields inbound protocol messages
//! - [`Bi::send`] accepts outbound protocol/domain items
//! - [`Bi::recv`] yields inbound messages
//! - [`Bi::send`] accepts outbound messages
//!
//! Transport-specific adapters, including protobuf or gRPC bridges, live in the
//! crates that own those boundaries rather than in `arbiter-proto`.
//!
//! # Generic Ordering Rule
//!
//! This module uses a single convention consistently: when a type or trait is
//! parameterized by protocol message directions, the generic parameters are
//! declared as `Inbound` first, then `Outbound`.
//! This module consistently uses `Inbound` first and `Outbound` second in
//! generic parameter lists.
//!
//! For [`Bi`], that means `Bi<Inbound, Outbound>`:
//! - `recv() -> Option<Inbound>`
//! - `send(Outbound)`
//!
//! For adapter types that are parameterized by direction-specific converters,
//! inbound-related converter parameters are declared before outbound-related
//! converter parameters.
//! [`expect_message`] is a small helper for request/response style flows: it
//! reads one inbound message from a transport and extracts a typed value from
//! it, failing if the channel closes or the message shape is not what the
//! caller expected.
//!
//! [`RecvConverter`] and [`SendConverter`] are infallible conversion traits used
//! by adapters to map between protocol-facing and transport-facing item types.
//! The traits themselves are not result-aware; adapters decide how transport
//! errors are handled before (or instead of) conversion.
//!
//! [`grpc::GrpcAdapter`] combines:
//! - a tonic inbound stream
//! - a Tokio sender for outbound transport items
//! - a [`RecvConverter`] for the receive path
//! - a [`SendConverter`] for the send path
//!
//! [`DummyTransport`] is a no-op implementation useful for tests and local actor
//! execution where no real network stream exists.
//!
//! # Component Interaction
//!
//! ```text
//! inbound (network -> protocol)
//! ============================
//!
//! tonic::Streaming<RecvTransport>
//! -> grpc::GrpcAdapter::recv()
//! |
//! +--> on `Ok(item)`: RecvConverter::convert(RecvTransport) -> Inbound
//! +--> on `Err(status)`: log error and close stream (`None`)
//! -> Bi::recv()
//! -> protocol/session actor
//!
//! outbound (protocol -> network)
//! ==============================
//!
//! protocol/session actor
//! -> Bi::send(Outbound)
//! -> grpc::GrpcAdapter::send()
//! |
//! +--> SendConverter::convert(Outbound) -> SendTransport
//! -> Tokio mpsc::Sender<SendTransport>
//! -> tonic response stream
//! ```
//! [`DummyTransport`] is a no-op implementation useful for tests and local
//! actor execution where no real stream exists.
//!
//! # Design Notes
//!
//! - `send()` returns [`Error`] only for transport delivery failures (for
//! example, when the outbound channel is closed).
//! - [`grpc::GrpcAdapter`] logs tonic receive errors and treats them as stream
//! closure (`None`).
//! - When protocol-facing and transport-facing types are identical, use
//! [`IdentityRecvConverter`] / [`IdentitySendConverter`].
//! - [`Bi::send`] returns [`Error`] only for transport delivery failures, such
//! as a closed outbound channel.
//! - [`Bi::recv`] returns `None` when the underlying transport closes.
//! - Message translation is intentionally out of scope for this module.
use std::marker::PhantomData;
@@ -83,6 +44,23 @@ use async_trait::async_trait;
pub enum Error {
#[error("Transport channel is closed")]
ChannelClosed,
#[error("Unexpected message received")]
UnexpectedMessage,
}
/// Receives one message from `transport` and extracts a value from it using
/// `extractor`. Returns [`Error::ChannelClosed`] if the transport closes and
/// [`Error::UnexpectedMessage`] if `extractor` returns `None`.
pub async fn expect_message<T, Inbound, Outbound, Target, F>(
transport: &mut T,
extractor: F,
) -> Result<Target, Error>
where
T: Bi<Inbound, Outbound> + ?Sized,
F: FnOnce(Inbound) -> Option<Target>,
{
let msg = transport.recv().await.ok_or(Error::ChannelClosed)?;
extractor(msg).ok_or(Error::UnexpectedMessage)
}
/// Minimal bidirectional transport abstraction used by protocol code.
@@ -97,163 +75,6 @@ pub trait Bi<Inbound, Outbound>: Send + Sync + 'static {
async fn recv(&mut self) -> Option<Inbound>;
}
/// Converts transport-facing inbound items into protocol-facing inbound items.
pub trait RecvConverter: Send + Sync + 'static {
type Input;
type Output;
fn convert(&self, item: Self::Input) -> Self::Output;
}
/// Converts protocol/domain outbound items into transport-facing outbound items.
pub trait SendConverter: Send + Sync + 'static {
type Input;
type Output;
fn convert(&self, item: Self::Input) -> Self::Output;
}
/// A [`RecvConverter`] that forwards values unchanged.
pub struct IdentityRecvConverter<T> {
_marker: PhantomData<T>,
}
impl<T> IdentityRecvConverter<T> {
pub fn new() -> Self {
Self {
_marker: PhantomData,
}
}
}
impl<T> Default for IdentityRecvConverter<T> {
fn default() -> Self {
Self::new()
}
}
impl<T> RecvConverter for IdentityRecvConverter<T>
where
T: Send + Sync + 'static,
{
type Input = T;
type Output = T;
fn convert(&self, item: Self::Input) -> Self::Output {
item
}
}
/// A [`SendConverter`] that forwards values unchanged.
pub struct IdentitySendConverter<T> {
_marker: PhantomData<T>,
}
impl<T> IdentitySendConverter<T> {
pub fn new() -> Self {
Self {
_marker: PhantomData,
}
}
}
impl<T> Default for IdentitySendConverter<T> {
fn default() -> Self {
Self::new()
}
}
impl<T> SendConverter for IdentitySendConverter<T>
where
T: Send + Sync + 'static,
{
type Input = T;
type Output = T;
fn convert(&self, item: Self::Input) -> Self::Output {
item
}
}
/// gRPC-specific transport adapters and helpers.
pub mod grpc {
use async_trait::async_trait;
use futures::StreamExt;
use tokio::sync::mpsc;
use tonic::Streaming;
use super::{Bi, Error, RecvConverter, SendConverter};
/// [`Bi`] adapter backed by a tonic gRPC bidirectional stream.
///
/// Tonic receive errors are logged and treated as stream closure (`None`).
/// The receive converter is only invoked for successful inbound transport
/// items.
pub struct GrpcAdapter<InboundConverter, OutboundConverter>
where
InboundConverter: RecvConverter,
OutboundConverter: SendConverter,
{
sender: mpsc::Sender<OutboundConverter::Output>,
receiver: Streaming<InboundConverter::Input>,
inbound_converter: InboundConverter,
outbound_converter: OutboundConverter,
}
impl<InboundTransport, Inbound, InboundConverter, OutboundConverter>
GrpcAdapter<InboundConverter, OutboundConverter>
where
InboundConverter: RecvConverter<Input = InboundTransport, Output = Inbound>,
OutboundConverter: SendConverter,
{
pub fn new(
sender: mpsc::Sender<OutboundConverter::Output>,
receiver: Streaming<InboundTransport>,
inbound_converter: InboundConverter,
outbound_converter: OutboundConverter,
) -> Self {
Self {
sender,
receiver,
inbound_converter,
outbound_converter,
}
}
}
#[async_trait]
impl<InboundConverter, OutboundConverter> Bi<InboundConverter::Output, OutboundConverter::Input>
for GrpcAdapter<InboundConverter, OutboundConverter>
where
InboundConverter: RecvConverter,
OutboundConverter: SendConverter,
OutboundConverter::Input: Send + 'static,
OutboundConverter::Output: Send + 'static,
{
#[tracing::instrument(level = "trace", skip(self, item))]
async fn send(&mut self, item: OutboundConverter::Input) -> Result<(), Error> {
let outbound = self.outbound_converter.convert(item);
self.sender
.send(outbound)
.await
.map_err(|_| Error::ChannelClosed)
}
#[tracing::instrument(level = "trace", skip(self))]
async fn recv(&mut self) -> Option<InboundConverter::Output> {
match self.receiver.next().await {
Some(Ok(item)) => Some(self.inbound_converter.convert(item)),
Some(Err(error)) => {
tracing::error!(error = ?error, "grpc transport recv failed; closing stream");
None
}
None => None,
}
}
}
}
/// No-op [`Bi`] transport for tests and manual actor usage.
///
/// `send` drops all items and succeeds. [`Bi::recv`] never resolves and therefore

View File

@@ -20,7 +20,7 @@ impl Display for ArbiterUrl {
"{ARBITER_URL_SCHEME}://{}:{}?{CERT_QUERY_KEY}={}",
self.host,
self.port,
BASE64_URL_SAFE.encode(self.ca_cert.to_vec())
BASE64_URL_SAFE.encode(&self.ca_cert)
);
if let Some(token) = &self.bootstrap_token {
base.push_str(&format!("&{BOOTSTRAP_TOKEN_QUERY_KEY}={}", token));

View File

@@ -5,6 +5,9 @@ edition = "2024"
repository = "https://git.markettakers.org/MarketTakers/arbiter"
license = "Apache-2.0"
[lints]
workspace = true
[dependencies]
diesel = { version = "2.3.6", features = ["chrono", "returning_clauses_for_sqlite_3_35", "serde_json", "time", "uuid"] }
diesel-async = { version = "0.7.4", features = [
@@ -42,8 +45,12 @@ argon2 = { version = "0.5.3", features = ["zeroize"] }
restructed = "0.2.2"
strum = { version = "0.27.2", features = ["derive"] }
pem = "3.0.6"
k256 = "0.13.4"
k256.workspace = true
rsa.workspace = true
sha2.workspace = true
spki.workspace = true
alloy.workspace = true
prost-types.workspace = true
arbiter-tokens-registry.path = "../arbiter-tokens-registry"
[dev-dependencies]

View File

@@ -46,6 +46,7 @@ create table if not exists useragent_client (
id integer not null primary key,
nonce integer not null default(1), -- used for auth challenge
public_key blob not null,
key_type integer not null default(1), -- 1=Ed25519, 2=ECDSA(secp256k1)
created_at integer not null default(unixepoch ('now')),
updated_at integer not null default(unixepoch ('now'))
) STRICT;

View File

@@ -3,12 +3,7 @@ use diesel::QueryDsl;
use diesel_async::RunQueryDsl;
use kameo::{Actor, messages};
use miette::Diagnostic;
use rand::{
RngExt,
distr::{Alphanumeric},
make_rng,
rngs::StdRng,
};
use rand::{RngExt, distr::Alphanumeric, make_rng, rngs::StdRng};
use thiserror::Error;
use crate::db::{self, DatabasePool, schema};
@@ -61,7 +56,6 @@ impl Bootstrapper {
drop(conn);
let token = if row_count == 0 {
let token = generate_token().await?;
Some(token)

View File

@@ -0,0 +1,237 @@
use arbiter_proto::{format_challenge, transport::expect_message};
use diesel::{
ExpressionMethods as _, OptionalExtension as _, QueryDsl as _, dsl::insert_into, update,
};
use diesel_async::RunQueryDsl as _;
use ed25519_dalek::VerifyingKey;
use kameo::error::SendError;
use tracing::error;
use crate::{
actors::{
client::{ClientConnection, ConnectErrorCode, Request, Response},
router::{self, RequestClientApproval},
},
db::{self, schema::program_client},
};
use super::session::ClientSession;
#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
pub enum Error {
#[error("Unexpected message payload")]
UnexpectedMessagePayload,
#[error("Invalid client public key length")]
InvalidClientPubkeyLength,
#[error("Invalid client public key encoding")]
InvalidAuthPubkeyEncoding,
#[error("Database pool unavailable")]
DatabasePoolUnavailable,
#[error("Database operation failed")]
DatabaseOperationFailed,
#[error("Invalid challenge solution")]
InvalidChallengeSolution,
#[error("Client approval request failed")]
ApproveError(#[from] ApproveError),
#[error("Internal error")]
InternalError,
#[error("Transport error")]
Transport,
}
#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
pub enum ApproveError {
#[error("Internal error")]
Internal,
#[error("Client connection denied by user agents")]
Denied,
#[error("Upstream error: {0}")]
Upstream(router::ApprovalError),
}
/// Atomically reads and increments the nonce for a known client.
/// Returns `None` if the pubkey is not registered.
async fn get_nonce(db: &db::DatabasePool, pubkey: &VerifyingKey) -> Result<Option<i32>, Error> {
let pubkey_bytes = pubkey.as_bytes().to_vec();
let mut conn = db.get().await.map_err(|e| {
error!(error = ?e, "Database pool error");
Error::DatabasePoolUnavailable
})?;
conn.exclusive_transaction(|conn| {
let pubkey_bytes = pubkey_bytes.clone();
Box::pin(async move {
let Some(current_nonce) = program_client::table
.filter(program_client::public_key.eq(&pubkey_bytes))
.select(program_client::nonce)
.first::<i32>(conn)
.await
.optional()?
else {
return Result::<_, diesel::result::Error>::Ok(None);
};
update(program_client::table)
.filter(program_client::public_key.eq(&pubkey_bytes))
.set(program_client::nonce.eq(current_nonce + 1))
.execute(conn)
.await?;
Ok(Some(current_nonce))
})
})
.await
.map_err(|e| {
error!(error = ?e, "Database error");
Error::DatabaseOperationFailed
})
}
async fn approve_new_client(
actors: &crate::actors::GlobalActors,
pubkey: VerifyingKey,
) -> Result<(), Error> {
let result = actors
.router
.ask(RequestClientApproval {
client_pubkey: pubkey,
})
.await;
match result {
Ok(true) => Ok(()),
Ok(false) => Err(Error::ApproveError(ApproveError::Denied)),
Err(SendError::HandlerError(e)) => {
error!(error = ?e, "Approval upstream error");
Err(Error::ApproveError(ApproveError::Upstream(e)))
}
Err(e) => {
error!(error = ?e, "Approval request to router failed");
Err(Error::ApproveError(ApproveError::Internal))
}
}
}
async fn insert_client(db: &db::DatabasePool, pubkey: &VerifyingKey) -> Result<(), Error> {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as i32;
let mut conn = db.get().await.map_err(|e| {
error!(error = ?e, "Database pool error");
Error::DatabasePoolUnavailable
})?;
insert_into(program_client::table)
.values((
program_client::public_key.eq(pubkey.as_bytes().to_vec()),
program_client::nonce.eq(1), // pre-incremented; challenge uses 0
program_client::created_at.eq(now),
program_client::updated_at.eq(now),
))
.execute(&mut conn)
.await
.map_err(|e| {
error!(error = ?e, "Failed to insert new client");
Error::DatabaseOperationFailed
})?;
Ok(())
}
async fn challenge_client(
props: &mut ClientConnection,
pubkey: VerifyingKey,
nonce: i32,
) -> Result<(), Error> {
let challenge_pubkey = pubkey.as_bytes().to_vec();
props
.transport
.send(Ok(Response::AuthChallenge {
pubkey: challenge_pubkey.clone(),
nonce,
}))
.await
.map_err(|e| {
error!(error = ?e, "Failed to send auth challenge");
Error::Transport
})?;
let signature = expect_message(&mut *props.transport, |req: Request| match req {
Request::AuthChallengeSolution { signature } => Some(signature),
_ => None,
})
.await
.map_err(|e| {
error!(error = ?e, "Failed to receive challenge solution");
Error::Transport
})?;
let formatted = format_challenge(nonce, &challenge_pubkey);
let sig = signature.as_slice().try_into().map_err(|_| {
error!("Invalid signature length");
Error::InvalidChallengeSolution
})?;
pubkey.verify_strict(&formatted, &sig).map_err(|_| {
error!("Challenge solution verification failed");
Error::InvalidChallengeSolution
})?;
Ok(())
}
fn connect_error_code(err: &Error) -> ConnectErrorCode {
match err {
Error::ApproveError(ApproveError::Denied) => ConnectErrorCode::ApprovalDenied,
Error::ApproveError(ApproveError::Upstream(
router::ApprovalError::NoUserAgentsConnected,
)) => ConnectErrorCode::NoUserAgentsOnline,
_ => ConnectErrorCode::Unknown,
}
}
async fn authenticate(props: &mut ClientConnection) -> Result<VerifyingKey, Error> {
let Some(Request::AuthChallengeRequest {
pubkey: challenge_pubkey,
}) = props.transport.recv().await
else {
return Err(Error::Transport);
};
let pubkey_bytes = challenge_pubkey
.as_array()
.ok_or(Error::InvalidClientPubkeyLength)?;
let pubkey =
VerifyingKey::from_bytes(pubkey_bytes).map_err(|_| Error::InvalidAuthPubkeyEncoding)?;
let nonce = match get_nonce(&props.db, &pubkey).await? {
Some(nonce) => nonce,
None => {
approve_new_client(&props.actors, pubkey).await?;
insert_client(&props.db, &pubkey).await?;
0
}
};
challenge_client(props, pubkey, nonce).await?;
Ok(pubkey)
}
pub async fn authenticate_and_create(mut props: ClientConnection) -> Result<ClientSession, Error> {
match authenticate(&mut props).await {
Ok(_pubkey) => Ok(ClientSession::new(props)),
Err(err) => {
let code = connect_error_code(&err);
let _ = props
.transport
.send(Ok(Response::ClientConnectError { code }))
.await;
Err(err)
}
}
}

View File

@@ -1,102 +0,0 @@
use arbiter_proto::proto::client::{
AuthChallengeRequest, AuthChallengeSolution, ClientRequest,
client_request::Payload as ClientRequestPayload,
};
use ed25519_dalek::VerifyingKey;
use tracing::error;
use crate::actors::client::{
ClientConnection,
auth::state::{AuthContext, AuthStateMachine},
session::ClientSession,
};
#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
pub enum Error {
#[error("Unexpected message payload")]
UnexpectedMessagePayload,
#[error("Invalid client public key length")]
InvalidClientPubkeyLength,
#[error("Invalid client public key encoding")]
InvalidAuthPubkeyEncoding,
#[error("Database pool unavailable")]
DatabasePoolUnavailable,
#[error("Database operation failed")]
DatabaseOperationFailed,
#[error("Public key not registered")]
PublicKeyNotRegistered,
#[error("Invalid signature length")]
InvalidSignatureLength,
#[error("Invalid challenge solution")]
InvalidChallengeSolution,
#[error("Transport error")]
Transport,
}
mod state;
use state::*;
fn parse_auth_event(payload: ClientRequestPayload) -> Result<AuthEvents, Error> {
match payload {
ClientRequestPayload::AuthChallengeRequest(AuthChallengeRequest { pubkey }) => {
let pubkey_bytes = pubkey.as_array().ok_or(Error::InvalidClientPubkeyLength)?;
let pubkey = VerifyingKey::from_bytes(pubkey_bytes)
.map_err(|_| Error::InvalidAuthPubkeyEncoding)?;
Ok(AuthEvents::AuthRequest(ChallengeRequest {
pubkey: pubkey.into(),
}))
}
ClientRequestPayload::AuthChallengeSolution(AuthChallengeSolution { signature }) => {
Ok(AuthEvents::ReceivedSolution(ChallengeSolution {
solution: signature,
}))
}
_ => Err(Error::UnexpectedMessagePayload) ,
}
}
pub async fn authenticate(props: &mut ClientConnection) -> Result<VerifyingKey, Error> {
let mut state = AuthStateMachine::new(AuthContext::new(props));
loop {
let transport = state.context_mut().conn.transport.as_mut();
let Some(ClientRequest {
payload: Some(payload),
}) = transport.recv().await
else {
return Err(Error::Transport);
};
let event = parse_auth_event(payload)?;
match state.process_event(event).await {
Ok(AuthStates::AuthOk(key)) => return Ok(key.clone()),
Err(AuthError::ActionFailed(err)) => {
error!(?err, "State machine action failed");
return Err(err);
}
Err(AuthError::GuardFailed(err)) => {
error!(?err, "State machine guard failed");
return Err(err);
}
Err(AuthError::InvalidEvent) => {
error!("Invalid event for current state");
return Err(Error::InvalidChallengeSolution);
}
Err(AuthError::TransitionsFailed) => {
error!("Invalid state transition");
return Err(Error::InvalidChallengeSolution);
}
_ => (),
}
}
}
pub async fn authenticate_and_create(
mut props: ClientConnection,
) -> Result<ClientSession, Error> {
let key = authenticate(&mut props).await?;
let session = ClientSession::new(props, key);
Ok(session)
}

View File

@@ -1,136 +0,0 @@
use arbiter_proto::proto::client::{
AuthChallenge, ClientResponse,
client_response::Payload as ClientResponsePayload,
};
use diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl, update};
use diesel_async::RunQueryDsl;
use ed25519_dalek::VerifyingKey;
use tracing::error;
use super::Error;
use crate::{actors::client::ClientConnection, db::schema};
pub struct ChallengeRequest {
pub pubkey: VerifyingKey,
}
pub struct ChallengeContext {
pub challenge: AuthChallenge,
pub key: VerifyingKey,
}
pub struct ChallengeSolution {
pub solution: Vec<u8>,
}
smlang::statemachine!(
name: Auth,
custom_error: true,
transitions: {
*Init + AuthRequest(ChallengeRequest) / async prepare_challenge = SentChallenge(ChallengeContext),
SentChallenge(ChallengeContext) + ReceivedSolution(ChallengeSolution) [async verify_solution] / provide_key = AuthOk(VerifyingKey),
}
);
async fn create_nonce(db: &crate::db::DatabasePool, pubkey_bytes: &[u8]) -> Result<i32, Error> {
let mut db_conn = db.get().await.map_err(|e| {
error!(error = ?e, "Database pool error");
Error::DatabasePoolUnavailable
})?;
db_conn
.exclusive_transaction(|conn| {
Box::pin(async move {
let current_nonce = schema::program_client::table
.filter(schema::program_client::public_key.eq(pubkey_bytes.to_vec()))
.select(schema::program_client::nonce)
.first::<i32>(conn)
.await?;
update(schema::program_client::table)
.filter(schema::program_client::public_key.eq(pubkey_bytes.to_vec()))
.set(schema::program_client::nonce.eq(current_nonce + 1))
.execute(conn)
.await?;
Result::<_, diesel::result::Error>::Ok(current_nonce)
})
})
.await
.optional()
.map_err(|e| {
error!(error = ?e, "Database error");
Error::DatabaseOperationFailed
})?
.ok_or_else(|| {
error!(?pubkey_bytes, "Public key not found in database");
Error::PublicKeyNotRegistered
})
}
pub struct AuthContext<'a> {
pub(super) conn: &'a mut ClientConnection,
}
impl<'a> AuthContext<'a> {
pub fn new(conn: &'a mut ClientConnection) -> Self {
Self { conn }
}
}
impl AuthStateMachineContext for AuthContext<'_> {
type Error = Error;
async fn verify_solution(
&self,
ChallengeContext { challenge, key }: &ChallengeContext,
ChallengeSolution { solution }: &ChallengeSolution,
) -> Result<bool, Self::Error> {
let formatted_challenge =
arbiter_proto::format_challenge(challenge.nonce, &challenge.pubkey);
let signature = solution.as_slice().try_into().map_err(|_| {
error!(?solution, "Invalid signature length");
Error::InvalidChallengeSolution
})?;
let valid = key.verify_strict(&formatted_challenge, &signature).is_ok();
Ok(valid)
}
async fn prepare_challenge(
&mut self,
ChallengeRequest { pubkey }: ChallengeRequest,
) -> Result<ChallengeContext, Self::Error> {
let nonce = create_nonce(&self.conn.db, pubkey.as_bytes()).await?;
let challenge = AuthChallenge {
pubkey: pubkey.as_bytes().to_vec(),
nonce,
};
self.conn
.transport
.send(Ok(ClientResponse {
payload: Some(ClientResponsePayload::AuthChallenge(challenge.clone())),
}))
.await
.map_err(|e| {
error!(?e, "Failed to send auth challenge");
Error::Transport
})?;
Ok(ChallengeContext {
challenge,
key: pubkey,
})
}
fn provide_key(
&mut self,
state_data: &ChallengeContext,
_: ChallengeSolution,
) -> Result<VerifyingKey, Self::Error> {
Ok(state_data.key)
}
}

View File

@@ -1,7 +1,4 @@
use arbiter_proto::{
proto::client::{ClientRequest, ClientResponse},
transport::Bi,
};
use arbiter_proto::transport::Bi;
use kameo::actor::Spawn;
use tracing::{error, info};
@@ -24,7 +21,27 @@ pub enum ClientError {
Auth(#[from] auth::Error),
}
pub type Transport = Box<dyn Bi<ClientRequest, Result<ClientResponse, ClientError>> + Send>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConnectErrorCode {
Unknown,
ApprovalDenied,
NoUserAgentsOnline,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Request {
AuthChallengeRequest { pubkey: Vec<u8> },
AuthChallengeSolution { signature: Vec<u8> },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Response {
AuthChallenge { pubkey: Vec<u8>, nonce: i32 },
AuthOk,
ClientConnectError { code: ConnectErrorCode },
}
pub type Transport = Box<dyn Bi<Request, Result<Response, ClientError>> + Send>;
pub struct ClientConnection {
pub(crate) db: db::DatabasePool,

View File

@@ -1,36 +1,32 @@
use arbiter_proto::proto::client::{ClientRequest, ClientResponse};
use ed25519_dalek::VerifyingKey;
use kameo::Actor;
use tokio::select;
use tracing::{error, info};
use crate::{actors::{
GlobalActors, client::{ClientError, ClientConnection}, router::RegisterClient
}, db};
use crate::{
actors::{
GlobalActors,
client::{ClientConnection, ClientError, Request, Response},
router::RegisterClient,
},
db,
};
pub struct ClientSession {
props: ClientConnection,
key: VerifyingKey,
}
impl ClientSession {
pub(crate) fn new(props: ClientConnection, key: VerifyingKey) -> Self {
Self { props, key }
pub(crate) fn new(props: ClientConnection) -> Self {
Self { props }
}
pub async fn process_transport_inbound(&mut self, req: ClientRequest) -> Output {
let msg = req.payload.ok_or_else(|| {
error!(actor = "client", "Received message with no payload");
ClientError::MissingRequestPayload
})?;
match msg {
_ => Err(ClientError::UnexpectedRequestPayload),
}
pub async fn process_transport_inbound(&mut self, req: Request) -> Output {
let _ = req;
Err(ClientError::UnexpectedRequestPayload)
}
}
type Output = Result<ClientResponse, ClientError>;
type Output = Result<Response, ClientError>;
impl Actor for ClientSession {
type Args = Self;
@@ -92,7 +88,6 @@ impl ClientSession {
use arbiter_proto::transport::DummyTransport;
let transport: super::Transport = Box::new(DummyTransport::new());
let props = ClientConnection::new(db, transport, actors);
let key = VerifyingKey::from_bytes(&[0u8; 32]).unwrap();
Self { props, key }
Self { props }
}
}

View File

@@ -1,5 +1,7 @@
use alloy::{consensus::TxEip1559, network::TxSigner, primitives::Address, signers::Signature};
use diesel::{ExpressionMethods, OptionalExtension as _, QueryDsl, SelectableHelper as _, dsl::insert_into};
use alloy::{consensus::TxEip1559, primitives::Address, signers::Signature};
use diesel::{
ExpressionMethods, OptionalExtension as _, QueryDsl, SelectableHelper as _, dsl::insert_into,
};
use diesel_async::RunQueryDsl;
use kameo::{Actor, actor::ActorRef, messages};
use memsafe::MemSafe;
@@ -7,13 +9,16 @@ use rand::{SeedableRng, rng, rngs::StdRng};
use crate::{
actors::keyholder::{CreateNew, Decrypt, KeyHolder},
db::{self, DatabasePool, models::{self, EvmBasicGrant, SqliteTimestamp}, schema},
db::{
self, DatabasePool,
models::{self, EvmBasicGrant, SqliteTimestamp},
schema,
},
evm::{
self, RunKind,
self, ListGrantsError, RunKind,
policies::{
FullGrant, SharedGrantSettings, SpecificGrant, SpecificMeaning,
ether_transfer::EtherTransfer,
token_transfers::TokenTransfer,
FullGrant, Grant, SharedGrantSettings, SpecificGrant, SpecificMeaning,
ether_transfer::EtherTransfer, token_transfers::TokenTransfer,
},
},
};
@@ -88,7 +93,12 @@ impl EvmActor {
// todo: audit
let rng = StdRng::from_rng(&mut rng());
let engine = evm::Engine::new(db.clone());
Self { keyholder, db, rng, engine }
Self {
keyholder,
db,
rng,
engine,
}
}
}
@@ -149,12 +159,24 @@ impl EvmActor {
match grant {
SpecificGrant::EtherTransfer(settings) => {
self.engine
.create_grant::<EtherTransfer>(client_id, FullGrant { basic, specific: settings })
.create_grant::<EtherTransfer>(
client_id,
FullGrant {
basic,
specific: settings,
},
)
.await
}
SpecificGrant::TokenTransfer(settings) => {
self.engine
.create_grant::<TokenTransfer>(client_id, FullGrant { basic, specific: settings })
.create_grant::<TokenTransfer>(
client_id,
FullGrant {
basic,
specific: settings,
},
)
.await
}
}
@@ -172,19 +194,12 @@ impl EvmActor {
}
#[message]
pub async fn useragent_list_grants(
&mut self,
wallet_id: Option<i32>,
) -> Result<Vec<EvmBasicGrant>, Error> {
let mut conn = self.db.get().await?;
let mut query = schema::evm_basic_grant::table
.select(EvmBasicGrant::as_select())
.filter(schema::evm_basic_grant::revoked_at.is_null())
.into_boxed();
if let Some(wid) = wallet_id {
query = query.filter(schema::evm_basic_grant::wallet_id.eq(wid));
pub async fn useragent_list_grants(&mut self) -> Result<Vec<Grant<SpecificGrant>>, Error> {
match self.engine.list_all_grants().await {
Ok(grants) => Ok(grants),
Err(ListGrantsError::Database(db)) => Err(Error::Database(db)),
Err(ListGrantsError::Pool(pool)) => Err(Error::DatabasePool(pool)),
}
Ok(query.load(&mut conn).await?)
}
#[message]
@@ -204,8 +219,14 @@ impl EvmActor {
.ok_or(SignTransactionError::WalletNotFound)?;
drop(conn);
let meaning = self.engine
.evaluate_transaction(wallet.id, client_id, transaction.clone(), RunKind::Execution)
let meaning = self
.engine
.evaluate_transaction(
wallet.id,
client_id,
transaction.clone(),
RunKind::Execution,
)
.await?;
Ok(meaning)
@@ -230,14 +251,21 @@ impl EvmActor {
let raw_key: MemSafe<Vec<u8>> = self
.keyholder
.ask(Decrypt { aead_id: wallet.aead_encrypted_id })
.ask(Decrypt {
aead_id: wallet.aead_encrypted_id,
})
.await
.map_err(|_| SignTransactionError::KeyholderSend)?;
let signer = safe_signer::SafeSigner::from_memsafe(raw_key)?;
self.engine
.evaluate_transaction(wallet.id, client_id, transaction.clone(), RunKind::Execution)
.evaluate_transaction(
wallet.id,
client_id,
transaction.clone(),
RunKind::Execution,
)
.await?;
use alloy::network::TxSignerSync as _;

View File

@@ -313,7 +313,7 @@ impl KeyHolder {
current_nonce: nonce.to_vec(),
schema_version: 1,
associated_root_key_id: *root_key_history_id,
created_at: Utc::now().into()
created_at: Utc::now().into(),
})
.returning(schema::aead_encrypted::id)
.get_result(&mut conn)

View File

@@ -1,17 +1,20 @@
use std::{
collections::{HashMap},
ops::ControlFlow,
};
use std::{collections::HashMap, ops::ControlFlow};
use ed25519_dalek::VerifyingKey;
use kameo::{
Actor,
actor::{ActorId, ActorRef},
messages,
prelude::{ActorStopReason, Context, WeakActorRef},
reply::DelegatedReply,
};
use tracing::info;
use tokio::{sync::watch, task::JoinSet};
use tracing::{info, warn};
use crate::actors::{client::session::ClientSession, user_agent::session::UserAgentSession};
use crate::actors::{
client::session::ClientSession,
user_agent::session::{RequestNewClientApproval, UserAgentSession},
};
#[derive(Default)]
pub struct MessageRouter {
@@ -53,6 +56,73 @@ impl Actor for MessageRouter {
}
}
#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq, Hash)]
pub enum ApprovalError {
#[error("No user agents connected")]
NoUserAgentsConnected,
}
async fn request_client_approval(
user_agents: &[WeakActorRef<UserAgentSession>],
client_pubkey: VerifyingKey,
) -> Result<bool, ApprovalError> {
if user_agents.is_empty() {
return Err(ApprovalError::NoUserAgentsConnected);
}
let mut pool = JoinSet::new();
let (cancel_tx, cancel_rx) = watch::channel(());
for weak_ref in user_agents {
match weak_ref.upgrade() {
Some(agent) => {
let cancel_rx = cancel_rx.clone();
pool.spawn(async move {
agent
.ask(RequestNewClientApproval {
client_pubkey,
cancel_flag: cancel_rx.clone(),
})
.await
});
}
None => {
warn!(
id = weak_ref.id().to_string(),
actor = "MessageRouter",
event = "useragent.disconnected_before_approval"
);
}
}
}
while let Some(result) = pool.join_next().await {
match result {
Ok(Ok(approved)) => {
// cancel other pending requests
let _ = cancel_tx.send(());
return Ok(approved);
}
Ok(Err(err)) => {
warn!(
?err,
actor = "MessageRouter",
event = "useragent.approval_error"
);
}
Err(err) => {
warn!(
?err,
actor = "MessageRouter",
event = "useragent.approval_task_failed"
);
}
}
}
Err(ApprovalError::NoUserAgentsConnected)
}
#[messages]
impl MessageRouter {
#[message(ctx)]
@@ -76,4 +146,29 @@ impl MessageRouter {
ctx.actor_ref().link(&actor).await;
self.clients.insert(actor.id(), actor);
}
#[message(ctx)]
pub async fn request_client_approval(
&mut self,
client_pubkey: VerifyingKey,
ctx: &mut Context<Self, DelegatedReply<Result<bool, ApprovalError>>>,
) -> DelegatedReply<Result<bool, ApprovalError>> {
let (reply, Some(reply_sender)) = ctx.reply_sender() else {
panic!("Exptected `request_client_approval` to have callback channel");
};
let weak_refs = self
.user_agents
.values()
.map(|agent| agent.downgrade())
.collect::<Vec<_>>();
// handle in subtask to not to lock the actor
tokio::task::spawn(async move {
let result = request_client_approval(&weak_refs, client_pubkey).await;
reply_sender.send(result);
});
reply
}
}

View File

@@ -1,13 +1,10 @@
use arbiter_proto::proto::user_agent::{
AuthChallengeRequest, AuthChallengeSolution, UserAgentRequest,
user_agent_request::Payload as UserAgentRequestPayload,
};
use ed25519_dalek::VerifyingKey;
use tracing::error;
use crate::actors::user_agent::{
UserAgentConnection,
auth::state::{AuthContext, AuthStateMachine}, session::UserAgentSession,
Request, UserAgentConnection,
auth::state::{AuthContext, AuthStateMachine},
AuthPublicKey,
session::UserAgentSession,
};
#[derive(thiserror::Error, Debug, PartialEq)]
@@ -37,32 +34,20 @@ pub enum Error {
mod state;
use state::*;
fn parse_auth_event(payload: UserAgentRequestPayload) -> Result<AuthEvents, Error> {
fn parse_auth_event(payload: Request) -> Result<AuthEvents, Error> {
match payload {
UserAgentRequestPayload::AuthChallengeRequest(AuthChallengeRequest {
Request::AuthChallengeRequest {
pubkey,
bootstrap_token: None,
}) => {
let pubkey_bytes = pubkey.as_array().ok_or(Error::InvalidClientPubkeyLength)?;
let pubkey = VerifyingKey::from_bytes(pubkey_bytes)
.map_err(|_| Error::InvalidAuthPubkeyEncoding)?;
Ok(AuthEvents::AuthRequest(ChallengeRequest {
pubkey: pubkey.into(),
}))
}
UserAgentRequestPayload::AuthChallengeRequest(AuthChallengeRequest {
} => Ok(AuthEvents::AuthRequest(ChallengeRequest { pubkey })),
Request::AuthChallengeRequest {
pubkey,
bootstrap_token: Some(token),
}) => {
let pubkey_bytes = pubkey.as_array().ok_or(Error::InvalidClientPubkeyLength)?;
let pubkey = VerifyingKey::from_bytes(pubkey_bytes)
.map_err(|_| Error::InvalidAuthPubkeyEncoding)?;
Ok(AuthEvents::BootstrapAuthRequest(BootstrapAuthRequest {
pubkey: pubkey.into(),
token,
}))
}
UserAgentRequestPayload::AuthChallengeSolution(AuthChallengeSolution { signature }) => {
} => Ok(AuthEvents::BootstrapAuthRequest(BootstrapAuthRequest {
pubkey,
token,
})),
Request::AuthChallengeSolution { signature } => {
Ok(AuthEvents::ReceivedSolution(ChallengeSolution {
solution: signature,
}))
@@ -71,16 +56,13 @@ fn parse_auth_event(payload: UserAgentRequestPayload) -> Result<AuthEvents, Erro
}
}
pub async fn authenticate(props: &mut UserAgentConnection) -> Result<VerifyingKey, Error> {
pub async fn authenticate(props: &mut UserAgentConnection) -> Result<AuthPublicKey, Error> {
let mut state = AuthStateMachine::new(AuthContext::new(props));
loop {
// This is needed because `state` now holds mutable reference to `ConnectionProps`, so we can't directly access `props` here
// `state` holds a mutable reference to `props` so we can't access it directly here
let transport = state.context_mut().conn.transport.as_mut();
let Some(UserAgentRequest {
payload: Some(payload),
}) = transport.recv().await
else {
let Some(payload) = transport.recv().await else {
return Err(Error::Transport);
};
@@ -110,9 +92,10 @@ pub async fn authenticate(props: &mut UserAgentConnection) -> Result<VerifyingKe
}
}
pub async fn authenticate_and_create(mut props: UserAgentConnection) -> Result<UserAgentSession, Error> {
let key = authenticate(&mut props).await?;
let session = UserAgentSession::new(props, key.clone());
pub async fn authenticate_and_create(
mut props: UserAgentConnection,
) -> Result<UserAgentSession, Error> {
let _key = authenticate(&mut props).await?;
let session = UserAgentSession::new(props);
Ok(session)
}

View File

@@ -1,30 +1,28 @@
use arbiter_proto::proto::user_agent::{
AuthChallenge, UserAgentResponse,
user_agent_response::Payload as UserAgentResponsePayload,
};
use diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl, update};
use diesel_async::RunQueryDsl;
use ed25519_dalek::VerifyingKey;
use tracing::error;
use super::Error;
use crate::{
actors::{bootstrap::ConsumeToken, user_agent::UserAgentConnection},
actors::{
bootstrap::ConsumeToken,
user_agent::{AuthPublicKey, Response, UserAgentConnection},
},
db::schema,
};
pub struct ChallengeRequest {
pub pubkey: VerifyingKey,
pub pubkey: AuthPublicKey,
}
pub struct BootstrapAuthRequest {
pub pubkey: VerifyingKey,
pub pubkey: AuthPublicKey,
pub token: String,
}
pub struct ChallengeContext {
pub challenge: AuthChallenge,
pub key: VerifyingKey,
pub challenge_nonce: i32,
pub key: AuthPublicKey,
}
pub struct ChallengeSolution {
@@ -36,8 +34,8 @@ smlang::statemachine!(
custom_error: true,
transitions: {
*Init + AuthRequest(ChallengeRequest) / async prepare_challenge = SentChallenge(ChallengeContext),
Init + BootstrapAuthRequest(BootstrapAuthRequest) [async verify_bootstrap_token] / provide_key_bootstrap = AuthOk(VerifyingKey),
SentChallenge(ChallengeContext) + ReceivedSolution(ChallengeSolution) [async verify_solution] / provide_key = AuthOk(VerifyingKey),
Init + BootstrapAuthRequest(BootstrapAuthRequest) / async verify_bootstrap_token = AuthOk(AuthPublicKey),
SentChallenge(ChallengeContext) + ReceivedSolution(ChallengeSolution) / async verify_solution = AuthOk(AuthPublicKey),
}
);
@@ -76,7 +74,9 @@ async fn create_nonce(db: &crate::db::DatabasePool, pubkey_bytes: &[u8]) -> Resu
})
}
async fn register_key(db: &crate::db::DatabasePool, pubkey_bytes: &[u8]) -> Result<(), Error> {
async fn register_key(db: &crate::db::DatabasePool, pubkey: &AuthPublicKey) -> Result<(), Error> {
let pubkey_bytes = pubkey.to_stored_bytes();
let key_type = pubkey.key_type();
let mut conn = db.get().await.map_err(|e| {
error!(error = ?e, "Database pool error");
Error::DatabasePoolUnavailable
@@ -84,8 +84,9 @@ async fn register_key(db: &crate::db::DatabasePool, pubkey_bytes: &[u8]) -> Resu
diesel::insert_into(schema::useragent_client::table)
.values((
schema::useragent_client::public_key.eq(pubkey_bytes.to_vec()),
schema::useragent_client::public_key.eq(pubkey_bytes),
schema::useragent_client::nonce.eq(1),
schema::useragent_client::key_type.eq(key_type),
))
.execute(&mut conn)
.await
@@ -110,40 +111,16 @@ impl<'a> AuthContext<'a> {
impl AuthStateMachineContext for AuthContext<'_> {
type Error = Error;
async fn verify_solution(
&self,
ChallengeContext { challenge, key }: &ChallengeContext,
ChallengeSolution { solution }: &ChallengeSolution,
) -> Result<bool, Self::Error> {
let formatted_challenge =
arbiter_proto::format_challenge(challenge.nonce, &challenge.pubkey);
let signature = solution.as_slice().try_into().map_err(|_| {
error!(?solution, "Invalid signature length");
Error::InvalidChallengeSolution
})?;
let valid = key.verify_strict(&formatted_challenge, &signature).is_ok();
Ok(valid)
}
async fn prepare_challenge(
&mut self,
ChallengeRequest { pubkey }: ChallengeRequest,
) -> Result<ChallengeContext, Self::Error> {
let nonce = create_nonce(&self.conn.db, pubkey.as_bytes()).await?;
let challenge = AuthChallenge {
pubkey: pubkey.as_bytes().to_vec(),
nonce,
};
let stored_bytes = pubkey.to_stored_bytes();
let nonce = create_nonce(&self.conn.db, &stored_bytes).await?;
self.conn
.transport
.send(Ok(UserAgentResponse {
payload: Some(UserAgentResponsePayload::AuthChallenge(challenge.clone())),
}))
.send(Ok(Response::AuthChallenge { nonce }))
.await
.map_err(|e| {
error!(?e, "Failed to send auth challenge");
@@ -151,7 +128,7 @@ impl AuthStateMachineContext for AuthContext<'_> {
})?;
Ok(ChallengeContext {
challenge,
challenge_nonce: nonce,
key: pubkey,
})
}
@@ -159,9 +136,9 @@ impl AuthStateMachineContext for AuthContext<'_> {
#[allow(missing_docs)]
#[allow(clippy::result_unit_err)]
async fn verify_bootstrap_token(
&self,
BootstrapAuthRequest { pubkey, token }: &BootstrapAuthRequest,
) -> Result<bool, Self::Error> {
&mut self,
BootstrapAuthRequest { pubkey, token }: BootstrapAuthRequest,
) -> Result<AuthPublicKey, Self::Error> {
let token_ok: bool = self
.conn
.actors
@@ -171,32 +148,70 @@ impl AuthStateMachineContext for AuthContext<'_> {
})
.await
.map_err(|e| {
error!(?pubkey, "Failed to consume bootstrap token: {e}");
error!(?e, "Failed to consume bootstrap token");
Error::BootstrapperActorUnreachable
})?;
if !token_ok {
error!(?pubkey, "Invalid bootstrap token provided");
error!("Invalid bootstrap token provided");
return Err(Error::InvalidBootstrapToken);
}
register_key(&self.conn.db, pubkey.as_bytes()).await?;
register_key(&self.conn.db, &pubkey).await?;
Ok(true)
self.conn
.transport
.send(Ok(Response::AuthOk))
.await
.map_err(|_| Error::Transport)?;
Ok(pubkey)
}
fn provide_key_bootstrap(
#[allow(missing_docs)]
#[allow(clippy::unused_unit)]
async fn verify_solution(
&mut self,
event_data: BootstrapAuthRequest,
) -> Result<VerifyingKey, Self::Error> {
Ok(event_data.pubkey)
}
ChallengeContext { challenge_nonce, key }: &ChallengeContext,
ChallengeSolution { solution }: ChallengeSolution,
) -> Result<AuthPublicKey, Self::Error> {
let formatted = arbiter_proto::format_challenge(*challenge_nonce, &key.to_stored_bytes());
fn provide_key(
&mut self,
state_data: &ChallengeContext,
_: ChallengeSolution,
) -> Result<VerifyingKey, Self::Error> {
Ok(state_data.key)
let valid = match key {
AuthPublicKey::Ed25519(vk) => {
let sig = solution.as_slice().try_into().map_err(|_| {
error!(?solution, "Invalid Ed25519 signature length");
Error::InvalidChallengeSolution
})?;
vk.verify_strict(&formatted, &sig).is_ok()
}
AuthPublicKey::EcdsaSecp256k1(vk) => {
use k256::ecdsa::signature::Verifier as _;
let sig = k256::ecdsa::Signature::try_from(solution.as_slice()).map_err(|_| {
error!(?solution, "Invalid ECDSA signature bytes");
Error::InvalidChallengeSolution
})?;
vk.verify(&formatted, &sig).is_ok()
}
AuthPublicKey::Rsa(pk) => {
use rsa::signature::Verifier as _;
let verifying_key = rsa::pss::VerifyingKey::<sha2::Sha256>::new(pk.clone());
let sig = rsa::pss::Signature::try_from(solution.as_slice()).map_err(|_| {
error!(?solution, "Invalid RSA signature bytes");
Error::InvalidChallengeSolution
})?;
verifying_key.verify(&formatted, &sig).is_ok()
}
};
if valid {
self.conn
.transport
.send(Ok(Response::AuthOk))
.await
.map_err(|_| Error::Transport)?;
}
Ok(key.clone())
}
}

View File

@@ -1,19 +1,16 @@
use arbiter_proto::{
proto::user_agent::{UserAgentRequest, UserAgentResponse},
transport::Bi,
};
use alloy::primitives::Address;
use arbiter_proto::{transport::Bi};
use kameo::actor::Spawn as _;
use tracing::{error, info};
use crate::{
actors::{GlobalActors, user_agent::session::UserAgentSession},
db::{self},
actors::{GlobalActors, evm, user_agent::session::UserAgentSession},
db::{self, models::KeyType}, evm::policies::{Grant, SpecificGrant},
evm::policies::SharedGrantSettings,
};
#[derive(Debug, thiserror::Error, PartialEq)]
pub enum UserAgentError {
#[error("Expected message with payload")]
MissingRequestPayload,
pub enum TransportResponseError {
#[error("Unexpected request payload")]
UnexpectedRequestPayload,
#[error("Invalid state for unseal encrypted key")]
@@ -30,8 +27,120 @@ pub enum UserAgentError {
ConnectionRegistrationFailed,
}
pub type Transport =
Box<dyn Bi<UserAgentRequest, Result<UserAgentResponse, UserAgentError>> + Send>;
/// Abstraction over Ed25519 / ECDSA-secp256k1 / RSA public keys used during the auth handshake.
#[derive(Clone, Debug)]
pub enum AuthPublicKey {
Ed25519(ed25519_dalek::VerifyingKey),
/// Compressed SEC1 public key; signature bytes are raw 64-byte (r||s).
EcdsaSecp256k1(k256::ecdsa::VerifyingKey),
/// RSA-2048+ public key (Windows Hello / KeyCredentialManager); signature bytes are PSS+SHA-256.
Rsa(rsa::RsaPublicKey),
}
impl AuthPublicKey {
/// Canonical bytes stored in DB and echoed back in the challenge.
/// Ed25519: raw 32 bytes. ECDSA: SEC1 compressed 33 bytes. RSA: DER-encoded SPKI.
pub fn to_stored_bytes(&self) -> Vec<u8> {
match self {
AuthPublicKey::Ed25519(k) => k.to_bytes().to_vec(),
// SEC1 compressed (33 bytes) is the natural compact format for secp256k1
AuthPublicKey::EcdsaSecp256k1(k) => k.to_encoded_point(true).as_bytes().to_vec(),
AuthPublicKey::Rsa(k) => {
use rsa::pkcs8::EncodePublicKey as _;
k.to_public_key_der()
.expect("rsa SPKI encoding is infallible")
.to_vec()
}
}
}
pub fn key_type(&self) -> KeyType {
match self {
AuthPublicKey::Ed25519(_) => KeyType::Ed25519,
AuthPublicKey::EcdsaSecp256k1(_) => KeyType::EcdsaSecp256k1,
AuthPublicKey::Rsa(_) => KeyType::Rsa,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UnsealError {
InvalidKey,
Unbootstrapped,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BootstrapError {
AlreadyBootstrapped,
InvalidKey,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VaultState {
Unbootstrapped,
Sealed,
Unsealed,
}
#[derive(Debug, Clone)]
pub enum Request {
AuthChallengeRequest {
pubkey: AuthPublicKey,
bootstrap_token: Option<String>,
},
AuthChallengeSolution {
signature: Vec<u8>,
},
UnsealStart {
client_pubkey: x25519_dalek::PublicKey,
},
UnsealEncryptedKey {
nonce: Vec<u8>,
ciphertext: Vec<u8>,
associated_data: Vec<u8>,
},
BootstrapEncryptedKey {
nonce: Vec<u8>,
ciphertext: Vec<u8>,
associated_data: Vec<u8>,
},
QueryVaultState,
EvmWalletCreate,
EvmWalletList,
ClientConnectionResponse {
approved: bool,
},
ListGrants,
EvmGrantCreate {
client_id: i32,
shared: SharedGrantSettings,
specific: SpecificGrant,
},
EvmGrantDelete {
grant_id: i32,
},
}
#[derive(Debug)]
pub enum Response {
AuthChallenge { nonce: i32 },
AuthOk,
UnsealStartResponse { server_pubkey: x25519_dalek::PublicKey },
UnsealResult(Result<(), UnsealError>),
BootstrapResult(Result<(), BootstrapError>),
VaultState(VaultState),
ClientConnectionRequest { pubkey: ed25519_dalek::VerifyingKey },
ClientConnectionCancel,
EvmWalletCreate(Result<(), evm::Error>),
EvmWalletList(Vec<Address>),
ListGrants(Vec<Grant<SpecificGrant>>),
EvmGrantCreate(Result<i32, evm::Error>),
EvmGrantDelete(Result<(), evm::Error>),
}
pub type Transport = Box<dyn Bi<Request, Result<Response, TransportResponseError>> + Send>;
pub struct UserAgentConnection {
db: db::DatabasePool,
@@ -52,6 +161,7 @@ impl UserAgentConnection {
pub mod auth;
pub mod session;
#[tracing::instrument(skip(props))]
pub async fn connect_user_agent(props: UserAgentConnection) {
match auth::authenticate_and_create(props).await {
Ok(session) => {

View File

@@ -1,254 +1,148 @@
use std::{ops::DerefMut, sync::Mutex};
use arbiter_proto::proto::{
evm as evm_proto,
user_agent::{
UnsealEncryptedKey, UnsealResult, UnsealStart, UnsealStartResponse, UserAgentRequest,
UserAgentResponse, user_agent_request::Payload as UserAgentRequestPayload,
user_agent_response::Payload as UserAgentResponsePayload,
},
};
use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit};
use chacha20poly1305::aead::KeyInit;
use ed25519_dalek::VerifyingKey;
use kameo::{
Actor,
error::SendError,
};
use memsafe::MemSafe;
use tokio::select;
use kameo::{Actor, messages, prelude::Context};
use tokio::{select, sync::watch};
use tracing::{error, info};
use x25519_dalek::{EphemeralSecret, PublicKey};
use crate::actors::{
evm::{Generate, ListWallets},
keyholder::{self, TryUnseal},
router::RegisterUserAgent,
user_agent::{UserAgentConnection, UserAgentError},
user_agent::{
Request, Response, TransportResponseError,
UserAgentConnection,
},
};
mod state;
use state::{DummyContext, UnsealContext, UserAgentEvents, UserAgentStateMachine, UserAgentStates};
use state::{DummyContext, UserAgentEvents, UserAgentStateMachine};
// Error for consumption by other actors
#[derive(Debug, thiserror::Error, PartialEq)]
pub enum Error {
#[error("User agent session ended due to connection loss")]
ConnectionLost,
#[error("User agent session ended due to unexpected message")]
UnexpectedMessage,
}
pub struct UserAgentSession {
props: UserAgentConnection,
key: VerifyingKey,
state: UserAgentStateMachine<DummyContext>,
}
mod connection;
impl UserAgentSession {
pub(crate) fn new(props: UserAgentConnection, key: VerifyingKey) -> Self {
pub(crate) fn new(props: UserAgentConnection) -> Self {
Self {
props,
key,
state: UserAgentStateMachine::new(DummyContext),
}
}
fn transition(&mut self, event: UserAgentEvents) -> Result<(), UserAgentError> {
pub(super) async fn send_msg<Reply: kameo::Reply>(
&mut self,
msg: Response,
_ctx: &mut Context<Self, Reply>,
) -> Result<(), Error> {
self.props.transport.send(Ok(msg)).await.map_err(|_| {
error!(
actor = "useragent",
reason = "channel closed",
"send.failed"
);
Error::ConnectionLost
})
}
async fn expect_msg<Extractor, Msg, Reply>(
&mut self,
extractor: Extractor,
ctx: &mut Context<Self, Reply>,
) -> Result<Msg, Error>
where
Extractor: FnOnce(Request) -> Option<Msg>,
Reply: kameo::Reply,
{
let msg = self.props.transport.recv().await.ok_or_else(|| {
error!(
actor = "useragent",
reason = "channel closed",
"recv.failed"
);
ctx.stop();
Error::ConnectionLost
})?;
extractor(msg).ok_or_else(|| {
error!(
actor = "useragent",
reason = "unexpected message",
"recv.failed"
);
ctx.stop();
Error::UnexpectedMessage
})
}
fn transition(&mut self, event: UserAgentEvents) -> Result<(), TransportResponseError> {
self.state.process_event(event).map_err(|e| {
error!(?e, "State transition failed");
UserAgentError::StateTransitionFailed
TransportResponseError::StateTransitionFailed
})?;
Ok(())
}
pub async fn process_transport_inbound(&mut self, req: UserAgentRequest) -> Output {
let msg = req.payload.ok_or_else(|| {
error!(actor = "useragent", "Received message with no payload");
UserAgentError::MissingRequestPayload
})?;
match msg {
UserAgentRequestPayload::UnsealStart(unseal_start) => {
self.handle_unseal_request(unseal_start).await
}
UserAgentRequestPayload::UnsealEncryptedKey(unseal_encrypted_key) => {
self.handle_unseal_encrypted_key(unseal_encrypted_key).await
}
UserAgentRequestPayload::EvmWalletCreate(_) => self.handle_evm_wallet_create().await,
UserAgentRequestPayload::EvmWalletList(_) => self.handle_evm_wallet_list().await,
_ => Err(UserAgentError::UnexpectedRequestPayload),
}
}
}
type Output = Result<UserAgentResponse, UserAgentError>;
fn response(payload: UserAgentResponsePayload) -> UserAgentResponse {
UserAgentResponse {
payload: Some(payload),
}
}
#[messages]
impl UserAgentSession {
async fn handle_unseal_request(&mut self, req: UnsealStart) -> Output {
let secret = EphemeralSecret::random();
let public_key = PublicKey::from(&secret);
let client_pubkey_bytes: [u8; 32] = req
.client_pubkey
.try_into()
.map_err(|_| UserAgentError::InvalidClientPubkeyLength)?;
let client_public_key = PublicKey::from(client_pubkey_bytes);
self.transition(UserAgentEvents::UnsealRequest(UnsealContext {
secret: Mutex::new(Some(secret)),
client_public_key,
}))?;
Ok(response(UserAgentResponsePayload::UnsealStartResponse(
UnsealStartResponse {
server_pubkey: public_key.as_bytes().to_vec(),
// TODO: Think about refactoring it to state-machine based flow, as we already have one
#[message(ctx)]
pub async fn request_new_client_approval(
&mut self,
client_pubkey: VerifyingKey,
mut cancel_flag: watch::Receiver<()>,
ctx: &mut Context<Self, Result<bool, Error>>,
) -> Result<bool, Error> {
self.send_msg(
Response::ClientConnectionRequest {
pubkey: client_pubkey,
},
)))
}
ctx,
)
.await?;
async fn handle_unseal_encrypted_key(&mut self, req: UnsealEncryptedKey) -> Output {
let UserAgentStates::WaitingForUnsealKey(unseal_context) = self.state.state() else {
error!("Received unseal encrypted key in invalid state");
return Err(UserAgentError::InvalidStateForUnsealEncryptedKey);
};
let ephemeral_secret = {
let mut secret_lock = unseal_context.secret.lock().unwrap();
let secret = secret_lock.take();
match secret {
Some(secret) => secret,
None => {
drop(secret_lock);
error!("Ephemeral secret already taken");
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
return Ok(response(UserAgentResponsePayload::UnsealResult(
UnsealResult::InvalidKey.into(),
)));
}
let extractor = |msg| {
if let Request::ClientConnectionResponse { approved } = msg {
Some(approved)
} else {
None
}
};
let nonce = XNonce::from_slice(&req.nonce);
let shared_secret = ephemeral_secret.diffie_hellman(&unseal_context.client_public_key);
let cipher = XChaCha20Poly1305::new(shared_secret.as_bytes().into());
let mut seal_key_buffer = MemSafe::new(req.ciphertext.clone()).unwrap();
let decryption_result = {
let mut write_handle = seal_key_buffer.write().unwrap();
let write_handle = write_handle.deref_mut();
cipher.decrypt_in_place(nonce, &req.associated_data, write_handle)
};
match decryption_result {
Ok(_) => {
match self
.props
.actors
.key_holder
.ask(TryUnseal {
seal_key_raw: seal_key_buffer,
})
.await
{
Ok(_) => {
info!("Successfully unsealed key with client-provided key");
self.transition(UserAgentEvents::ReceivedValidKey)?;
Ok(response(UserAgentResponsePayload::UnsealResult(
UnsealResult::Success.into(),
)))
}
Err(SendError::HandlerError(keyholder::Error::InvalidKey)) => {
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
Ok(response(UserAgentResponsePayload::UnsealResult(
UnsealResult::InvalidKey.into(),
)))
}
Err(SendError::HandlerError(err)) => {
error!(?err, "Keyholder failed to unseal key");
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
Ok(response(UserAgentResponsePayload::UnsealResult(
UnsealResult::InvalidKey.into(),
)))
}
Err(err) => {
error!(?err, "Failed to send unseal request to keyholder");
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
Err(UserAgentError::KeyHolderActorUnreachable)
}
}
tokio::select! {
_ = cancel_flag.changed() => {
info!(actor = "useragent", "client connection approval cancelled");
self.send_msg(
Response::ClientConnectionCancel,
ctx,
).await?;
Ok(false)
}
Err(err) => {
error!(?err, "Failed to decrypt unseal key");
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
Ok(response(UserAgentResponsePayload::UnsealResult(
UnsealResult::InvalidKey.into(),
)))
result = self.expect_msg(extractor, ctx) => {
let result = result?;
info!(actor = "useragent", "received client connection approval result: approved={}", result);
Ok(result)
}
}
}
}
impl UserAgentSession {
async fn handle_evm_wallet_create(&mut self) -> Output {
use evm_proto::wallet_create_response::Result as CreateResult;
let result = match self.props.actors.evm.ask(Generate {}).await {
Ok(address) => CreateResult::Wallet(evm_proto::WalletEntry {
address: address.as_slice().to_vec(),
}),
Err(err) => CreateResult::Error(map_evm_error("wallet create", err).into()),
};
Ok(response(UserAgentResponsePayload::EvmWalletCreate(
evm_proto::WalletCreateResponse {
result: Some(result),
},
)))
}
async fn handle_evm_wallet_list(&mut self) -> Output {
use evm_proto::wallet_list_response::Result as ListResult;
let result = match self.props.actors.evm.ask(ListWallets {}).await {
Ok(wallets) => ListResult::Wallets(evm_proto::WalletList {
wallets: wallets
.into_iter()
.map(|addr| evm_proto::WalletEntry {
address: addr.as_slice().to_vec(),
})
.collect(),
}),
Err(err) => ListResult::Error(map_evm_error("wallet list", err).into()),
};
Ok(response(UserAgentResponsePayload::EvmWalletList(
evm_proto::WalletListResponse {
result: Some(result),
},
)))
}
}
fn map_evm_error<M>(op: &str, err: SendError<M, crate::actors::evm::Error>) -> evm_proto::EvmError {
use crate::actors::{evm::Error as EvmError, keyholder::Error as KhError};
match err {
SendError::HandlerError(EvmError::Keyholder(KhError::NotBootstrapped)) => {
evm_proto::EvmError::VaultSealed
}
SendError::HandlerError(err) => {
error!(?err, "EVM {op} failed");
evm_proto::EvmError::Internal
}
_ => {
error!("EVM actor unreachable during {op}");
evm_proto::EvmError::Internal
}
}
}
impl Actor for UserAgentSession {
type Args = Self;
type Error = UserAgentError;
type Error = TransportResponseError;
async fn on_start(
args: Self::Args,
@@ -263,7 +157,7 @@ impl Actor for UserAgentSession {
.await
.map_err(|err| {
error!(?err, "Failed to register user agent connection with router");
UserAgentError::ConnectionRegistrationFailed
TransportResponseError::ConnectionRegistrationFailed
})?;
Ok(args)
}
@@ -310,10 +204,8 @@ impl UserAgentSession {
use arbiter_proto::transport::DummyTransport;
let transport: super::Transport = Box::new(DummyTransport::new());
let props = UserAgentConnection::new(db, actors, transport);
let key = VerifyingKey::from_bytes(&[0u8; 32]).unwrap();
Self {
props,
key,
state: UserAgentStateMachine::new(DummyContext),
}
}

View File

@@ -0,0 +1,349 @@
use std::{ops::DerefMut, sync::Mutex};
use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit};
use kameo::error::SendError;
use memsafe::MemSafe;
use tracing::{error, info};
use x25519_dalek::{EphemeralSecret, PublicKey};
use crate::actors::{
evm::{Generate, ListWallets, UseragentListGrants},
evm::{UseragentCreateGrant, UseragentDeleteGrant},
keyholder::{self, Bootstrap, TryUnseal},
user_agent::{
BootstrapError, Request, Response, TransportResponseError, UnsealError, VaultState,
session::{
UserAgentSession,
state::{UnsealContext, UserAgentEvents, UserAgentStates},
},
},
};
impl UserAgentSession {
pub async fn process_transport_inbound(&mut self, req: Request) -> Output {
match req {
Request::UnsealStart { client_pubkey } => {
self.handle_unseal_request(client_pubkey).await
}
Request::UnsealEncryptedKey {
nonce,
ciphertext,
associated_data,
} => {
self.handle_unseal_encrypted_key(nonce, ciphertext, associated_data)
.await
}
Request::BootstrapEncryptedKey {
nonce,
ciphertext,
associated_data,
} => {
self.handle_bootstrap_encrypted_key(nonce, ciphertext, associated_data)
.await
}
Request::ListGrants => self.handle_grant_list().await,
Request::QueryVaultState => self.handle_query_vault_state().await,
Request::EvmWalletCreate => self.handle_evm_wallet_create().await,
Request::EvmWalletList => self.handle_evm_wallet_list().await,
Request::AuthChallengeRequest { .. }
| Request::AuthChallengeSolution { .. }
| Request::ClientConnectionResponse { .. } => {
Err(TransportResponseError::UnexpectedRequestPayload)
}
Request::EvmGrantCreate {
client_id,
shared,
specific,
} => self.handle_grant_create(client_id, shared, specific).await,
Request::EvmGrantDelete { grant_id } => self.handle_grant_delete(grant_id).await,
}
}
}
type Output = Result<Response, TransportResponseError>;
impl UserAgentSession {
fn take_unseal_secret(
&mut self,
) -> Result<(EphemeralSecret, PublicKey), TransportResponseError> {
let UserAgentStates::WaitingForUnsealKey(unseal_context) = self.state.state() else {
error!("Received encrypted key in invalid state");
return Err(TransportResponseError::InvalidStateForUnsealEncryptedKey);
};
let ephemeral_secret = {
let mut secret_lock = unseal_context.secret.lock().unwrap();
let secret = secret_lock.take();
match secret {
Some(secret) => secret,
None => {
drop(secret_lock);
error!("Ephemeral secret already taken");
return Err(TransportResponseError::StateTransitionFailed);
}
}
};
Ok((ephemeral_secret, unseal_context.client_public_key))
}
fn decrypt_client_key_material(
ephemeral_secret: EphemeralSecret,
client_public_key: PublicKey,
nonce: &[u8],
ciphertext: &[u8],
associated_data: &[u8],
) -> Result<MemSafe<Vec<u8>>, ()> {
let nonce = XNonce::from_slice(nonce);
let shared_secret = ephemeral_secret.diffie_hellman(&client_public_key);
let cipher = XChaCha20Poly1305::new(shared_secret.as_bytes().into());
let mut key_buffer = MemSafe::new(ciphertext.to_vec()).unwrap();
let decryption_result = {
let mut write_handle = key_buffer.write().unwrap();
let write_handle = write_handle.deref_mut();
cipher.decrypt_in_place(nonce, associated_data, write_handle)
};
match decryption_result {
Ok(_) => Ok(key_buffer),
Err(err) => {
error!(?err, "Failed to decrypt encrypted key material");
Err(())
}
}
}
async fn handle_unseal_request(&mut self, client_pubkey: x25519_dalek::PublicKey) -> Output {
let secret = EphemeralSecret::random();
let public_key = PublicKey::from(&secret);
self.transition(UserAgentEvents::UnsealRequest(UnsealContext {
secret: Mutex::new(Some(secret)),
client_public_key: client_pubkey,
}))?;
Ok(Response::UnsealStartResponse {
server_pubkey: public_key,
})
}
async fn handle_unseal_encrypted_key(
&mut self,
nonce: Vec<u8>,
ciphertext: Vec<u8>,
associated_data: Vec<u8>,
) -> Output {
let (ephemeral_secret, client_public_key) = match self.take_unseal_secret() {
Ok(values) => values,
Err(TransportResponseError::StateTransitionFailed) => {
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
return Ok(Response::UnsealResult(Err(UnsealError::InvalidKey)));
}
Err(err) => return Err(err),
};
let seal_key_buffer = match Self::decrypt_client_key_material(
ephemeral_secret,
client_public_key,
&nonce,
&ciphertext,
&associated_data,
) {
Ok(buffer) => buffer,
Err(()) => {
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
return Ok(Response::UnsealResult(Err(UnsealError::InvalidKey)));
}
};
match self
.props
.actors
.key_holder
.ask(TryUnseal {
seal_key_raw: seal_key_buffer,
})
.await
{
Ok(_) => {
info!("Successfully unsealed key with client-provided key");
self.transition(UserAgentEvents::ReceivedValidKey)?;
Ok(Response::UnsealResult(Ok(())))
}
Err(SendError::HandlerError(keyholder::Error::InvalidKey)) => {
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
Ok(Response::UnsealResult(Err(UnsealError::InvalidKey)))
}
Err(SendError::HandlerError(err)) => {
error!(?err, "Keyholder failed to unseal key");
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
Ok(Response::UnsealResult(Err(UnsealError::InvalidKey)))
}
Err(err) => {
error!(?err, "Failed to send unseal request to keyholder");
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
Err(TransportResponseError::KeyHolderActorUnreachable)
}
}
}
async fn handle_bootstrap_encrypted_key(
&mut self,
nonce: Vec<u8>,
ciphertext: Vec<u8>,
associated_data: Vec<u8>,
) -> Output {
let (ephemeral_secret, client_public_key) = match self.take_unseal_secret() {
Ok(values) => values,
Err(TransportResponseError::StateTransitionFailed) => {
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
return Ok(Response::BootstrapResult(Err(BootstrapError::InvalidKey)));
}
Err(err) => return Err(err),
};
let seal_key_buffer = match Self::decrypt_client_key_material(
ephemeral_secret,
client_public_key,
&nonce,
&ciphertext,
&associated_data,
) {
Ok(buffer) => buffer,
Err(()) => {
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
return Ok(Response::BootstrapResult(Err(BootstrapError::InvalidKey)));
}
};
match self
.props
.actors
.key_holder
.ask(Bootstrap {
seal_key_raw: seal_key_buffer,
})
.await
{
Ok(_) => {
info!("Successfully bootstrapped vault with client-provided key");
self.transition(UserAgentEvents::ReceivedValidKey)?;
Ok(Response::BootstrapResult(Ok(())))
}
Err(SendError::HandlerError(keyholder::Error::AlreadyBootstrapped)) => {
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
Ok(Response::BootstrapResult(Err(
BootstrapError::AlreadyBootstrapped,
)))
}
Err(SendError::HandlerError(err)) => {
error!(?err, "Keyholder failed to bootstrap vault");
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
Ok(Response::BootstrapResult(Err(BootstrapError::InvalidKey)))
}
Err(err) => {
error!(?err, "Failed to send bootstrap request to keyholder");
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
Err(TransportResponseError::KeyHolderActorUnreachable)
}
}
}
}
impl UserAgentSession {
async fn handle_query_vault_state(&mut self) -> Output {
use crate::actors::keyholder::{GetState, StateDiscriminants};
let vault_state = match self.props.actors.key_holder.ask(GetState {}).await {
Ok(StateDiscriminants::Unbootstrapped) => VaultState::Unbootstrapped,
Ok(StateDiscriminants::Sealed) => VaultState::Sealed,
Ok(StateDiscriminants::Unsealed) => VaultState::Unsealed,
Err(err) => {
error!(?err, actor = "useragent", "keyholder.query.failed");
return Err(TransportResponseError::KeyHolderActorUnreachable);
}
};
Ok(Response::VaultState(vault_state))
}
}
impl UserAgentSession {
async fn handle_evm_wallet_create(&mut self) -> Output {
let result = match self.props.actors.evm.ask(Generate {}).await {
Ok(_address) => return Ok(Response::EvmWalletCreate(Ok(()))),
Err(SendError::HandlerError(err)) => Err(err),
Err(err) => {
error!(?err, "EVM actor unreachable during wallet create");
return Err(TransportResponseError::KeyHolderActorUnreachable);
}
};
Ok(Response::EvmWalletCreate(result))
}
async fn handle_evm_wallet_list(&mut self) -> Output {
match self.props.actors.evm.ask(ListWallets {}).await {
Ok(wallets) => Ok(Response::EvmWalletList(wallets)),
Err(err) => {
error!(?err, "EVM wallet list failed");
Err(TransportResponseError::KeyHolderActorUnreachable)
}
}
}
}
impl UserAgentSession {
async fn handle_grant_list(&mut self) -> Output {
match self.props.actors.evm.ask(UseragentListGrants {}).await {
Ok(grants) => Ok(Response::ListGrants(grants)),
Err(err) => {
error!(?err, "EVM grant list failed");
Err(TransportResponseError::KeyHolderActorUnreachable)
}
}
}
async fn handle_grant_create(
&mut self,
client_id: i32,
basic: crate::evm::policies::SharedGrantSettings,
grant: crate::evm::policies::SpecificGrant,
) -> Output {
match self
.props
.actors
.evm
.ask(UseragentCreateGrant {
client_id,
basic,
grant,
})
.await
{
Ok(grant_id) => Ok(Response::EvmGrantCreate(Ok(grant_id))),
Err(err) => {
error!(?err, "EVM grant create failed");
Err(TransportResponseError::KeyHolderActorUnreachable)
}
}
}
async fn handle_grant_delete(&mut self, grant_id: i32) -> Output {
match self
.props
.actors
.evm
.ask(UseragentDeleteGrant { grant_id })
.await
{
Ok(()) => Ok(Response::EvmGrantDelete(Ok(()))),
Err(err) => {
error!(?err, "EVM grant delete failed");
Err(TransportResponseError::KeyHolderActorUnreachable)
}
}
}
}

View File

@@ -8,7 +8,7 @@ use rcgen::{
BasicConstraints, Certificate, CertificateParams, CertifiedIssuer, DistinguishedName, DnType,
IsCa, Issuer, KeyPair, KeyUsagePurpose,
};
use rustls::pki_types::{pem::PemObject};
use rustls::pki_types::pem::PemObject;
use thiserror::Error;
use tonic::transport::CertificateDer;
@@ -59,10 +59,7 @@ pub enum InitError {
pub type PemCert = String;
pub fn encode_cert_to_pem(cert: &CertificateDer) -> PemCert {
pem::encode_config(
&Pem::new("CERTIFICATE", cert.to_vec()),
ENCODE_CONFIG,
)
pem::encode_config(&Pem::new("CERTIFICATE", cert.to_vec()), ENCODE_CONFIG)
}
#[allow(unused)]

View File

@@ -135,10 +135,10 @@ pub async fn create_test_pool() -> DatabasePool {
let tempfile_name = Alphanumeric.sample_string(&mut rand::rng(), 16);
let file = std::env::temp_dir().join(tempfile_name);
let url = format!(
"{}?mode=rwc",
file.to_str().expect("temp file path is not valid UTF-8")
);
let url = file
.to_str()
.expect("temp file path is not valid UTF-8")
.to_string();
create_pool(Some(&url))
.await

View File

@@ -2,15 +2,16 @@
#![allow(clippy::all)]
use crate::db::schema::{
self, aead_encrypted, arbiter_settings, evm_basic_grant, evm_ether_transfer_grant, evm_ether_transfer_grant_target, evm_ether_transfer_limit, evm_token_transfer_grant, evm_token_transfer_log, evm_token_transfer_volume_limit, evm_transaction_log, evm_wallet, root_key_history, tls_history
self, aead_encrypted, arbiter_settings, evm_basic_grant, evm_ether_transfer_grant,
evm_ether_transfer_grant_target, evm_ether_transfer_limit, evm_token_transfer_grant,
evm_token_transfer_log, evm_token_transfer_volume_limit, evm_transaction_log, evm_wallet,
root_key_history, tls_history,
};
use chrono::{DateTime, Utc};
use diesel::{prelude::*, sqlite::Sqlite};
use restructed::Models;
pub mod types {
use std::os::unix;
use chrono::{DateTime, Utc};
use diesel::{
deserialize::{FromSql, FromSqlRow},
@@ -35,9 +36,9 @@ pub mod types {
SqliteTimestamp(dt)
}
}
impl Into<chrono::DateTime<Utc>> for SqliteTimestamp {
fn into(self) -> chrono::DateTime<Utc> {
self.0
impl From<SqliteTimestamp> for chrono::DateTime<Utc> {
fn from(ts: SqliteTimestamp) -> Self {
ts.0
}
}
@@ -65,12 +66,46 @@ pub mod types {
};
let unix_timestamp = bytes.read_long();
let datetime = DateTime::from_timestamp(unix_timestamp, 0)
.ok_or("Timestamp is out of bounds")?;
let datetime =
DateTime::from_timestamp(unix_timestamp, 0).ok_or("Timestamp is out of bounds")?;
Ok(SqliteTimestamp(datetime))
}
}
/// Key algorithm stored in the `useragent_client.key_type` column.
/// Values must stay stable — they are persisted in the database.
#[derive(Debug, Clone, Copy, PartialEq, Eq, FromSqlRow, AsExpression, strum::FromRepr)]
#[diesel(sql_type = Integer)]
#[repr(i32)]
pub enum KeyType {
Ed25519 = 1,
EcdsaSecp256k1 = 2,
Rsa = 3,
}
impl ToSql<Integer, Sqlite> for KeyType {
fn to_sql<'b>(
&'b self,
out: &mut diesel::serialize::Output<'b, '_, Sqlite>,
) -> diesel::serialize::Result {
out.set_value(*self as i32);
Ok(IsNull::No)
}
}
impl FromSql<Integer, Sqlite> for KeyType {
fn from_sql(
mut bytes: <Sqlite as diesel::backend::Backend>::RawValue<'_>,
) -> diesel::deserialize::Result<Self> {
let Some(SqliteType::Long) = bytes.value_type() else {
return Err("Expected Integer for KeyType".into());
};
let discriminant = bytes.read_long();
KeyType::from_repr(discriminant as i32)
.ok_or_else(|| format!("Unknown KeyType discriminant: {discriminant}").into())
}
}
}
pub use types::*;
@@ -150,7 +185,7 @@ pub struct EvmWallet {
pub created_at: SqliteTimestamp,
}
#[derive(Queryable, Debug)]
#[derive(Queryable, Debug, Insertable, Selectable)]
#[diesel(table_name = schema::program_client, check_for_backend(Sqlite))]
pub struct ProgramClient {
pub id: i32,
@@ -168,6 +203,7 @@ pub struct UseragentClient {
pub public_key: Vec<u8>,
pub created_at: SqliteTimestamp,
pub updated_at: SqliteTimestamp,
pub key_type: KeyType,
}
#[derive(Models, Queryable, Debug, Insertable, Selectable)]
@@ -253,7 +289,6 @@ pub struct EvmEtherTransferGrantTarget {
pub address: Vec<u8>,
}
#[derive(Models, Queryable, Debug, Insertable, Selectable)]
#[diesel(table_name = evm_token_transfer_grant, check_for_backend(Sqlite))]
#[view(

View File

@@ -153,6 +153,7 @@ diesel::table! {
public_key -> Binary,
created_at -> Integer,
updated_at -> Integer,
key_type -> Integer,
}
}

View File

@@ -117,18 +117,17 @@ async fn check_shared_constraints(
let now = Utc::now();
// Validity window
if shared.valid_from.map_or(false, |t| now < t) || shared.valid_until.map_or(false, |t| now > t)
{
if shared.valid_from.is_some_and(|t| now < t) || shared.valid_until.is_some_and(|t| now > t) {
violations.push(EvalViolation::InvalidTime);
}
// Gas fee caps
let fee_exceeded = shared
.max_gas_fee_per_gas
.map_or(false, |cap| U256::from(context.max_fee_per_gas) > cap);
let priority_exceeded = shared.max_priority_fee_per_gas.map_or(false, |cap| {
U256::from(context.max_priority_fee_per_gas) > cap
});
.is_some_and(|cap| U256::from(context.max_fee_per_gas) > cap);
let priority_exceeded = shared
.max_priority_fee_per_gas
.is_some_and(|cap| U256::from(context.max_priority_fee_per_gas) > cap);
if fee_exceeded || priority_exceeded {
violations.push(EvalViolation::GasLimitExceeded {
max_gas_fee_per_gas: shared.max_gas_fee_per_gas,
@@ -228,7 +227,7 @@ impl Engine {
.values(&NewEvmBasicGrant {
wallet_id: full_grant.basic.wallet_id,
chain_id: full_grant.basic.chain as i32,
client_id: client_id,
client_id,
valid_from: full_grant.basic.valid_from.map(SqliteTimestamp),
valid_until: full_grant.basic.valid_until.map(SqliteTimestamp),
max_gas_fee_per_gas: full_grant

View File

@@ -66,6 +66,7 @@ pub enum EvalViolation {
pub type DatabaseID = i32;
#[derive(Debug)]
pub struct Grant<PolicySettings> {
pub id: DatabaseID,
pub shared_grant_id: DatabaseID, // ID of the basic grant for shared-logic checks like rate limits and validity periods
@@ -73,7 +74,6 @@ pub struct Grant<PolicySettings> {
pub settings: PolicySettings,
}
pub trait Policy: Sized {
type Settings: Send + Sync + 'static + Into<SpecificGrant>;
type Meaning: Display + std::fmt::Debug + Send + Sync + 'static + Into<SpecificMeaning>;
@@ -146,6 +146,7 @@ pub struct VolumeRateLimit {
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct SharedGrantSettings {
pub wallet_id: i32,
pub client_id: i32,
pub chain: ChainId,
pub valid_from: Option<DateTime<Utc>>,
@@ -161,6 +162,7 @@ impl SharedGrantSettings {
fn try_from_model(model: EvmBasicGrant) -> QueryResult<Self> {
Ok(Self {
wallet_id: model.wallet_id,
client_id: model.client_id,
chain: model.chain_id as u64, // safe because chain_id is stored as i32 but is guaranteed to be a valid ChainId by the API when creating grants
valid_from: model.valid_from.map(Into::into),
valid_until: model.valid_until.map(Into::into),
@@ -198,6 +200,7 @@ impl SharedGrantSettings {
}
}
#[derive(Debug, Clone)]
pub enum SpecificGrant {
EtherTransfer(ether_transfer::Settings),
TokenTransfer(token_transfers::Settings),

View File

@@ -41,29 +41,25 @@ pub struct Meaning {
}
impl Display for Meaning {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Ether transfer of {} to {}",
self.value,
self.to.to_string()
)
write!(f, "Ether transfer of {} to {}", self.value, self.to)
}
}
impl Into<SpecificMeaning> for Meaning {
fn into(self) -> SpecificMeaning {
SpecificMeaning::EtherTransfer(self)
impl From<Meaning> for SpecificMeaning {
fn from(val: Meaning) -> SpecificMeaning {
SpecificMeaning::EtherTransfer(val)
}
}
// A grant for ether transfers, which can be scoped to specific target addresses and volume limits
#[derive(Debug, Clone)]
pub struct Settings {
target: Vec<Address>,
limit: VolumeRateLimit,
pub target: Vec<Address>,
pub limit: VolumeRateLimit,
}
impl Into<SpecificGrant> for Settings {
fn into(self) -> SpecificGrant {
SpecificGrant::EtherTransfer(self)
impl From<Settings> for SpecificGrant {
fn from(val: Settings) -> SpecificGrant {
SpecificGrant::EtherTransfer(val)
}
}

View File

@@ -9,9 +9,7 @@ use crate::db::{
schema::{evm_basic_grant, evm_transaction_log},
};
use crate::evm::{
policies::{
EvalContext, EvalViolation, Grant, Policy, SharedGrantSettings, VolumeRateLimit,
},
policies::{EvalContext, EvalViolation, Grant, Policy, SharedGrantSettings, VolumeRateLimit},
utils,
};
@@ -76,6 +74,7 @@ fn shared() -> SharedGrantSettings {
max_gas_fee_per_gas: None,
max_priority_fee_per_gas: None,
rate_limit: None,
client_id: CLIENT_ID,
}
}

View File

@@ -51,21 +51,22 @@ impl std::fmt::Display for Meaning {
)
}
}
impl Into<SpecificMeaning> for Meaning {
fn into(self) -> SpecificMeaning {
SpecificMeaning::TokenTransfer(self)
impl From<Meaning> for SpecificMeaning {
fn from(val: Meaning) -> SpecificMeaning {
SpecificMeaning::TokenTransfer(val)
}
}
// A grant for token transfers, which can be scoped to specific target addresses and volume limits
#[derive(Debug, Clone)]
pub struct Settings {
token_contract: Address,
target: Option<Address>,
volume_limits: Vec<VolumeRateLimit>,
pub token_contract: Address,
pub target: Option<Address>,
pub volume_limits: Vec<VolumeRateLimit>,
}
impl Into<SpecificGrant> for Settings {
fn into(self) -> SpecificGrant {
SpecificGrant::TokenTransfer(self)
impl From<Settings> for SpecificGrant {
fn from(val: Settings) -> SpecificGrant {
SpecificGrant::TokenTransfer(val)
}
}
@@ -156,10 +157,10 @@ impl Policy for TokenTransfer {
return Ok(violations);
}
if let Some(allowed) = grant.settings.target {
if allowed != meaning.to {
violations.push(EvalViolation::InvalidTarget { target: meaning.to });
}
if let Some(allowed) = grant.settings.target
&& allowed != meaning.to
{
violations.push(EvalViolation::InvalidTarget { target: meaning.to });
}
let rate_violations = check_volume_rate_limits(grant, db).await?;

View File

@@ -93,6 +93,7 @@ fn shared() -> SharedGrantSettings {
max_gas_fee_per_gas: None,
max_priority_fee_per_gas: None,
rate_limit: None,
client_id: CLIENT_ID,
}
}
@@ -140,10 +141,18 @@ async fn evaluate_rejects_nonzero_eth_value() {
let mut context = ctx(DAI, calldata);
context.value = U256::from(1u64); // ETH attached to an ERC-20 call
let m = TokenTransfer::analyze(&EvalContext { value: U256::ZERO, ..context.clone() })
let m = TokenTransfer::analyze(&EvalContext {
value: U256::ZERO,
..context.clone()
})
.unwrap();
let v = TokenTransfer::evaluate(&context, &m, &grant, &mut *conn)
.await
.unwrap();
let v = TokenTransfer::evaluate(&context, &m, &grant, &mut *conn).await.unwrap();
assert!(v.iter().any(|e| matches!(e, EvalViolation::InvalidTransactionType)));
assert!(
v.iter()
.any(|e| matches!(e, EvalViolation::InvalidTransactionType))
);
}
#[tokio::test]
@@ -160,7 +169,9 @@ async fn evaluate_passes_any_recipient_when_no_restriction() {
let calldata = transfer_calldata(RECIPIENT, U256::from(100u64));
let context = ctx(DAI, calldata);
let m = TokenTransfer::analyze(&context).unwrap();
let v = TokenTransfer::evaluate(&context, &m, &grant, &mut *conn).await.unwrap();
let v = TokenTransfer::evaluate(&context, &m, &grant, &mut *conn)
.await
.unwrap();
assert!(v.is_empty());
}
@@ -178,7 +189,9 @@ async fn evaluate_passes_matching_restricted_recipient() {
let calldata = transfer_calldata(RECIPIENT, U256::from(100u64));
let context = ctx(DAI, calldata);
let m = TokenTransfer::analyze(&context).unwrap();
let v = TokenTransfer::evaluate(&context, &m, &grant, &mut *conn).await.unwrap();
let v = TokenTransfer::evaluate(&context, &m, &grant, &mut *conn)
.await
.unwrap();
assert!(v.is_empty());
}
@@ -196,8 +209,13 @@ async fn evaluate_rejects_wrong_restricted_recipient() {
let calldata = transfer_calldata(OTHER, U256::from(100u64));
let context = ctx(DAI, calldata);
let m = TokenTransfer::analyze(&context).unwrap();
let v = TokenTransfer::evaluate(&context, &m, &grant, &mut *conn).await.unwrap();
assert!(v.iter().any(|e| matches!(e, EvalViolation::InvalidTarget { .. })));
let v = TokenTransfer::evaluate(&context, &m, &grant, &mut *conn)
.await
.unwrap();
assert!(
v.iter()
.any(|e| matches!(e, EvalViolation::InvalidTarget { .. }))
);
}
#[tokio::test]
@@ -207,7 +225,9 @@ async fn evaluate_passes_volume_within_limit() {
let basic = insert_basic(&mut conn, false).await;
let settings = make_settings(None, Some(1_000));
let grant_id = TokenTransfer::create_grant(&basic, &settings, &mut *conn).await.unwrap();
let grant_id = TokenTransfer::create_grant(&basic, &settings, &mut *conn)
.await
.unwrap();
// Record a past transfer of 500 (within 1000 limit)
use crate::db::{models::NewEvmTokenTransferLog, schema::evm_token_transfer_log};
@@ -224,12 +244,22 @@ async fn evaluate_passes_volume_within_limit() {
.await
.unwrap();
let grant = Grant { id: grant_id, shared_grant_id: basic.id, shared: shared(), settings };
let grant = Grant {
id: grant_id,
shared_grant_id: basic.id,
shared: shared(),
settings,
};
let calldata = transfer_calldata(RECIPIENT, U256::from(100u64));
let context = ctx(DAI, calldata);
let m = TokenTransfer::analyze(&context).unwrap();
let v = TokenTransfer::evaluate(&context, &m, &grant, &mut *conn).await.unwrap();
assert!(!v.iter().any(|e| matches!(e, EvalViolation::VolumetricLimitExceeded)));
let v = TokenTransfer::evaluate(&context, &m, &grant, &mut *conn)
.await
.unwrap();
assert!(
!v.iter()
.any(|e| matches!(e, EvalViolation::VolumetricLimitExceeded))
);
}
#[tokio::test]
@@ -239,7 +269,9 @@ async fn evaluate_rejects_volume_over_limit() {
let basic = insert_basic(&mut conn, false).await;
let settings = make_settings(None, Some(1_000));
let grant_id = TokenTransfer::create_grant(&basic, &settings, &mut *conn).await.unwrap();
let grant_id = TokenTransfer::create_grant(&basic, &settings, &mut *conn)
.await
.unwrap();
use crate::db::{models::NewEvmTokenTransferLog, schema::evm_token_transfer_log};
insert_into(evm_token_transfer_log::table)
@@ -255,12 +287,22 @@ async fn evaluate_rejects_volume_over_limit() {
.await
.unwrap();
let grant = Grant { id: grant_id, shared_grant_id: basic.id, shared: shared(), settings };
let grant = Grant {
id: grant_id,
shared_grant_id: basic.id,
shared: shared(),
settings,
};
let calldata = transfer_calldata(RECIPIENT, U256::from(100u64));
let context = ctx(DAI, calldata);
let m = TokenTransfer::analyze(&context).unwrap();
let v = TokenTransfer::evaluate(&context, &m, &grant, &mut *conn).await.unwrap();
assert!(v.iter().any(|e| matches!(e, EvalViolation::VolumetricLimitExceeded)));
let v = TokenTransfer::evaluate(&context, &m, &grant, &mut *conn)
.await
.unwrap();
assert!(
v.iter()
.any(|e| matches!(e, EvalViolation::VolumetricLimitExceeded))
);
}
#[tokio::test]
@@ -277,8 +319,13 @@ async fn evaluate_no_volume_limits_always_passes() {
let calldata = transfer_calldata(RECIPIENT, U256::from(u64::MAX));
let context = ctx(DAI, calldata);
let m = TokenTransfer::analyze(&context).unwrap();
let v = TokenTransfer::evaluate(&context, &m, &grant, &mut *conn).await.unwrap();
assert!(!v.iter().any(|e| matches!(e, EvalViolation::VolumetricLimitExceeded)));
let v = TokenTransfer::evaluate(&context, &m, &grant, &mut *conn)
.await
.unwrap();
assert!(
!v.iter()
.any(|e| matches!(e, EvalViolation::VolumetricLimitExceeded))
);
}
// ── try_find_grant ───────────────────────────────────────────────────────
@@ -290,7 +337,9 @@ async fn try_find_grant_roundtrip() {
let basic = insert_basic(&mut conn, false).await;
let settings = make_settings(Some(RECIPIENT), Some(5_000));
TokenTransfer::create_grant(&basic, &settings, &mut *conn).await.unwrap();
TokenTransfer::create_grant(&basic, &settings, &mut *conn)
.await
.unwrap();
let calldata = transfer_calldata(RECIPIENT, U256::from(100u64));
let found = TokenTransfer::try_find_grant(&ctx(DAI, calldata), &mut *conn)
@@ -312,7 +361,9 @@ async fn try_find_grant_revoked_returns_none() {
let basic = insert_basic(&mut conn, true).await;
let settings = make_settings(None, None);
TokenTransfer::create_grant(&basic, &settings, &mut *conn).await.unwrap();
TokenTransfer::create_grant(&basic, &settings, &mut *conn)
.await
.unwrap();
let calldata = transfer_calldata(RECIPIENT, U256::from(1u64));
let found = TokenTransfer::try_find_grant(&ctx(DAI, calldata), &mut *conn)
@@ -328,7 +379,9 @@ async fn try_find_grant_unknown_token_returns_none() {
let basic = insert_basic(&mut conn, false).await;
let settings = make_settings(None, None);
TokenTransfer::create_grant(&basic, &settings, &mut *conn).await.unwrap();
TokenTransfer::create_grant(&basic, &settings, &mut *conn)
.await
.unwrap();
// Query with a different token contract
let calldata = transfer_calldata(RECIPIENT, U256::from(1u64));
@@ -355,9 +408,13 @@ async fn find_all_grants_excludes_revoked() {
let settings = make_settings(None, Some(1_000));
let active = insert_basic(&mut conn, false).await;
TokenTransfer::create_grant(&active, &settings, &mut *conn).await.unwrap();
TokenTransfer::create_grant(&active, &settings, &mut *conn)
.await
.unwrap();
let revoked = insert_basic(&mut conn, true).await;
TokenTransfer::create_grant(&revoked, &settings, &mut *conn).await.unwrap();
TokenTransfer::create_grant(&revoked, &settings, &mut *conn)
.await
.unwrap();
let all = TokenTransfer::find_all_grants(&mut *conn).await.unwrap();
assert_eq!(all.len(), 1);
@@ -370,12 +427,17 @@ async fn find_all_grants_loads_volume_limits() {
let basic = insert_basic(&mut conn, false).await;
let settings = make_settings(None, Some(9_999));
TokenTransfer::create_grant(&basic, &settings, &mut *conn).await.unwrap();
TokenTransfer::create_grant(&basic, &settings, &mut *conn)
.await
.unwrap();
let all = TokenTransfer::find_all_grants(&mut *conn).await.unwrap();
assert_eq!(all.len(), 1);
assert_eq!(all[0].settings.volume_limits.len(), 1);
assert_eq!(all[0].settings.volume_limits[0].max_volume, U256::from(9_999u64));
assert_eq!(
all[0].settings.volume_limits[0].max_volume,
U256::from(9_999u64)
);
}
#[tokio::test]
@@ -388,9 +450,13 @@ async fn find_all_grants_multiple_grants_batch_loaded() {
.await
.unwrap();
let b2 = insert_basic(&mut conn, false).await;
TokenTransfer::create_grant(&b2, &make_settings(Some(RECIPIENT), Some(2_000)), &mut *conn)
.await
.unwrap();
TokenTransfer::create_grant(
&b2,
&make_settings(Some(RECIPIENT), Some(2_000)),
&mut *conn,
)
.await
.unwrap();
let all = TokenTransfer::find_all_grants(&mut *conn).await.unwrap();
assert_eq!(all.len(), 2);

View File

@@ -3,11 +3,11 @@ use std::sync::Mutex;
use alloy::{
consensus::SignableTransaction,
network::{TxSigner, TxSignerSync},
primitives::{Address, ChainId, Signature, B256},
primitives::{Address, B256, ChainId, Signature},
signers::{Error, Result, Signer, SignerSync, utils::secret_key_to_address},
};
use async_trait::async_trait;
use k256::ecdsa::{self, signature::hazmat::PrehashSigner, RecoveryId, SigningKey};
use k256::ecdsa::{self, RecoveryId, SigningKey, signature::hazmat::PrehashSigner};
use memsafe::MemSafe;
/// An Ethereum signer that stores its secp256k1 secret key inside a
@@ -90,19 +90,17 @@ impl SafeSigner {
Ok(sig.into())
}
fn sign_tx_inner(
&self,
tx: &mut dyn SignableTransaction<Signature>,
) -> Result<Signature> {
if let Some(chain_id) = self.chain_id {
if !tx.set_chain_id_checked(chain_id) {
return Err(Error::TransactionChainIdMismatch {
signer: chain_id,
tx: tx.chain_id().unwrap(),
});
}
fn sign_tx_inner(&self, tx: &mut dyn SignableTransaction<Signature>) -> Result<Signature> {
if let Some(chain_id) = self.chain_id
&& !tx.set_chain_id_checked(chain_id)
{
return Err(Error::TransactionChainIdMismatch {
signer: chain_id,
tx: tx.chain_id().unwrap(),
});
}
self.sign_hash_inner(&tx.signature_hash()).map_err(Error::other)
self.sign_hash_inner(&tx.signature_hash())
.map_err(Error::other)
}
}

View File

@@ -0,0 +1,137 @@
use arbiter_proto::{
proto::client::{
AuthChallenge as ProtoAuthChallenge,
AuthChallengeRequest as ProtoAuthChallengeRequest,
AuthChallengeSolution as ProtoAuthChallengeSolution, AuthOk as ProtoAuthOk,
ClientConnectError, ClientRequest, ClientResponse,
client_connect_error::Code as ProtoClientConnectErrorCode,
client_request::Payload as ClientRequestPayload,
client_response::Payload as ClientResponsePayload,
},
transport::{Bi, Error as TransportError},
};
use async_trait::async_trait;
use futures::StreamExt as _;
use tokio::sync::mpsc;
use tonic::{Status, Streaming};
use crate::actors::client::{
self, ClientError, ConnectErrorCode, Request as DomainRequest, Response as DomainResponse,
};
pub struct GrpcTransport {
sender: mpsc::Sender<Result<ClientResponse, Status>>,
receiver: Streaming<ClientRequest>,
}
impl GrpcTransport {
pub fn new(
sender: mpsc::Sender<Result<ClientResponse, Status>>,
receiver: Streaming<ClientRequest>,
) -> Self {
Self { sender, receiver }
}
fn request_to_domain(request: ClientRequest) -> Result<DomainRequest, Status> {
match request.payload {
Some(ClientRequestPayload::AuthChallengeRequest(ProtoAuthChallengeRequest {
pubkey,
})) => Ok(DomainRequest::AuthChallengeRequest { pubkey }),
Some(ClientRequestPayload::AuthChallengeSolution(
ProtoAuthChallengeSolution { signature },
)) => Ok(DomainRequest::AuthChallengeSolution { signature }),
None => Err(Status::invalid_argument("Missing client request payload")),
}
}
fn response_to_proto(response: DomainResponse) -> ClientResponse {
let payload = match response {
DomainResponse::AuthChallenge { pubkey, nonce } => {
ClientResponsePayload::AuthChallenge(ProtoAuthChallenge { pubkey, nonce })
}
DomainResponse::AuthOk => ClientResponsePayload::AuthOk(ProtoAuthOk {}),
DomainResponse::ClientConnectError { code } => {
ClientResponsePayload::ClientConnectError(ClientConnectError {
code: match code {
ConnectErrorCode::Unknown => ProtoClientConnectErrorCode::Unknown,
ConnectErrorCode::ApprovalDenied => {
ProtoClientConnectErrorCode::ApprovalDenied
}
ConnectErrorCode::NoUserAgentsOnline => {
ProtoClientConnectErrorCode::NoUserAgentsOnline
}
}
.into(),
})
}
};
ClientResponse {
payload: Some(payload),
}
}
fn error_to_status(value: ClientError) -> Status {
match value {
ClientError::MissingRequestPayload | ClientError::UnexpectedRequestPayload => {
Status::invalid_argument("Expected message with payload")
}
ClientError::StateTransitionFailed => Status::internal("State machine error"),
ClientError::Auth(ref err) => auth_error_status(err),
ClientError::ConnectionRegistrationFailed => {
Status::internal("Connection registration failed")
}
}
}
}
#[async_trait]
impl Bi<DomainRequest, Result<DomainResponse, ClientError>> for GrpcTransport {
async fn send(&mut self, item: Result<DomainResponse, ClientError>) -> Result<(), TransportError> {
let outbound = match item {
Ok(message) => Ok(Self::response_to_proto(message)),
Err(err) => Err(Self::error_to_status(err)),
};
self.sender
.send(outbound)
.await
.map_err(|_| TransportError::ChannelClosed)
}
async fn recv(&mut self) -> Option<DomainRequest> {
match self.receiver.next().await {
Some(Ok(item)) => match Self::request_to_domain(item) {
Ok(request) => Some(request),
Err(status) => {
let _ = self.sender.send(Err(status)).await;
None
}
},
Some(Err(error)) => {
tracing::error!(error = ?error, "grpc client recv failed; closing stream");
None
}
None => None,
}
}
}
fn auth_error_status(value: &client::auth::Error) -> Status {
use client::auth::Error;
match value {
Error::UnexpectedMessagePayload | Error::InvalidClientPubkeyLength => {
Status::invalid_argument(value.to_string())
}
Error::InvalidAuthPubkeyEncoding => {
Status::invalid_argument("Failed to convert pubkey to VerifyingKey")
}
Error::InvalidChallengeSolution => Status::unauthenticated(value.to_string()),
Error::ApproveError(_) => Status::permission_denied(value.to_string()),
Error::Transport => Status::internal("Transport error"),
Error::DatabasePoolUnavailable => Status::internal("Database pool error"),
Error::DatabaseOperationFailed => Status::internal("Database error"),
Error::InternalError => Status::internal("Internal error"),
}
}

View File

@@ -0,0 +1,65 @@
use arbiter_proto::proto::{
client::{ClientRequest, ClientResponse},
user_agent::{UserAgentRequest, UserAgentResponse},
};
use tokio::sync::mpsc;
use tokio_stream::wrappers::ReceiverStream;
use tonic::{Request, Response, Status, async_trait};
use tracing::info;
use crate::{
DEFAULT_CHANNEL_SIZE,
actors::{client::{ClientConnection, connect_client}, user_agent::{UserAgentConnection, connect_user_agent}},
};
pub mod client;
pub mod user_agent;
#[async_trait]
impl arbiter_proto::proto::arbiter_service_server::ArbiterService for super::Server {
type UserAgentStream = ReceiverStream<Result<UserAgentResponse, Status>>;
type ClientStream = ReceiverStream<Result<ClientResponse, Status>>;
#[tracing::instrument(level = "debug", skip(self))]
async fn client(
&self,
request: Request<tonic::Streaming<ClientRequest>>,
) -> Result<Response<Self::ClientStream>, Status> {
let req_stream = request.into_inner();
let (tx, rx) = mpsc::channel(DEFAULT_CHANNEL_SIZE);
let transport = client::GrpcTransport::new(tx, req_stream);
let props = ClientConnection::new(
self.context.db.clone(),
Box::new(transport),
self.context.actors.clone(),
);
tokio::spawn(connect_client(props));
info!(event = "connection established", "grpc.client");
Ok(Response::new(ReceiverStream::new(rx)))
}
#[tracing::instrument(level = "debug", skip(self))]
async fn user_agent(
&self,
request: Request<tonic::Streaming<UserAgentRequest>>,
) -> Result<Response<Self::UserAgentStream>, Status> {
let req_stream = request.into_inner();
let (tx, rx) = mpsc::channel(DEFAULT_CHANNEL_SIZE);
let transport = user_agent::GrpcTransport::new(tx, req_stream);
let props = UserAgentConnection::new(
self.context.db.clone(),
self.context.actors.clone(),
Box::new(transport),
);
tokio::spawn(connect_user_agent(props));
info!(event = "connection established", "grpc.user_agent");
Ok(Response::new(ReceiverStream::new(rx)))
}
}

View File

@@ -0,0 +1,511 @@
use arbiter_proto::{
proto::{
self,
evm::{
EtherTransferSettings as ProtoEtherTransferSettings, EvmError as ProtoEvmError,
EvmGrantCreateRequest, EvmGrantCreateResponse, EvmGrantDeleteRequest,
EvmGrantDeleteResponse, EvmGrantList, EvmGrantListResponse, GrantEntry,
SharedSettings as ProtoSharedSettings, SpecificGrant as ProtoSpecificGrant,
SpecificGrant as ProtoGrantSpecificGrant,
TokenTransferSettings as ProtoTokenTransferSettings,
VolumeRateLimit as ProtoVolumeRateLimit, WalletCreateResponse, WalletEntry, WalletList,
WalletListResponse, evm_grant_create_response::Result as EvmGrantCreateResult,
evm_grant_delete_response::Result as EvmGrantDeleteResult,
evm_grant_list_response::Result as EvmGrantListResult,
specific_grant::Grant as ProtoSpecificGrantType,
wallet_create_response::Result as WalletCreateResult,
wallet_list_response::Result as WalletListResult,
},
user_agent::{
AuthChallenge as ProtoAuthChallenge, AuthChallengeRequest as ProtoAuthChallengeRequest,
AuthChallengeSolution as ProtoAuthChallengeSolution, AuthOk as ProtoAuthOk,
BootstrapEncryptedKey as ProtoBootstrapEncryptedKey,
BootstrapResult as ProtoBootstrapResult, ClientConnectionCancel,
ClientConnectionRequest, ClientConnectionResponse, KeyType as ProtoKeyType,
UnsealEncryptedKey as ProtoUnsealEncryptedKey, UnsealResult as ProtoUnsealResult,
UnsealStart, UnsealStartResponse, UserAgentRequest, UserAgentResponse,
VaultState as ProtoVaultState, user_agent_request::Payload as UserAgentRequestPayload,
user_agent_response::Payload as UserAgentResponsePayload,
},
},
transport::{Bi, Error as TransportError},
};
use async_trait::async_trait;
use futures::StreamExt as _;
use prost_types::Timestamp;
use tokio::sync::mpsc;
use tonic::{Status, Streaming};
use crate::{
actors::user_agent::{
self, AuthPublicKey, BootstrapError, Request as DomainRequest, Response as DomainResponse,
TransportResponseError, UnsealError, VaultState,
},
evm::{
self,
policies::{Grant, SpecificGrant},
policies::{
SharedGrantSettings, TransactionRateLimit, VolumeRateLimit, ether_transfer,
token_transfers,
},
},
};
use alloy::primitives::{Address, U256};
use chrono::{DateTime, TimeZone, Utc};
pub struct GrpcTransport {
sender: mpsc::Sender<Result<UserAgentResponse, Status>>,
receiver: Streaming<UserAgentRequest>,
}
impl GrpcTransport {
pub fn new(
sender: mpsc::Sender<Result<UserAgentResponse, Status>>,
receiver: Streaming<UserAgentRequest>,
) -> Self {
Self { sender, receiver }
}
fn request_to_domain(request: UserAgentRequest) -> Result<DomainRequest, Status> {
match request.payload {
Some(UserAgentRequestPayload::AuthChallengeRequest(ProtoAuthChallengeRequest {
pubkey,
bootstrap_token,
key_type,
})) => Ok(DomainRequest::AuthChallengeRequest {
pubkey: parse_auth_pubkey(key_type, pubkey)?,
bootstrap_token,
}),
Some(UserAgentRequestPayload::AuthChallengeSolution(ProtoAuthChallengeSolution {
signature,
})) => Ok(DomainRequest::AuthChallengeSolution { signature }),
Some(UserAgentRequestPayload::UnsealStart(UnsealStart { client_pubkey })) => {
let client_pubkey: [u8; 32] = client_pubkey
.as_slice()
.try_into()
.map_err(|_| Status::invalid_argument("client_pubkey must be 32 bytes"))?;
Ok(DomainRequest::UnsealStart {
client_pubkey: x25519_dalek::PublicKey::from(client_pubkey),
})
}
Some(UserAgentRequestPayload::UnsealEncryptedKey(ProtoUnsealEncryptedKey {
nonce,
ciphertext,
associated_data,
})) => Ok(DomainRequest::UnsealEncryptedKey {
nonce,
ciphertext,
associated_data,
}),
Some(UserAgentRequestPayload::BootstrapEncryptedKey(ProtoBootstrapEncryptedKey {
nonce,
ciphertext,
associated_data,
})) => Ok(DomainRequest::BootstrapEncryptedKey {
nonce,
ciphertext,
associated_data,
}),
Some(UserAgentRequestPayload::QueryVaultState(_)) => Ok(DomainRequest::QueryVaultState),
Some(UserAgentRequestPayload::EvmWalletCreate(_)) => Ok(DomainRequest::EvmWalletCreate),
Some(UserAgentRequestPayload::EvmWalletList(_)) => Ok(DomainRequest::EvmWalletList),
Some(UserAgentRequestPayload::ClientConnectionResponse(ClientConnectionResponse {
approved,
})) => Ok(DomainRequest::ClientConnectionResponse { approved }),
Some(UserAgentRequestPayload::EvmGrantList(_)) => Ok(DomainRequest::ListGrants),
Some(UserAgentRequestPayload::EvmGrantCreate(EvmGrantCreateRequest {
client_id,
shared,
specific,
})) => {
let shared = parse_shared_settings(client_id, shared)?;
let specific = parse_specific_grant(specific)?;
Ok(DomainRequest::EvmGrantCreate {
client_id,
shared,
specific,
})
}
Some(UserAgentRequestPayload::EvmGrantDelete(EvmGrantDeleteRequest { grant_id })) => {
Ok(DomainRequest::EvmGrantDelete { grant_id })
}
None => Err(Status::invalid_argument(
"Missing user-agent request payload",
)),
}
}
fn response_to_proto(response: DomainResponse) -> UserAgentResponse {
let payload = match response {
DomainResponse::AuthChallenge { nonce } => {
UserAgentResponsePayload::AuthChallenge(ProtoAuthChallenge {
pubkey: Vec::new(),
nonce,
})
}
DomainResponse::AuthOk => UserAgentResponsePayload::AuthOk(ProtoAuthOk {}),
DomainResponse::UnsealStartResponse { server_pubkey } => {
UserAgentResponsePayload::UnsealStartResponse(UnsealStartResponse {
server_pubkey: server_pubkey.as_bytes().to_vec(),
})
}
DomainResponse::UnsealResult(result) => UserAgentResponsePayload::UnsealResult(
match result {
Ok(()) => ProtoUnsealResult::Success,
Err(UnsealError::InvalidKey) => ProtoUnsealResult::InvalidKey,
Err(UnsealError::Unbootstrapped) => ProtoUnsealResult::Unbootstrapped,
}
.into(),
),
DomainResponse::BootstrapResult(result) => UserAgentResponsePayload::BootstrapResult(
match result {
Ok(()) => ProtoBootstrapResult::Success,
Err(BootstrapError::AlreadyBootstrapped) => {
ProtoBootstrapResult::AlreadyBootstrapped
}
Err(BootstrapError::InvalidKey) => ProtoBootstrapResult::InvalidKey,
}
.into(),
),
DomainResponse::VaultState(state) => UserAgentResponsePayload::VaultState(
match state {
VaultState::Unbootstrapped => ProtoVaultState::Unbootstrapped,
VaultState::Sealed => ProtoVaultState::Sealed,
VaultState::Unsealed => ProtoVaultState::Unsealed,
}
.into(),
),
DomainResponse::ClientConnectionRequest { pubkey } => {
UserAgentResponsePayload::ClientConnectionRequest(ClientConnectionRequest {
pubkey: pubkey.to_bytes().to_vec(),
})
}
DomainResponse::ClientConnectionCancel => {
UserAgentResponsePayload::ClientConnectionCancel(ClientConnectionCancel {})
}
DomainResponse::EvmWalletCreate(result) => {
UserAgentResponsePayload::EvmWalletCreate(WalletCreateResponse {
result: Some(match result {
Ok(()) => WalletCreateResult::Wallet(WalletEntry {
address: Vec::new(),
}),
Err(_) => WalletCreateResult::Error(ProtoEvmError::Internal.into()),
}),
})
}
DomainResponse::EvmWalletList(wallets) => {
UserAgentResponsePayload::EvmWalletList(WalletListResponse {
result: Some(WalletListResult::Wallets(WalletList {
wallets: wallets
.into_iter()
.map(|addr| WalletEntry {
address: addr.as_slice().to_vec(),
})
.collect(),
})),
})
}
DomainResponse::ListGrants(grants) => {
UserAgentResponsePayload::EvmGrantList(EvmGrantListResponse {
result: Some(EvmGrantListResult::Grants(EvmGrantList {
grants: grants.into_iter().map(grant_to_proto).collect(),
})),
})
}
DomainResponse::EvmGrantCreate(result) => {
UserAgentResponsePayload::EvmGrantCreate(EvmGrantCreateResponse {
result: Some(match result {
Ok(grant_id) => EvmGrantCreateResult::GrantId(grant_id),
Err(_) => EvmGrantCreateResult::Error(ProtoEvmError::Internal.into()),
}),
})
}
DomainResponse::EvmGrantDelete(result) => {
UserAgentResponsePayload::EvmGrantDelete(EvmGrantDeleteResponse {
result: Some(match result {
Ok(()) => EvmGrantDeleteResult::Ok(()),
Err(_) => EvmGrantDeleteResult::Error(ProtoEvmError::Internal.into()),
}),
})
}
};
UserAgentResponse {
payload: Some(payload),
}
}
fn error_to_status(value: TransportResponseError) -> Status {
match value {
TransportResponseError::UnexpectedRequestPayload => {
Status::invalid_argument("Expected message with payload")
}
TransportResponseError::InvalidStateForUnsealEncryptedKey => {
Status::failed_precondition("Invalid state for unseal encrypted key")
}
TransportResponseError::InvalidClientPubkeyLength => {
Status::invalid_argument("client_pubkey must be 32 bytes")
}
TransportResponseError::StateTransitionFailed => {
Status::internal("State machine error")
}
TransportResponseError::KeyHolderActorUnreachable => {
Status::internal("Vault is not available")
}
TransportResponseError::Auth(ref err) => auth_error_status(err),
TransportResponseError::ConnectionRegistrationFailed => {
Status::internal("Failed registering connection")
}
}
}
}
#[async_trait]
impl Bi<DomainRequest, Result<DomainResponse, TransportResponseError>> for GrpcTransport {
async fn send(
&mut self,
item: Result<DomainResponse, TransportResponseError>,
) -> Result<(), TransportError> {
let outbound = match item {
Ok(message) => Ok(Self::response_to_proto(message)),
Err(err) => Err(Self::error_to_status(err)),
};
self.sender
.send(outbound)
.await
.map_err(|_| TransportError::ChannelClosed)
}
async fn recv(&mut self) -> Option<DomainRequest> {
match self.receiver.next().await {
Some(Ok(item)) => match Self::request_to_domain(item) {
Ok(request) => Some(request),
Err(status) => {
let _ = self.sender.send(Err(status)).await;
None
}
},
Some(Err(error)) => {
tracing::error!(error = ?error, "grpc user-agent recv failed; closing stream");
None
}
None => None,
}
}
}
fn grant_to_proto(grant: Grant<SpecificGrant>) -> proto::evm::GrantEntry {
GrantEntry {
id: grant.id,
specific: Some(match grant.settings {
SpecificGrant::EtherTransfer(settings) => ProtoSpecificGrant {
grant: Some(ProtoSpecificGrantType::EtherTransfer(
ProtoEtherTransferSettings {
targets: settings
.target
.into_iter()
.map(|addr| addr.as_slice().to_vec())
.collect(),
limit: Some(proto::evm::VolumeRateLimit {
max_volume: settings.limit.max_volume.to_be_bytes_vec(),
window_secs: settings.limit.window.num_seconds(),
}),
},
)),
},
SpecificGrant::TokenTransfer(settings) => ProtoSpecificGrant {
grant: Some(ProtoSpecificGrantType::TokenTransfer(
ProtoTokenTransferSettings {
token_contract: settings.token_contract.as_slice().to_vec(),
target: settings.target.map(|addr| addr.as_slice().to_vec()),
volume_limits: settings
.volume_limits
.into_iter()
.map(|vrl| proto::evm::VolumeRateLimit {
max_volume: vrl.max_volume.to_be_bytes_vec(),
window_secs: vrl.window.num_seconds(),
})
.collect(),
},
)),
},
}),
client_id: grant.shared.client_id,
shared: Some(proto::evm::SharedSettings {
wallet_id: grant.shared.wallet_id,
chain_id: grant.shared.chain,
valid_from: grant.shared.valid_from.map(|dt| Timestamp {
seconds: dt.timestamp(),
nanos: 0,
}),
valid_until: grant.shared.valid_until.map(|dt| Timestamp {
seconds: dt.timestamp(),
nanos: 0,
}),
max_gas_fee_per_gas: grant
.shared
.max_gas_fee_per_gas
.map(|fee| fee.to_be_bytes_vec()),
max_priority_fee_per_gas: grant
.shared
.max_priority_fee_per_gas
.map(|fee| fee.to_be_bytes_vec()),
rate_limit: grant
.shared
.rate_limit
.map(|limit| proto::evm::TransactionRateLimit {
count: limit.count,
window_secs: limit.window.num_seconds(),
}),
}),
}
}
fn parse_volume_rate_limit(vrl: ProtoVolumeRateLimit) -> Result<VolumeRateLimit, Status> {
Ok(VolumeRateLimit {
max_volume: U256::from_be_slice(&vrl.max_volume),
window: chrono::Duration::seconds(vrl.window_secs),
})
}
fn parse_shared_settings(
client_id: i32,
proto: Option<ProtoSharedSettings>,
) -> Result<SharedGrantSettings, Status> {
let s = proto.ok_or_else(|| Status::invalid_argument("missing shared settings"))?;
let parse_u256 = |b: Vec<u8>| -> Result<U256, Status> {
if b.is_empty() {
Err(Status::invalid_argument("U256 bytes must not be empty"))
} else {
Ok(U256::from_be_slice(&b))
}
};
let parse_ts = |ts: prost_types::Timestamp| -> Result<DateTime<Utc>, Status> {
Utc.timestamp_opt(ts.seconds, ts.nanos as u32)
.single()
.ok_or_else(|| Status::invalid_argument("invalid timestamp"))
};
Ok(SharedGrantSettings {
wallet_id: s.wallet_id,
client_id,
chain: s.chain_id,
valid_from: s.valid_from.map(parse_ts).transpose()?,
valid_until: s.valid_until.map(parse_ts).transpose()?,
max_gas_fee_per_gas: s.max_gas_fee_per_gas.map(parse_u256).transpose()?,
max_priority_fee_per_gas: s.max_priority_fee_per_gas.map(parse_u256).transpose()?,
rate_limit: s.rate_limit.map(|rl| TransactionRateLimit {
count: rl.count,
window: chrono::Duration::seconds(rl.window_secs),
}),
})
}
fn parse_specific_grant(proto: Option<proto::evm::SpecificGrant>) -> Result<SpecificGrant, Status> {
use proto::evm::specific_grant::Grant as ProtoGrant;
let g = proto
.and_then(|sg| sg.grant)
.ok_or_else(|| Status::invalid_argument("missing specific grant"))?;
match g {
ProtoGrant::EtherTransfer(s) => {
let limit = parse_volume_rate_limit(
s.limit
.ok_or_else(|| Status::invalid_argument("missing ether transfer limit"))?,
)?;
let target = s
.targets
.into_iter()
.map(|b| {
if b.len() == 20 {
Ok(Address::from_slice(&b))
} else {
Err(Status::invalid_argument(
"ether transfer target must be 20 bytes",
))
}
})
.collect::<Result<Vec<_>, _>>()?;
Ok(SpecificGrant::EtherTransfer(ether_transfer::Settings {
target,
limit,
}))
}
ProtoGrant::TokenTransfer(s) => {
if s.token_contract.len() != 20 {
return Err(Status::invalid_argument("token_contract must be 20 bytes"));
}
let target = s
.target
.map(|b| {
if b.len() == 20 {
Ok(Address::from_slice(&b))
} else {
Err(Status::invalid_argument(
"token transfer target must be 20 bytes",
))
}
})
.transpose()?;
let volume_limits = s
.volume_limits
.into_iter()
.map(parse_volume_rate_limit)
.collect::<Result<Vec<_>, _>>()?;
Ok(SpecificGrant::TokenTransfer(token_transfers::Settings {
token_contract: Address::from_slice(&s.token_contract),
target,
volume_limits,
}))
}
}
}
fn parse_auth_pubkey(key_type: i32, pubkey: Vec<u8>) -> Result<AuthPublicKey, Status> {
match ProtoKeyType::try_from(key_type).unwrap_or(ProtoKeyType::Unspecified) {
ProtoKeyType::Unspecified | ProtoKeyType::Ed25519 => {
let bytes: [u8; 32] = pubkey
.as_slice()
.try_into()
.map_err(|_| Status::invalid_argument("invalid Ed25519 public key length"))?;
let key = ed25519_dalek::VerifyingKey::from_bytes(&bytes)
.map_err(|_| Status::invalid_argument("invalid Ed25519 public key encoding"))?;
Ok(AuthPublicKey::Ed25519(key))
}
ProtoKeyType::EcdsaSecp256k1 => {
let key = k256::ecdsa::VerifyingKey::from_sec1_bytes(&pubkey)
.map_err(|_| Status::invalid_argument("invalid secp256k1 public key encoding"))?;
Ok(AuthPublicKey::EcdsaSecp256k1(key))
}
ProtoKeyType::Rsa => {
use rsa::pkcs8::DecodePublicKey as _;
let key = rsa::RsaPublicKey::from_public_key_der(&pubkey)
.map_err(|_| Status::invalid_argument("invalid RSA public key encoding"))?;
Ok(AuthPublicKey::Rsa(key))
}
}
}
fn auth_error_status(value: &user_agent::auth::Error) -> Status {
use user_agent::auth::Error;
match value {
Error::UnexpectedMessagePayload | Error::InvalidClientPubkeyLength => {
Status::invalid_argument(value.to_string())
}
Error::InvalidAuthPubkeyEncoding => {
Status::invalid_argument("Failed to convert pubkey to VerifyingKey")
}
Error::PublicKeyNotRegistered | Error::InvalidChallengeSolution => {
Status::unauthenticated(value.to_string())
}
Error::InvalidBootstrapToken => Status::invalid_argument("Invalid bootstrap token"),
Error::Transport => Status::internal("Transport error"),
Error::BootstrapperActorUnreachable => {
Status::internal("Bootstrap token consumption failed")
}
Error::DatabasePoolUnavailable => Status::internal("Database pool error"),
Error::DatabaseOperationFailed => Status::internal("Database error"),
}
}

View File

@@ -1,135 +1,20 @@
#![forbid(unsafe_code)]
use arbiter_proto::{
proto::{
client::{ClientRequest, ClientResponse},
user_agent::{UserAgentRequest, UserAgentResponse},
},
transport::{IdentityRecvConverter, SendConverter, grpc},
};
use async_trait::async_trait;
use tokio_stream::wrappers::ReceiverStream;
#![deny(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic
)]
use tokio::sync::mpsc;
use tonic::{Request, Response, Status};
use tracing::info;
use crate::{
actors::{
client::{self, ClientError, ClientConnection as ClientConnectionProps, connect_client},
user_agent::{self, UserAgentConnection, UserAgentError, connect_user_agent},
},
context::ServerContext,
};
use crate::context::ServerContext;
pub mod actors;
pub mod context;
pub mod db;
pub mod evm;
pub mod grpc;
const DEFAULT_CHANNEL_SIZE: usize = 1000;
struct UserAgentGrpcSender;
impl SendConverter for UserAgentGrpcSender {
type Input = Result<UserAgentResponse, UserAgentError>;
type Output = Result<UserAgentResponse, Status>;
fn convert(&self, item: Self::Input) -> Self::Output {
match item {
Ok(message) => Ok(message),
Err(err) => Err(user_agent_error_status(err)),
}
}
}
struct ClientGrpcSender;
impl SendConverter for ClientGrpcSender {
type Input = Result<ClientResponse, ClientError>;
type Output = Result<ClientResponse, Status>;
fn convert(&self, item: Self::Input) -> Self::Output {
match item {
Ok(message) => Ok(message),
Err(err) => Err(client_error_status(err)),
}
}
}
fn client_error_status(value: ClientError) -> Status {
match value {
ClientError::MissingRequestPayload | ClientError::UnexpectedRequestPayload => {
Status::invalid_argument("Expected message with payload")
}
ClientError::StateTransitionFailed => Status::internal("State machine error"),
ClientError::Auth(ref err) => client_auth_error_status(err),
ClientError::ConnectionRegistrationFailed => {
Status::internal("Connection registration failed")
}
}
}
fn client_auth_error_status(value: &client::auth::Error) -> Status {
use client::auth::Error;
match value {
Error::UnexpectedMessagePayload | Error::InvalidClientPubkeyLength => {
Status::invalid_argument(value.to_string())
}
Error::InvalidAuthPubkeyEncoding => {
Status::invalid_argument("Failed to convert pubkey to VerifyingKey")
}
Error::InvalidSignatureLength => Status::invalid_argument("Invalid signature length"),
Error::PublicKeyNotRegistered | Error::InvalidChallengeSolution => {
Status::unauthenticated(value.to_string())
}
Error::Transport => Status::internal("Transport error"),
Error::DatabasePoolUnavailable => Status::internal("Database pool error"),
Error::DatabaseOperationFailed => Status::internal("Database error"),
}
}
fn user_agent_error_status(value: UserAgentError) -> Status {
match value {
UserAgentError::MissingRequestPayload | UserAgentError::UnexpectedRequestPayload => {
Status::invalid_argument("Expected message with payload")
}
UserAgentError::InvalidStateForUnsealEncryptedKey => {
Status::failed_precondition("Invalid state for unseal encrypted key")
}
UserAgentError::InvalidClientPubkeyLength => {
Status::invalid_argument("client_pubkey must be 32 bytes")
}
UserAgentError::StateTransitionFailed => Status::internal("State machine error"),
UserAgentError::KeyHolderActorUnreachable => Status::internal("Vault is not available"),
UserAgentError::Auth(ref err) => auth_error_status(err),
UserAgentError::ConnectionRegistrationFailed => {
Status::internal("Failed registering connection")
}
}
}
fn auth_error_status(value: &user_agent::auth::Error) -> Status {
use user_agent::auth::Error;
match value {
Error::UnexpectedMessagePayload | Error::InvalidClientPubkeyLength => {
Status::invalid_argument(value.to_string())
}
Error::InvalidAuthPubkeyEncoding => {
Status::invalid_argument("Failed to convert pubkey to VerifyingKey")
}
Error::PublicKeyNotRegistered | Error::InvalidChallengeSolution => {
Status::unauthenticated(value.to_string())
}
Error::InvalidBootstrapToken => Status::invalid_argument("Invalid bootstrap token"),
Error::Transport => Status::internal("Transport error"),
Error::BootstrapperActorUnreachable => {
Status::internal("Bootstrap token consumption failed")
}
Error::DatabasePoolUnavailable => Status::internal("Database pool error"),
Error::DatabaseOperationFailed => Status::internal("Database error"),
}
}
pub struct Server {
context: ServerContext,
}
@@ -140,60 +25,4 @@ impl Server {
}
}
#[async_trait]
impl arbiter_proto::proto::arbiter_service_server::ArbiterService for Server {
type UserAgentStream = ReceiverStream<Result<UserAgentResponse, Status>>;
type ClientStream = ReceiverStream<Result<ClientResponse, Status>>;
#[tracing::instrument(level = "debug", skip(self))]
async fn client(
&self,
request: Request<tonic::Streaming<ClientRequest>>,
) -> Result<Response<Self::ClientStream>, Status> {
let req_stream = request.into_inner();
let (tx, rx) = mpsc::channel(DEFAULT_CHANNEL_SIZE);
let transport = grpc::GrpcAdapter::new(
tx,
req_stream,
IdentityRecvConverter::<ClientRequest>::new(),
ClientGrpcSender,
);
let props = ClientConnectionProps::new(
self.context.db.clone(),
Box::new(transport),
self.context.actors.clone(),
);
tokio::spawn(connect_client(props));
info!(event = "connection established", "grpc.client");
Ok(Response::new(ReceiverStream::new(rx)))
}
#[tracing::instrument(level = "debug", skip(self))]
async fn user_agent(
&self,
request: Request<tonic::Streaming<UserAgentRequest>>,
) -> Result<Response<Self::UserAgentStream>, Status> {
let req_stream = request.into_inner();
let (tx, rx) = mpsc::channel(DEFAULT_CHANNEL_SIZE);
let transport = grpc::GrpcAdapter::new(
tx,
req_stream,
IdentityRecvConverter::<UserAgentRequest>::new(),
UserAgentGrpcSender,
);
let props = UserAgentConnection::new(
self.context.db.clone(),
self.context.actors.clone(),
Box::new(transport),
);
tokio::spawn(connect_user_agent(props));
info!(event = "connection established", "grpc.user_agent");
Ok(Response::new(ReceiverStream::new(rx)))
}
}

View File

@@ -3,6 +3,7 @@ use std::net::SocketAddr;
use arbiter_proto::{proto::arbiter_service_server::ArbiterServiceServer, url::ArbiterUrl};
use arbiter_server::{Server, actors::bootstrap::GetToken, context::ServerContext, db};
use miette::miette;
use rustls::crypto::aws_lc_rs;
use tonic::transport::{Identity, ServerTlsConfig};
use tracing::info;
@@ -10,6 +11,8 @@ const PORT: u16 = 50051;
#[tokio::main]
async fn main() -> miette::Result<()> {
aws_lc_rs::default_provider().install_default().unwrap();
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()

View File

@@ -1,12 +1,7 @@
use arbiter_proto::proto::client::{
AuthChallengeRequest, AuthChallengeSolution, ClientRequest,
client_request::Payload as ClientRequestPayload,
client_response::Payload as ClientResponsePayload,
};
use arbiter_proto::transport::Bi;
use arbiter_server::actors::GlobalActors;
use arbiter_server::{
actors::client::{ClientConnection, connect_client},
actors::client::{ClientConnection, Request, Response, connect_client},
db::{self, schema},
};
use diesel::{ExpressionMethods as _, insert_into};
@@ -29,12 +24,8 @@ pub async fn test_unregistered_pubkey_rejected() {
let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec();
test_transport
.send(ClientRequest {
payload: Some(ClientRequestPayload::AuthChallengeRequest(
AuthChallengeRequest {
pubkey: pubkey_bytes,
},
)),
.send(Request::AuthChallengeRequest {
pubkey: pubkey_bytes,
})
.await
.unwrap();
@@ -68,12 +59,8 @@ pub async fn test_challenge_auth() {
// Send challenge request
test_transport
.send(ClientRequest {
payload: Some(ClientRequestPayload::AuthChallengeRequest(
AuthChallengeRequest {
pubkey: pubkey_bytes,
},
)),
.send(Request::AuthChallengeRequest {
pubkey: pubkey_bytes,
})
.await
.unwrap();
@@ -84,24 +71,20 @@ pub async fn test_challenge_auth() {
.await
.expect("should receive challenge");
let challenge = match response {
Ok(resp) => match resp.payload {
Some(ClientResponsePayload::AuthChallenge(c)) => c,
Ok(resp) => match resp {
Response::AuthChallenge { pubkey, nonce } => (pubkey, nonce),
other => panic!("Expected AuthChallenge, got {other:?}"),
},
Err(err) => panic!("Expected Ok response, got Err({err:?})"),
};
// Sign the challenge and send solution
let formatted_challenge = arbiter_proto::format_challenge(challenge.nonce, &challenge.pubkey);
let formatted_challenge = arbiter_proto::format_challenge(challenge.1, &challenge.0);
let signature = new_key.sign(&formatted_challenge);
test_transport
.send(ClientRequest {
payload: Some(ClientRequestPayload::AuthChallengeSolution(
AuthChallengeSolution {
signature: signature.to_bytes().to_vec(),
},
)),
.send(Request::AuthChallengeSolution {
signature: signature.to_bytes().to_vec(),
})
.await
.unwrap();

View File

@@ -9,7 +9,6 @@ use diesel_async::RunQueryDsl;
use memsafe::MemSafe;
use tokio::sync::mpsc;
#[allow(dead_code)]
pub async fn bootstrapped_keyholder(db: &db::DatabasePool) -> KeyHolder {
let mut actor = KeyHolder::new(db.clone()).await.unwrap();
@@ -31,13 +30,14 @@ pub async fn root_key_history_id(db: &db::DatabasePool) -> i32 {
id.expect("root_key_id should be set after bootstrap")
}
#[allow(dead_code)]
pub struct ChannelTransport<T, Y> {
receiver: mpsc::Receiver<T>,
sender: mpsc::Sender<Y>,
}
impl<T, Y> ChannelTransport<T, Y> {
#[allow(dead_code)]
pub fn new() -> (Self, ChannelTransport<Y, T>) {
let (tx1, rx1) = mpsc::channel(10);
let (tx2, rx2) = mpsc::channel(10);
@@ -54,8 +54,6 @@ impl<T, Y> ChannelTransport<T, Y> {
}
}
#[async_trait]
impl<T, Y> Bi<T, Y> for ChannelTransport<T, Y>
where

View File

@@ -1,14 +1,9 @@
use arbiter_proto::proto::user_agent::{
AuthChallengeRequest, AuthChallengeSolution, UserAgentRequest,
user_agent_request::Payload as UserAgentRequestPayload,
user_agent_response::Payload as UserAgentResponsePayload,
};
use arbiter_proto::transport::Bi;
use arbiter_server::{
actors::{
GlobalActors,
bootstrap::GetToken,
user_agent::{UserAgentConnection, connect_user_agent},
user_agent::{AuthPublicKey, Request, Response, UserAgentConnection, connect_user_agent},
},
db::{self, schema},
};
@@ -30,16 +25,10 @@ pub async fn test_bootstrap_token_auth() {
let task = tokio::spawn(connect_user_agent(props));
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec();
test_transport
.send(UserAgentRequest {
payload: Some(UserAgentRequestPayload::AuthChallengeRequest(
AuthChallengeRequest {
pubkey: pubkey_bytes,
bootstrap_token: Some(token),
},
)),
.send(Request::AuthChallengeRequest {
pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()),
bootstrap_token: Some(token),
})
.await
.unwrap();
@@ -66,16 +55,10 @@ pub async fn test_bootstrap_invalid_token_auth() {
let task = tokio::spawn(connect_user_agent(props));
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec();
test_transport
.send(UserAgentRequest {
payload: Some(UserAgentRequestPayload::AuthChallengeRequest(
AuthChallengeRequest {
pubkey: pubkey_bytes,
bootstrap_token: Some("invalid_token".to_string()),
},
)),
.send(Request::AuthChallengeRequest {
pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()),
bootstrap_token: Some("invalid_token".to_string()),
})
.await
.unwrap();
@@ -102,10 +85,14 @@ pub async fn test_challenge_auth() {
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec();
// Pre-register key with key_type
{
let mut conn = db.get().await.unwrap();
insert_into(schema::useragent_client::table)
.values(schema::useragent_client::public_key.eq(pubkey_bytes.clone()))
.values((
schema::useragent_client::public_key.eq(pubkey_bytes.clone()),
schema::useragent_client::key_type.eq(1i32),
))
.execute(&mut conn)
.await
.unwrap();
@@ -117,13 +104,9 @@ pub async fn test_challenge_auth() {
// Send challenge request
test_transport
.send(UserAgentRequest {
payload: Some(UserAgentRequestPayload::AuthChallengeRequest(
AuthChallengeRequest {
pubkey: pubkey_bytes,
bootstrap_token: None,
},
)),
.send(Request::AuthChallengeRequest {
pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()),
bootstrap_token: None,
})
.await
.unwrap();
@@ -134,24 +117,19 @@ pub async fn test_challenge_auth() {
.await
.expect("should receive challenge");
let challenge = match response {
Ok(resp) => match resp.payload {
Some(UserAgentResponsePayload::AuthChallenge(c)) => c,
Ok(resp) => match resp {
Response::AuthChallenge { nonce } => nonce,
other => panic!("Expected AuthChallenge, got {other:?}"),
},
Err(err) => panic!("Expected Ok response, got Err({err:?})"),
};
// Sign the challenge and send solution
let formatted_challenge = arbiter_proto::format_challenge(challenge.nonce, &challenge.pubkey);
let formatted_challenge = arbiter_proto::format_challenge(challenge, &pubkey_bytes);
let signature = new_key.sign(&formatted_challenge);
test_transport
.send(UserAgentRequest {
payload: Some(UserAgentRequestPayload::AuthChallengeSolution(
AuthChallengeSolution {
signature: signature.to_bytes().to_vec(),
},
)),
.send(Request::AuthChallengeSolution {
signature: signature.to_bytes().to_vec(),
})
.await
.unwrap();

View File

@@ -1,13 +1,8 @@
use arbiter_proto::proto::user_agent::{
UnsealEncryptedKey, UnsealResult, UnsealStart, UserAgentRequest,
user_agent_request::Payload as UserAgentRequestPayload,
user_agent_response::Payload as UserAgentResponsePayload,
};
use arbiter_server::{
actors::{
GlobalActors,
keyholder::{Bootstrap, Seal},
user_agent::session::UserAgentSession,
user_agent::{Request, Response, UnsealError, session::UserAgentSession},
},
db,
};
@@ -15,9 +10,7 @@ use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit};
use memsafe::MemSafe;
use x25519_dalek::{EphemeralSecret, PublicKey};
async fn setup_sealed_user_agent(
seal_key: &[u8],
) -> (db::DatabasePool, UserAgentSession) {
async fn setup_sealed_user_agent(seal_key: &[u8]) -> (db::DatabasePool, UserAgentSession) {
let db = db::create_test_pool().await;
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
@@ -35,29 +28,23 @@ async fn setup_sealed_user_agent(
(db, session)
}
async fn client_dh_encrypt(
user_agent: &mut UserAgentSession,
key_to_send: &[u8],
) -> UnsealEncryptedKey {
async fn client_dh_encrypt(user_agent: &mut UserAgentSession, key_to_send: &[u8]) -> Request {
let client_secret = EphemeralSecret::random();
let client_public = PublicKey::from(&client_secret);
let response = user_agent
.process_transport_inbound(UserAgentRequest {
payload: Some(UserAgentRequestPayload::UnsealStart(UnsealStart {
client_pubkey: client_public.as_bytes().to_vec(),
})),
.process_transport_inbound(Request::UnsealStart {
client_pubkey: client_public,
})
.await
.unwrap();
let server_pubkey = match response.payload.unwrap() {
UserAgentResponsePayload::UnsealStartResponse(resp) => resp.server_pubkey,
let server_pubkey = match response {
Response::UnsealStartResponse { server_pubkey } => server_pubkey,
other => panic!("Expected UnsealStartResponse, got {other:?}"),
};
let server_public = PublicKey::from(<[u8; 32]>::try_from(server_pubkey.as_slice()).unwrap());
let shared_secret = client_secret.diffie_hellman(&server_public);
let shared_secret = client_secret.diffie_hellman(&server_pubkey);
let cipher = XChaCha20Poly1305::new(shared_secret.as_bytes().into());
let nonce = XNonce::from([0u8; 24]);
let associated_data = b"unseal";
@@ -66,19 +53,13 @@ async fn client_dh_encrypt(
.encrypt_in_place(&nonce, associated_data, &mut ciphertext)
.unwrap();
UnsealEncryptedKey {
Request::UnsealEncryptedKey {
nonce: nonce.to_vec(),
ciphertext,
associated_data: associated_data.to_vec(),
}
}
fn unseal_key_request(req: UnsealEncryptedKey) -> UserAgentRequest {
UserAgentRequest {
payload: Some(UserAgentRequestPayload::UnsealEncryptedKey(req)),
}
}
#[tokio::test]
#[test_log::test]
pub async fn test_unseal_success() {
@@ -88,14 +69,11 @@ pub async fn test_unseal_success() {
let encrypted_key = client_dh_encrypt(&mut user_agent, seal_key).await;
let response = user_agent
.process_transport_inbound(unseal_key_request(encrypted_key))
.process_transport_inbound(encrypted_key)
.await
.unwrap();
assert_eq!(
response.payload.unwrap(),
UserAgentResponsePayload::UnsealResult(UnsealResult::Success.into()),
);
assert!(matches!(response, Response::UnsealResult(Ok(()))));
}
#[tokio::test]
@@ -106,14 +84,14 @@ pub async fn test_unseal_wrong_seal_key() {
let encrypted_key = client_dh_encrypt(&mut user_agent, b"wrong-key").await;
let response = user_agent
.process_transport_inbound(unseal_key_request(encrypted_key))
.process_transport_inbound(encrypted_key)
.await
.unwrap();
assert_eq!(
response.payload.unwrap(),
UserAgentResponsePayload::UnsealResult(UnsealResult::InvalidKey.into()),
);
assert!(matches!(
response,
Response::UnsealResult(Err(UnsealError::InvalidKey))
));
}
#[tokio::test]
@@ -125,27 +103,25 @@ pub async fn test_unseal_corrupted_ciphertext() {
let client_public = PublicKey::from(&client_secret);
user_agent
.process_transport_inbound(UserAgentRequest {
payload: Some(UserAgentRequestPayload::UnsealStart(UnsealStart {
client_pubkey: client_public.as_bytes().to_vec(),
})),
.process_transport_inbound(Request::UnsealStart {
client_pubkey: client_public,
})
.await
.unwrap();
let response = user_agent
.process_transport_inbound(unseal_key_request(UnsealEncryptedKey {
.process_transport_inbound(Request::UnsealEncryptedKey {
nonce: vec![0u8; 24],
ciphertext: vec![0u8; 32],
associated_data: vec![],
}))
})
.await
.unwrap();
assert_eq!(
response.payload.unwrap(),
UserAgentResponsePayload::UnsealResult(UnsealResult::InvalidKey.into()),
);
assert!(matches!(
response,
Response::UnsealResult(Err(UnsealError::InvalidKey))
));
}
#[tokio::test]
@@ -158,27 +134,24 @@ pub async fn test_unseal_retry_after_invalid_key() {
let encrypted_key = client_dh_encrypt(&mut user_agent, b"wrong-key").await;
let response = user_agent
.process_transport_inbound(unseal_key_request(encrypted_key))
.process_transport_inbound(encrypted_key)
.await
.unwrap();
assert_eq!(
response.payload.unwrap(),
UserAgentResponsePayload::UnsealResult(UnsealResult::InvalidKey.into()),
);
assert!(matches!(
response,
Response::UnsealResult(Err(UnsealError::InvalidKey))
));
}
{
let encrypted_key = client_dh_encrypt(&mut user_agent, seal_key).await;
let response = user_agent
.process_transport_inbound(unseal_key_request(encrypted_key))
.process_transport_inbound(encrypted_key)
.await
.unwrap();
assert_eq!(
response.payload.unwrap(),
UserAgentResponsePayload::UnsealResult(UnsealResult::Success.into()),
);
assert!(matches!(response, Response::UnsealResult(Ok(()))));
}
}

View File

@@ -1,21 +0,0 @@
[package]
name = "arbiter-useragent"
version = "0.1.0"
edition = "2024"
license = "Apache-2.0"
[dependencies]
arbiter-proto.path = "../arbiter-proto"
kameo.workspace = true
tokio = {workspace = true, features = ["net"]}
tonic.workspace = true
tonic.features = ["tls-aws-lc"]
tracing.workspace = true
ed25519-dalek.workspace = true
smlang.workspace = true
x25519-dalek.workspace = true
thiserror.workspace = true
tokio-stream.workspace = true
http = "1.4.0"
rustls-webpki = { version = "0.103.9", features = ["aws-lc-rs"] }
async-trait.workspace = true

View File

@@ -1,72 +0,0 @@
use arbiter_proto::{
proto::{
user_agent::{UserAgentRequest, UserAgentResponse},
arbiter_service_client::ArbiterServiceClient,
},
transport::{IdentityRecvConverter, IdentitySendConverter, grpc},
url::ArbiterUrl,
};
use ed25519_dalek::SigningKey;
use kameo::actor::{ActorRef, Spawn};
use tokio::sync::mpsc;
use tokio_stream::wrappers::ReceiverStream;
use tonic::transport::ClientTlsConfig;
#[derive(Debug, thiserror::Error)]
pub enum ConnectError {
#[error("Could establish connection")]
Connection(#[from] tonic::transport::Error),
#[error("Invalid server URI")]
InvalidUri(#[from] http::uri::InvalidUri),
#[error("Invalid CA certificate")]
InvalidCaCert(#[from] webpki::Error),
#[error("gRPC error")]
Grpc(#[from] tonic::Status),
}
use super::UserAgentActor;
pub type UserAgentGrpc = ActorRef<
UserAgentActor<
grpc::GrpcAdapter<
IdentityRecvConverter<UserAgentResponse>,
IdentitySendConverter<UserAgentRequest>,
>,
>,
>;
pub async fn connect_grpc(
url: ArbiterUrl,
key: SigningKey,
) -> Result<UserAgentGrpc, ConnectError> {
let bootstrap_token = url.bootstrap_token.clone();
let anchor = webpki::anchor_from_trusted_cert(&url.ca_cert)?.to_owned();
let tls = ClientTlsConfig::new().trust_anchor(anchor);
// TODO: if `host` is localhost, we need to verify server's process authenticity
let channel = tonic::transport::Channel::from_shared(format!("{}:{}", url.host, url.port))?
.tls_config(tls)?
.connect()
.await?;
let mut client = ArbiterServiceClient::new(channel);
let (tx, rx) = mpsc::channel(16);
let bistream = client.user_agent(ReceiverStream::new(rx)).await?;
let bistream = bistream.into_inner();
let adapter = grpc::GrpcAdapter::new(
tx,
bistream,
IdentityRecvConverter::new(),
IdentitySendConverter::new(),
);
let actor = UserAgentActor::spawn(UserAgentActor::new(key, bootstrap_token, adapter));
Ok(actor)
}

View File

@@ -1,195 +0,0 @@
use arbiter_proto::{
format_challenge,
proto::user_agent::{
AuthChallengeRequest, AuthChallengeSolution, AuthOk,
UserAgentRequest, UserAgentResponse,
user_agent_request::Payload as UserAgentRequestPayload,
user_agent_response::Payload as UserAgentResponsePayload,
},
transport::Bi,
};
use ed25519_dalek::{Signer, SigningKey};
use kameo::{Actor, actor::ActorRef};
use smlang::statemachine;
use tokio::select;
use tracing::{error, info};
statemachine! {
name: UserAgent,
custom_error: false,
transitions: {
*Init + SentAuthChallengeRequest = WaitingForServerAuth,
WaitingForServerAuth + ReceivedAuthChallenge = WaitingForAuthOk,
WaitingForServerAuth + ReceivedAuthOk = Authenticated,
WaitingForAuthOk + ReceivedAuthOk = Authenticated,
}
}
pub struct DummyContext;
impl UserAgentStateMachineContext for DummyContext {}
#[derive(Debug, thiserror::Error)]
pub enum InboundError {
#[error("Invalid user agent response")]
InvalidResponse,
#[error("Expected response payload")]
MissingResponsePayload,
#[error("Unexpected response payload")]
UnexpectedResponsePayload,
#[error("Invalid state for auth challenge")]
InvalidStateForAuthChallenge,
#[error("Invalid state for auth ok")]
InvalidStateForAuthOk,
#[error("State machine error")]
StateTransitionFailed,
#[error("Transport send failed")]
TransportSendFailed,
}
pub struct UserAgentActor<Transport>
where
Transport: Bi<UserAgentResponse, UserAgentRequest>,
{
key: SigningKey,
bootstrap_token: Option<String>,
state: UserAgentStateMachine<DummyContext>,
transport: Transport,
}
impl<Transport> UserAgentActor<Transport>
where
Transport: Bi<UserAgentResponse, UserAgentRequest>,
{
pub fn new(key: SigningKey, bootstrap_token: Option<String>, transport: Transport) -> Self {
Self {
key,
bootstrap_token,
state: UserAgentStateMachine::new(DummyContext),
transport,
}
}
fn transition(&mut self, event: UserAgentEvents) -> Result<(), InboundError> {
self.state.process_event(event).map_err(|e| {
error!(?e, "useragent state transition failed");
InboundError::StateTransitionFailed
})?;
Ok(())
}
async fn send_auth_challenge_request(&mut self) -> Result<(), InboundError> {
let req = AuthChallengeRequest {
pubkey: self.key.verifying_key().to_bytes().to_vec(),
bootstrap_token: self.bootstrap_token.take(),
};
self.transition(UserAgentEvents::SentAuthChallengeRequest)?;
self.transport
.send(UserAgentRequest {
payload: Some(UserAgentRequestPayload::AuthChallengeRequest(req)),
})
.await
.map_err(|_| InboundError::TransportSendFailed)?;
info!(actor = "useragent", "auth.request.sent");
Ok(())
}
async fn handle_auth_challenge(
&mut self,
challenge: arbiter_proto::proto::user_agent::AuthChallenge,
) -> Result<(), InboundError> {
self.transition(UserAgentEvents::ReceivedAuthChallenge)?;
let formatted = format_challenge(challenge.nonce, &challenge.pubkey);
let signature = self.key.sign(&formatted);
let solution = AuthChallengeSolution {
signature: signature.to_bytes().to_vec(),
};
self.transport
.send(UserAgentRequest {
payload: Some(UserAgentRequestPayload::AuthChallengeSolution(solution)),
})
.await
.map_err(|_| InboundError::TransportSendFailed)?;
info!(actor = "useragent", "auth.solution.sent");
Ok(())
}
fn handle_auth_ok(&mut self, _ok: AuthOk) -> Result<(), InboundError> {
self.transition(UserAgentEvents::ReceivedAuthOk)?;
info!(actor = "useragent", "auth.ok");
Ok(())
}
pub async fn process_inbound_transport(
&mut self,
inbound: UserAgentResponse
) -> Result<(), InboundError> {
let payload = inbound
.payload
.ok_or(InboundError::MissingResponsePayload)?;
match payload {
UserAgentResponsePayload::AuthChallenge(challenge) => {
self.handle_auth_challenge(challenge).await
}
UserAgentResponsePayload::AuthOk(ok) => self.handle_auth_ok(ok),
_ => Err(InboundError::UnexpectedResponsePayload),
}
}
}
impl<Transport> Actor for UserAgentActor<Transport>
where
Transport: Bi<UserAgentResponse, UserAgentRequest>,
{
type Args = Self;
type Error = ();
async fn on_start(
mut args: Self::Args,
_actor_ref: ActorRef<Self>,
) -> Result<Self, Self::Error> {
if let Err(err) = args.send_auth_challenge_request().await {
error!(?err, actor = "useragent", "auth.start.failed");
return Err(());
}
Ok(args)
}
async fn next(
&mut self,
_actor_ref: kameo::prelude::WeakActorRef<Self>,
mailbox_rx: &mut kameo::prelude::MailboxReceiver<Self>,
) -> Option<kameo::mailbox::Signal<Self>> {
loop {
select! {
signal = mailbox_rx.recv() => {
return signal;
}
inbound = self.transport.recv() => {
match inbound {
Some(inbound) => {
if let Err(err) = self.process_inbound_transport(inbound).await {
error!(?err, actor = "useragent", "transport.inbound.failed");
return Some(kameo::mailbox::Signal::Stop);
}
}
None => {
info!(actor = "useragent", "transport.closed");
return Some(kameo::mailbox::Signal::Stop);
}
}
}
}
}
}
}
mod grpc;
pub use grpc::{connect_grpc, ConnectError};

View File

@@ -1,141 +0,0 @@
use arbiter_proto::{
format_challenge,
proto::user_agent::{
AuthChallenge, AuthOk,
UserAgentRequest, UserAgentResponse,
user_agent_request::Payload as UserAgentRequestPayload,
user_agent_response::Payload as UserAgentResponsePayload,
},
transport::Bi,
};
use arbiter_useragent::UserAgentActor;
use ed25519_dalek::SigningKey;
use kameo::actor::Spawn;
use tokio::sync::mpsc;
use tokio::time::{Duration, timeout};
use async_trait::async_trait;
struct TestTransport {
inbound_rx: mpsc::Receiver<UserAgentResponse>,
outbound_tx: mpsc::Sender<UserAgentRequest>,
}
#[async_trait]
impl Bi<UserAgentResponse, UserAgentRequest> for TestTransport {
async fn send(&mut self, item: UserAgentRequest) -> Result<(), arbiter_proto::transport::Error> {
self.outbound_tx
.send(item)
.await
.map_err(|_| arbiter_proto::transport::Error::ChannelClosed)
}
async fn recv(&mut self) -> Option<UserAgentResponse> {
self.inbound_rx.recv().await
}
}
fn make_transport() -> (
TestTransport,
mpsc::Sender<UserAgentResponse>,
mpsc::Receiver<UserAgentRequest>,
) {
let (inbound_tx, inbound_rx) = mpsc::channel(8);
let (outbound_tx, outbound_rx) = mpsc::channel(8);
(
TestTransport {
inbound_rx,
outbound_tx,
},
inbound_tx,
outbound_rx,
)
}
fn test_key() -> SigningKey {
SigningKey::from_bytes(&[7u8; 32])
}
#[tokio::test]
async fn sends_auth_request_on_start_with_bootstrap_token() {
let key = test_key();
let pubkey = key.verifying_key().to_bytes().to_vec();
let bootstrap_token = Some("bootstrap-123".to_string());
let (transport, inbound_tx, mut outbound_rx) = make_transport();
let actor = UserAgentActor::spawn(UserAgentActor::new(key, bootstrap_token.clone(), transport));
let outbound = timeout(Duration::from_secs(1), outbound_rx.recv())
.await
.expect("timed out waiting for auth request")
.expect("channel closed before auth request");
let UserAgentRequest {
payload: Some(UserAgentRequestPayload::AuthChallengeRequest(req)),
} = outbound
else {
panic!("expected auth challenge request");
};
assert_eq!(req.pubkey, pubkey);
assert_eq!(req.bootstrap_token, bootstrap_token);
drop(inbound_tx);
drop(actor);
}
#[tokio::test]
async fn challenge_flow_sends_solution_from_transport_inbound() {
let key = test_key();
let verify_key = key.verifying_key();
let (transport, inbound_tx, mut outbound_rx) = make_transport();
let actor = UserAgentActor::spawn(UserAgentActor::new(key, None, transport));
let _initial_auth_request = timeout(Duration::from_secs(1), outbound_rx.recv())
.await
.expect("timed out waiting for initial auth request")
.expect("missing initial auth request");
let challenge = AuthChallenge {
pubkey: verify_key.to_bytes().to_vec(),
nonce: 42,
};
inbound_tx
.send(UserAgentResponse {
payload: Some(UserAgentResponsePayload::AuthChallenge(challenge.clone())),
})
.await
.unwrap();
let outbound = timeout(Duration::from_secs(1), outbound_rx.recv())
.await
.expect("timed out waiting for challenge solution")
.expect("missing challenge solution");
let UserAgentRequest {
payload: Some(UserAgentRequestPayload::AuthChallengeSolution(solution)),
} = outbound
else {
panic!("expected auth challenge solution");
};
let formatted = format_challenge(challenge.nonce, &challenge.pubkey);
let sig: ed25519_dalek::Signature = solution
.signature
.as_slice()
.try_into()
.expect("signature bytes length");
verify_key
.verify_strict(&formatted, &sig)
.expect("solution signature should verify");
inbound_tx
.send(UserAgentResponse {
payload: Some(UserAgentResponsePayload::AuthOk(AuthOk {})),
})
.await
.unwrap();
drop(inbound_tx);
drop(actor);
}

View File

@@ -1,6 +1,41 @@
# cargo-vet audits file
[[audits.alloy-primitives]]
who = "CleverWild <cleverwilddev@gmail.com>"
criteria = "safe-to-deploy"
version = "1.5.7"
[[audits.console]]
who = "CleverWild <cleverwilddev@gmail.com>"
criteria = "safe-to-deploy"
version = "0.15.11"
[[audits.encode_unicode]]
who = "CleverWild <cleverwilddev@gmail.com>"
criteria = "safe-to-deploy"
version = "0.3.6"
[[audits.futures-timer]]
who = "CleverWild <cleverwilddev@gmail.com>"
criteria = "safe-to-run"
version = "3.0.3"
[[audits.insta]]
who = "CleverWild <cleverwilddev@gmail.com>"
criteria = "safe-to-run"
version = "1.46.3"
[[audits.pin-project]]
who = "CleverWild <cleverwilddev@gmail.com>"
criteria = "safe-to-deploy"
version = "0.2.16"
[[audits.protoc-bin-vendored]]
who = "CleverWild <cleverwilddev@gmail.com>"
criteria = "safe-to-deploy"
version = "3.2.0"
[[audits.similar]]
who = "hdbg <httpdebugger@protonmail.com>"
criteria = "safe-to-deploy"
@@ -16,11 +51,214 @@ who = "hdbg <httpdebugger@protonmail.com>"
criteria = "safe-to-deploy"
delta = "0.2.18 -> 0.2.19"
[[audits.wasm-bindgen]]
who = "CleverWild <cleverwilddev@gmail.com>"
criteria = "safe-to-deploy"
delta = "0.2.100 -> 0.2.114"
[[trusted.addr2line]]
criteria = "safe-to-deploy"
user-id = 4415 # Philip Craig (philipc)
start = "2019-05-01"
end = "2027-03-14"
[[trusted.aho-corasick]]
criteria = "safe-to-deploy"
user-id = 189 # Andrew Gallant (BurntSushi)
start = "2019-03-28"
end = "2027-03-14"
[[trusted.anyhow]]
criteria = "safe-to-deploy"
user-id = 3618 # David Tolnay (dtolnay)
start = "2019-10-05"
end = "2027-03-14"
[[trusted.async-stream]]
criteria = "safe-to-deploy"
user-id = 10 # Carl Lerche (carllerche)
start = "2019-06-07"
end = "2027-03-14"
[[trusted.async-stream]]
criteria = "safe-to-deploy"
user-id = 33035 # Taiki Endo (taiki-e)
start = "2021-04-21"
end = "2027-03-14"
[[trusted.async-stream-impl]]
criteria = "safe-to-deploy"
user-id = 10 # Carl Lerche (carllerche)
start = "2019-08-13"
end = "2027-03-14"
[[trusted.async-stream-impl]]
criteria = "safe-to-deploy"
user-id = 33035 # Taiki Endo (taiki-e)
start = "2021-04-21"
end = "2027-03-14"
[[trusted.async-trait]]
criteria = "safe-to-deploy"
user-id = 3618 # David Tolnay (dtolnay)
start = "2019-07-23"
end = "2027-03-14"
[[trusted.auto_impl]]
criteria = "safe-to-deploy"
user-id = 3204 # Ashley Mannix (KodrAus)
start = "2022-06-01"
end = "2027-03-14"
[[trusted.aws-lc-rs]]
criteria = "safe-to-deploy"
user-id = 156764 # Justin W Smith (justsmth)
start = "2023-04-11"
end = "2027-03-14"
[[trusted.aws-lc-sys]]
criteria = "safe-to-deploy"
user-id = 156764 # Justin W Smith (justsmth)
start = "2022-11-09"
end = "2027-03-14"
[[trusted.backtrace]]
criteria = "safe-to-deploy"
user-id = 55123 # rust-lang-owner
start = "2025-05-06"
end = "2027-03-14"
[[trusted.bitflags]]
criteria = "safe-to-deploy"
user-id = 3204 # Ashley Mannix (KodrAus)
start = "2019-05-02"
end = "2027-03-14"
[[trusted.bytes]]
criteria = "safe-to-deploy"
user-id = 359 # Sean McArthur (seanmonstar)
start = "2019-11-27"
end = "2027-03-14"
[[trusted.bytes]]
criteria = "safe-to-deploy"
user-id = 6741 # Alice Ryhl (Darksonn)
start = "2021-01-11"
end = "2027-03-14"
[[trusted.cc]]
criteria = "safe-to-deploy"
user-id = 55123 # rust-lang-owner
start = "2022-10-29"
end = "2027-02-16"
end = "2027-03-14"
[[trusted.cmake]]
criteria = "safe-to-deploy"
user-id = 55123 # rust-lang-owner
start = "2022-10-29"
end = "2027-03-14"
[[trusted.crossbeam-utils]]
criteria = "safe-to-deploy"
user-id = 33035 # Taiki Endo (taiki-e)
start = "2020-10-12"
end = "2027-03-14"
[[trusted.derive_more]]
criteria = "safe-to-deploy"
user-id = 3797 # Jelte Fennema-Nio (JelteF)
start = "2019-05-25"
end = "2027-03-14"
[[trusted.derive_more-impl]]
criteria = "safe-to-deploy"
user-id = 3797 # Jelte Fennema-Nio (JelteF)
start = "2023-07-23"
end = "2027-03-14"
[[trusted.dyn-clone]]
criteria = "safe-to-deploy"
user-id = 3618 # David Tolnay (dtolnay)
start = "2019-12-23"
end = "2027-03-14"
[[trusted.ff]]
criteria = "safe-to-deploy"
user-id = 6289 # Jack Grigg (str4d)
start = "2021-08-11"
end = "2027-03-14"
[[trusted.find-msvc-tools]]
criteria = "safe-to-deploy"
user-id = 539 # Josh Stone (cuviper)
start = "2025-08-29"
end = "2027-03-14"
[[trusted.flate2]]
criteria = "safe-to-deploy"
user-id = 980 # Sebastian Thiel (Byron)
start = "2023-08-15"
end = "2027-03-14"
[[trusted.futures]]
criteria = "safe-to-deploy"
user-id = 33035 # Taiki Endo (taiki-e)
start = "2020-10-05"
end = "2027-03-14"
[[trusted.futures-channel]]
criteria = "safe-to-deploy"
user-id = 33035 # Taiki Endo (taiki-e)
start = "2020-10-05"
end = "2027-03-14"
[[trusted.futures-core]]
criteria = "safe-to-deploy"
user-id = 33035 # Taiki Endo (taiki-e)
start = "2020-10-05"
end = "2027-03-14"
[[trusted.futures-executor]]
criteria = "safe-to-deploy"
user-id = 33035 # Taiki Endo (taiki-e)
start = "2020-10-05"
end = "2027-03-14"
[[trusted.futures-io]]
criteria = "safe-to-deploy"
user-id = 33035 # Taiki Endo (taiki-e)
start = "2020-10-05"
end = "2027-03-14"
[[trusted.futures-macro]]
criteria = "safe-to-deploy"
user-id = 33035 # Taiki Endo (taiki-e)
start = "2020-10-05"
end = "2027-03-14"
[[trusted.futures-sink]]
criteria = "safe-to-deploy"
user-id = 33035 # Taiki Endo (taiki-e)
start = "2020-10-05"
end = "2027-03-14"
[[trusted.futures-task]]
criteria = "safe-to-deploy"
user-id = 33035 # Taiki Endo (taiki-e)
start = "2019-07-29"
end = "2027-03-14"
[[trusted.futures-util]]
criteria = "safe-to-deploy"
user-id = 33035 # Taiki Endo (taiki-e)
start = "2020-10-05"
end = "2027-03-14"
[[trusted.group]]
criteria = "safe-to-deploy"
user-id = 1244 # ebfull
start = "2019-10-08"
end = "2027-03-14"
[[trusted.h2]]
criteria = "safe-to-deploy"
@@ -28,36 +266,372 @@ user-id = 359 # Sean McArthur (seanmonstar)
start = "2019-03-13"
end = "2027-02-14"
[[trusted.hashbrown]]
criteria = "safe-to-deploy"
user-id = 2915 # Amanieu d'Antras (Amanieu)
start = "2019-04-02"
end = "2027-03-14"
[[trusted.hashbrown]]
criteria = "safe-to-deploy"
user-id = 55123 # rust-lang-owner
start = "2025-04-30"
end = "2027-02-14"
[[trusted.http]]
criteria = "safe-to-deploy"
user-id = 359 # Sean McArthur (seanmonstar)
start = "2019-04-05"
end = "2027-03-14"
[[trusted.http-body-util]]
criteria = "safe-to-deploy"
user-id = 359 # Sean McArthur (seanmonstar)
start = "2022-10-25"
end = "2027-03-14"
[[trusted.httparse]]
criteria = "safe-to-deploy"
user-id = 359 # Sean McArthur (seanmonstar)
start = "2019-07-03"
end = "2027-03-14"
[[trusted.hyper]]
criteria = "safe-to-deploy"
user-id = 359 # Sean McArthur (seanmonstar)
start = "2019-03-01"
end = "2027-03-14"
[[trusted.hyper-util]]
criteria = "safe-to-deploy"
user-id = 359 # Sean McArthur (seanmonstar)
start = "2022-01-15"
end = "2027-02-14"
[[trusted.id-arena]]
criteria = "safe-to-deploy"
user-id = 696 # Nick Fitzgerald (fitzgen)
start = "2026-01-14"
end = "2027-03-14"
[[trusted.indexmap]]
criteria = "safe-to-deploy"
user-id = 539 # Josh Stone (cuviper)
start = "2020-01-15"
end = "2027-03-14"
[[trusted.itoa]]
criteria = "safe-to-deploy"
user-id = 3618 # David Tolnay (dtolnay)
start = "2019-05-02"
end = "2027-03-14"
[[trusted.jobserver]]
criteria = "safe-to-deploy"
user-id = 55123 # rust-lang-owner
start = "2024-07-23"
end = "2027-03-14"
[[trusted.js-sys]]
criteria = "safe-to-deploy"
user-id = 1 # Alex Crichton (alexcrichton)
start = "2019-03-04"
end = "2027-03-14"
[[trusted.libc]]
criteria = "safe-to-deploy"
user-id = 55123 # rust-lang-owner
start = "2024-08-15"
end = "2027-02-16"
[[trusted.libm]]
criteria = "safe-to-deploy"
user-id = 55123 # rust-lang-owner
start = "2024-10-26"
end = "2027-03-14"
[[trusted.linux-raw-sys]]
criteria = "safe-to-deploy"
user-id = 6825 # Dan Gohman (sunfishcode)
start = "2021-06-12"
end = "2027-03-14"
[[trusted.lock_api]]
criteria = "safe-to-deploy"
user-id = 2915 # Amanieu d'Antras (Amanieu)
start = "2019-05-04"
end = "2027-03-14"
[[trusted.log]]
criteria = "safe-to-deploy"
user-id = 3204 # Ashley Mannix (KodrAus)
start = "2019-07-10"
end = "2027-03-14"
[[trusted.macro-string]]
criteria = "safe-to-deploy"
user-id = 3618 # David Tolnay (dtolnay)
start = "2025-02-02"
end = "2027-03-14"
[[trusted.memchr]]
criteria = "safe-to-deploy"
user-id = 189 # Andrew Gallant (BurntSushi)
start = "2019-07-07"
end = "2027-03-14"
[[trusted.mime]]
criteria = "safe-to-deploy"
user-id = 359 # Sean McArthur (seanmonstar)
start = "2019-09-09"
end = "2027-03-14"
[[trusted.mio]]
criteria = "safe-to-deploy"
user-id = 6025 # Thomas de Zeeuw (Thomasdezeeuw)
start = "2019-12-17"
end = "2027-03-14"
[[trusted.num-bigint]]
criteria = "safe-to-deploy"
user-id = 539 # Josh Stone (cuviper)
start = "2019-09-04"
end = "2027-03-14"
[[trusted.num_cpus]]
criteria = "safe-to-deploy"
user-id = 359 # Sean McArthur (seanmonstar)
start = "2019-06-10"
end = "2027-03-14"
[[trusted.object]]
criteria = "safe-to-deploy"
user-id = 4415 # Philip Craig (philipc)
start = "2019-04-26"
end = "2027-03-14"
[[trusted.parking_lot]]
criteria = "safe-to-deploy"
user-id = 2915 # Amanieu d'Antras (Amanieu)
start = "2019-05-04"
end = "2027-03-14"
[[trusted.parking_lot_core]]
criteria = "safe-to-deploy"
user-id = 2915 # Amanieu d'Antras (Amanieu)
start = "2019-05-04"
end = "2027-03-14"
[[trusted.paste]]
criteria = "safe-to-deploy"
user-id = 3618 # David Tolnay (dtolnay)
start = "2019-03-19"
end = "2027-03-14"
[[trusted.pin-project]]
criteria = "safe-to-deploy"
user-id = 33035 # Taiki Endo (taiki-e)
start = "2019-03-02"
end = "2027-03-14"
[[trusted.pin-project-internal]]
criteria = "safe-to-deploy"
user-id = 33035 # Taiki Endo (taiki-e)
start = "2019-08-11"
end = "2027-03-14"
[[trusted.pin-project-lite]]
criteria = "safe-to-deploy"
user-id = 33035 # Taiki Endo (taiki-e)
start = "2019-10-22"
end = "2027-03-14"
[[trusted.portable-atomic]]
criteria = "safe-to-deploy"
user-id = 33035 # Taiki Endo (taiki-e)
start = "2022-02-24"
end = "2027-03-14"
[[trusted.prettyplease]]
criteria = "safe-to-deploy"
user-id = 3618 # David Tolnay (dtolnay)
start = "2022-01-04"
end = "2027-03-14"
[[trusted.proc-macro2]]
criteria = "safe-to-deploy"
user-id = 3618 # David Tolnay (dtolnay)
start = "2019-04-23"
end = "2027-03-14"
[[trusted.prost]]
criteria = "safe-to-deploy"
user-id = 3959 # Lucio Franco (LucioFranco)
start = "2021-07-08"
end = "2027-03-14"
[[trusted.prost-build]]
criteria = "safe-to-deploy"
user-id = 3959 # Lucio Franco (LucioFranco)
start = "2021-07-08"
end = "2027-03-14"
[[trusted.prost-derive]]
criteria = "safe-to-deploy"
user-id = 3959 # Lucio Franco (LucioFranco)
start = "2021-07-08"
end = "2027-03-14"
[[trusted.prost-types]]
criteria = "safe-to-deploy"
user-id = 3959 # Lucio Franco (LucioFranco)
start = "2021-07-08"
end = "2027-03-14"
[[trusted.protoc-bin-vendored-linux-aarch_64]]
criteria = "safe-to-deploy"
user-id = 220 # Stepan Koltsov (stepancheg)
start = "2022-02-07"
end = "2027-03-14"
[[trusted.protoc-bin-vendored-linux-ppcle_64]]
criteria = "safe-to-deploy"
user-id = 220 # Stepan Koltsov (stepancheg)
start = "2022-02-07"
end = "2027-03-14"
[[trusted.protoc-bin-vendored-linux-s390_64]]
criteria = "safe-to-deploy"
user-id = 220 # Stepan Koltsov (stepancheg)
start = "2025-07-21"
end = "2027-03-14"
[[trusted.protoc-bin-vendored-linux-x86_32]]
criteria = "safe-to-deploy"
user-id = 220 # Stepan Koltsov (stepancheg)
start = "2022-02-07"
end = "2027-03-14"
[[trusted.protoc-bin-vendored-linux-x86_64]]
criteria = "safe-to-deploy"
user-id = 220 # Stepan Koltsov (stepancheg)
start = "2022-02-07"
end = "2027-03-14"
[[trusted.protoc-bin-vendored-macos-aarch_64]]
criteria = "safe-to-deploy"
user-id = 220 # Stepan Koltsov (stepancheg)
start = "2024-09-30"
end = "2027-03-14"
[[trusted.protoc-bin-vendored-macos-x86_64]]
criteria = "safe-to-deploy"
user-id = 220 # Stepan Koltsov (stepancheg)
start = "2022-02-07"
end = "2027-03-14"
[[trusted.protoc-bin-vendored-win32]]
criteria = "safe-to-deploy"
user-id = 220 # Stepan Koltsov (stepancheg)
start = "2022-02-07"
end = "2027-03-14"
[[trusted.pulldown-cmark-to-cmark]]
criteria = "safe-to-deploy"
user-id = 980 # Sebastian Thiel (Byron)
start = "2019-07-03"
end = "2027-03-14"
[[trusted.quote]]
criteria = "safe-to-deploy"
user-id = 3618 # David Tolnay (dtolnay)
start = "2019-04-09"
end = "2027-03-14"
[[trusted.ref-cast]]
criteria = "safe-to-deploy"
user-id = 3618 # David Tolnay (dtolnay)
start = "2019-05-05"
end = "2027-03-14"
[[trusted.ref-cast-impl]]
criteria = "safe-to-deploy"
user-id = 3618 # David Tolnay (dtolnay)
start = "2019-05-05"
end = "2027-03-14"
[[trusted.regex]]
criteria = "safe-to-deploy"
user-id = 189 # Andrew Gallant (BurntSushi)
start = "2019-02-27"
end = "2027-03-14"
[[trusted.regex-automata]]
criteria = "safe-to-deploy"
user-id = 189 # Andrew Gallant (BurntSushi)
start = "2019-02-25"
end = "2027-03-14"
[[trusted.regex-syntax]]
criteria = "safe-to-deploy"
user-id = 189 # Andrew Gallant (BurntSushi)
start = "2019-03-30"
end = "2027-03-14"
[[trusted.reqwest]]
criteria = "safe-to-deploy"
user-id = 359 # Sean McArthur (seanmonstar)
start = "2019-03-04"
end = "2027-03-14"
[[trusted.rustc-demangle]]
criteria = "safe-to-deploy"
user-id = 55123 # rust-lang-owner
start = "2023-03-23"
end = "2027-03-14"
[[trusted.rustix]]
criteria = "safe-to-deploy"
user-id = 6825 # Dan Gohman (sunfishcode)
start = "2021-10-29"
end = "2027-02-14"
[[trusted.ryu]]
criteria = "safe-to-deploy"
user-id = 3618 # David Tolnay (dtolnay)
start = "2019-05-02"
end = "2027-03-14"
[[trusted.scopeguard]]
criteria = "safe-to-deploy"
user-id = 2915 # Amanieu d'Antras (Amanieu)
start = "2020-02-16"
end = "2027-03-14"
[[trusted.semver]]
criteria = "safe-to-deploy"
user-id = 3618 # David Tolnay (dtolnay)
start = "2021-05-25"
end = "2027-03-14"
[[trusted.serde_json]]
criteria = "safe-to-deploy"
user-id = 3618 # David Tolnay (dtolnay)
start = "2019-02-28"
end = "2027-02-14"
[[trusted.slab]]
criteria = "safe-to-deploy"
user-id = 6741 # Alice Ryhl (Darksonn)
start = "2021-10-13"
end = "2027-03-14"
[[trusted.socket2]]
criteria = "safe-to-deploy"
user-id = 6025 # Thomas de Zeeuw (Thomasdezeeuw)
start = "2020-09-09"
end = "2027-03-14"
[[trusted.syn]]
criteria = "safe-to-deploy"
user-id = 3618 # David Tolnay (dtolnay)
@@ -70,26 +644,350 @@ user-id = 2915 # Amanieu d'Antras (Amanieu)
start = "2019-09-07"
end = "2027-02-16"
[[trusted.time]]
criteria = "safe-to-deploy"
user-id = 15682 # Jacob Pratt (jhpratt)
start = "2019-12-19"
end = "2027-03-14"
[[trusted.tinystr]]
criteria = "safe-to-deploy"
user-id = 1139 # Manish Goregaokar (Manishearth)
start = "2021-01-14"
end = "2027-03-14"
[[trusted.tokio]]
criteria = "safe-to-deploy"
user-id = 6741 # Alice Ryhl (Darksonn)
start = "2020-12-25"
end = "2027-03-14"
[[trusted.tokio-macros]]
criteria = "safe-to-deploy"
user-id = 6741 # Alice Ryhl (Darksonn)
start = "2020-10-26"
end = "2027-03-14"
[[trusted.tokio-stream]]
criteria = "safe-to-deploy"
user-id = 6741 # Alice Ryhl (Darksonn)
start = "2021-01-04"
end = "2027-03-14"
[[trusted.tokio-util]]
criteria = "safe-to-deploy"
user-id = 6741 # Alice Ryhl (Darksonn)
start = "2021-01-12"
end = "2027-03-14"
[[trusted.toml]]
criteria = "safe-to-deploy"
user-id = 6743 # Ed Page (epage)
start = "2022-12-14"
end = "2027-02-16"
[[trusted.toml_datetime]]
criteria = "safe-to-deploy"
user-id = 6743 # Ed Page (epage)
start = "2022-10-21"
end = "2027-03-14"
[[trusted.toml_edit]]
criteria = "safe-to-deploy"
user-id = 6743 # Ed Page (epage)
start = "2021-09-13"
end = "2027-03-14"
[[trusted.toml_parser]]
criteria = "safe-to-deploy"
user-id = 6743 # Ed Page (epage)
start = "2025-07-08"
end = "2027-02-16"
[[trusted.tonic]]
criteria = "safe-to-deploy"
user-id = 3959 # Lucio Franco (LucioFranco)
start = "2019-10-02"
end = "2027-03-14"
[[trusted.tonic-build]]
criteria = "safe-to-deploy"
user-id = 10
user-id = 10 # Carl Lerche (carllerche)
start = "2019-09-10"
end = "2027-02-16"
end = "2027-03-14"
[[trusted.tonic-build]]
criteria = "safe-to-deploy"
user-id = 3959 # Lucio Franco (LucioFranco)
start = "2019-10-02"
end = "2027-03-14"
[[trusted.tonic-prost]]
criteria = "safe-to-deploy"
user-id = 3959 # Lucio Franco (LucioFranco)
start = "2025-07-28"
end = "2027-03-14"
[[trusted.tonic-prost-build]]
criteria = "safe-to-deploy"
user-id = 3959 # Lucio Franco (LucioFranco)
start = "2025-07-28"
end = "2027-03-14"
[[trusted.tower]]
criteria = "safe-to-deploy"
user-id = 359 # Sean McArthur (seanmonstar)
start = "2024-09-09"
end = "2027-03-14"
[[trusted.tower-http]]
criteria = "safe-to-deploy"
user-id = 359 # Sean McArthur (seanmonstar)
start = "2024-09-23"
end = "2027-03-14"
[[trusted.tower-layer]]
criteria = "safe-to-deploy"
user-id = 10 # Carl Lerche (carllerche)
start = "2019-04-27"
end = "2027-03-14"
[[trusted.tower-layer]]
criteria = "safe-to-deploy"
user-id = 3959 # Lucio Franco (LucioFranco)
start = "2019-09-11"
end = "2027-03-14"
[[trusted.tower-service]]
criteria = "safe-to-deploy"
user-id = 3959 # Lucio Franco (LucioFranco)
start = "2019-08-20"
end = "2027-03-14"
[[trusted.tracing-subscriber]]
criteria = "safe-to-deploy"
user-id = 10 # Carl Lerche (carllerche)
start = "2025-08-29"
end = "2027-03-14"
[[trusted.ucd-trie]]
criteria = "safe-to-deploy"
user-id = 189 # Andrew Gallant (BurntSushi)
start = "2019-07-21"
end = "2027-03-14"
[[trusted.unicase]]
criteria = "safe-to-deploy"
user-id = 359 # Sean McArthur (seanmonstar)
start = "2019-03-05"
end = "2027-03-14"
[[trusted.unicode-ident]]
criteria = "safe-to-deploy"
user-id = 3618 # David Tolnay (dtolnay)
start = "2021-10-02"
end = "2027-03-14"
[[trusted.url]]
criteria = "safe-to-deploy"
user-id = 1139 # Manish Goregaokar (Manishearth)
start = "2021-02-18"
end = "2027-03-14"
[[trusted.uuid]]
criteria = "safe-to-deploy"
user-id = 3204 # Ashley Mannix (KodrAus)
start = "2019-10-18"
end = "2027-03-14"
[[trusted.valuable]]
criteria = "safe-to-deploy"
user-id = 10 # Carl Lerche (carllerche)
start = "2022-01-03"
end = "2027-03-14"
[[trusted.wait-timeout]]
criteria = "safe-to-deploy"
user-id = 1 # Alex Crichton (alexcrichton)
start = "2025-02-03"
end = "2027-03-14"
[[trusted.wasi]]
criteria = "safe-to-deploy"
user-id = 1 # Alex Crichton (alexcrichton)
start = "2020-06-03"
end = "2027-03-14"
[[trusted.wasi]]
criteria = "safe-to-deploy"
user-id = 6825 # Dan Gohman (sunfishcode)
start = "2019-07-22"
end = "2027-03-14"
[[trusted.wasm-bindgen]]
criteria = "safe-to-deploy"
user-id = 1 # Alex Crichton (alexcrichton)
start = "2019-03-04"
end = "2027-03-14"
[[trusted.wasm-bindgen-futures]]
criteria = "safe-to-deploy"
user-id = 1 # Alex Crichton (alexcrichton)
start = "2019-03-04"
end = "2027-03-14"
[[trusted.wasm-bindgen-macro]]
criteria = "safe-to-deploy"
user-id = 1 # Alex Crichton (alexcrichton)
start = "2019-03-04"
end = "2027-03-14"
[[trusted.wasm-bindgen-macro-support]]
criteria = "safe-to-deploy"
user-id = 1 # Alex Crichton (alexcrichton)
start = "2019-03-04"
end = "2027-03-14"
[[trusted.wasm-bindgen-shared]]
criteria = "safe-to-deploy"
user-id = 1 # Alex Crichton (alexcrichton)
start = "2019-03-04"
end = "2027-03-14"
[[trusted.web-sys]]
criteria = "safe-to-deploy"
user-id = 1 # Alex Crichton (alexcrichton)
start = "2019-03-04"
end = "2027-03-14"
[[trusted.windows-core]]
criteria = "safe-to-deploy"
user-id = 64539 # Kenny Kerr (kennykerr)
start = "2021-11-15"
end = "2027-03-14"
[[trusted.windows-implement]]
criteria = "safe-to-deploy"
user-id = 64539 # Kenny Kerr (kennykerr)
start = "2022-01-27"
end = "2027-03-14"
[[trusted.windows-interface]]
criteria = "safe-to-deploy"
user-id = 64539 # Kenny Kerr (kennykerr)
start = "2022-02-18"
end = "2027-03-14"
[[trusted.windows-result]]
criteria = "safe-to-deploy"
user-id = 64539 # Kenny Kerr (kennykerr)
start = "2024-02-02"
end = "2027-03-14"
[[trusted.windows-strings]]
criteria = "safe-to-deploy"
user-id = 64539 # Kenny Kerr (kennykerr)
start = "2024-02-02"
end = "2027-03-14"
[[trusted.windows-sys]]
criteria = "safe-to-deploy"
user-id = 64539 # Kenny Kerr (kennykerr)
start = "2021-11-15"
end = "2027-02-16"
[[trusted.windows-targets]]
criteria = "safe-to-deploy"
user-id = 64539 # Kenny Kerr (kennykerr)
start = "2022-09-09"
end = "2027-03-14"
[[trusted.windows_aarch64_gnullvm]]
criteria = "safe-to-deploy"
user-id = 64539 # Kenny Kerr (kennykerr)
start = "2022-09-01"
end = "2027-03-14"
[[trusted.windows_aarch64_msvc]]
criteria = "safe-to-deploy"
user-id = 64539 # Kenny Kerr (kennykerr)
start = "2021-11-05"
end = "2027-03-14"
[[trusted.windows_i686_gnu]]
criteria = "safe-to-deploy"
user-id = 64539 # Kenny Kerr (kennykerr)
start = "2021-10-28"
end = "2027-03-14"
[[trusted.windows_i686_gnullvm]]
criteria = "safe-to-deploy"
user-id = 64539 # Kenny Kerr (kennykerr)
start = "2024-04-02"
end = "2027-03-14"
[[trusted.windows_i686_msvc]]
criteria = "safe-to-deploy"
user-id = 64539 # Kenny Kerr (kennykerr)
start = "2021-10-27"
end = "2027-03-14"
[[trusted.windows_x86_64_gnu]]
criteria = "safe-to-deploy"
user-id = 64539 # Kenny Kerr (kennykerr)
start = "2021-10-28"
end = "2027-03-14"
[[trusted.windows_x86_64_gnullvm]]
criteria = "safe-to-deploy"
user-id = 64539 # Kenny Kerr (kennykerr)
start = "2022-09-01"
end = "2027-03-14"
[[trusted.windows_x86_64_msvc]]
criteria = "safe-to-deploy"
user-id = 64539 # Kenny Kerr (kennykerr)
start = "2021-10-27"
end = "2027-03-14"
[[trusted.winnow]]
criteria = "safe-to-deploy"
user-id = 6743 # Ed Page (epage)
start = "2023-02-22"
end = "2027-03-14"
[[trusted.yoke]]
criteria = "safe-to-deploy"
user-id = 1139 # Manish Goregaokar (Manishearth)
start = "2021-05-01"
end = "2027-03-14"
[[trusted.zerocopy]]
criteria = "safe-to-deploy"
user-id = 7178 # Joshua Liebow-Feeser (joshlf)
start = "2019-02-28"
end = "2027-03-14"
[[trusted.zerocopy-derive]]
criteria = "safe-to-deploy"
user-id = 7178 # Joshua Liebow-Feeser (joshlf)
start = "2019-02-28"
end = "2027-03-14"
[[trusted.zerotrie]]
criteria = "safe-to-deploy"
user-id = 1139 # Manish Goregaokar (Manishearth)
start = "2023-10-03"
end = "2027-03-14"
[[trusted.zerovec]]
criteria = "safe-to-deploy"
user-id = 1139 # Manish Goregaokar (Manishearth)
start = "2021-04-19"
end = "2027-03-14"
[[trusted.zmij]]
criteria = "safe-to-deploy"
user-id = 3618 # David Tolnay (dtolnay)
start = "2025-12-18"
end = "2027-03-14"

View File

@@ -4,30 +4,27 @@
[cargo-vet]
version = "0.10"
[imports.OpenDevicePartnership]
url = "https://raw.githubusercontent.com/OpenDevicePartnership/rust-crate-audits/refs/heads/main/audits.toml"
[imports.bytecode-alliance]
url = "https://raw.githubusercontent.com/bytecodealliance/wasmtime/main/supply-chain/audits.toml"
[imports.embark-studios]
url = "https://raw.githubusercontent.com/EmbarkStudios/rust-ecosystem/main/audits.toml"
[imports.google]
url = "https://raw.githubusercontent.com/google/supply-chain/main/audits.toml"
[imports.isrg]
url = "https://raw.githubusercontent.com/divviup/libprio-rs/main/supply-chain/audits.toml"
[imports.mozilla]
url = "https://raw.githubusercontent.com/mozilla/supply-chain/main/audits.toml"
[imports.zcash]
url = "https://raw.githubusercontent.com/zcash/rust-ecosystem/main/supply-chain/audits.toml"
[[exemptions.addr2line]]
version = "0.25.1"
criteria = "safe-to-deploy"
[[exemptions.aho-corasick]]
version = "1.1.4"
criteria = "safe-to-deploy"
[[exemptions.anyhow]]
version = "1.0.101"
criteria = "safe-to-deploy"
[[exemptions.asn1-rs]]
version = "0.7.1"
criteria = "safe-to-deploy"
@@ -40,18 +37,6 @@ criteria = "safe-to-deploy"
version = "0.2.0"
criteria = "safe-to-deploy"
[[exemptions.async-trait]]
version = "0.1.89"
criteria = "safe-to-deploy"
[[exemptions.aws-lc-rs]]
version = "1.15.4"
criteria = "safe-to-deploy"
[[exemptions.aws-lc-sys]]
version = "0.37.0"
criteria = "safe-to-deploy"
[[exemptions.axum]]
version = "0.8.8"
criteria = "safe-to-deploy"
@@ -60,10 +45,6 @@ criteria = "safe-to-deploy"
version = "0.5.6"
criteria = "safe-to-deploy"
[[exemptions.backtrace]]
version = "0.3.76"
criteria = "safe-to-deploy"
[[exemptions.backtrace-ext]]
version = "0.2.1"
criteria = "safe-to-deploy"
@@ -72,26 +53,14 @@ criteria = "safe-to-deploy"
version = "0.9.1"
criteria = "safe-to-deploy"
[[exemptions.bitflags]]
version = "2.10.0"
criteria = "safe-to-deploy"
[[exemptions.block-buffer]]
version = "0.11.0"
criteria = "safe-to-deploy"
[[exemptions.bytes]]
version = "1.11.1"
criteria = "safe-to-deploy"
[[exemptions.cc]]
version = "1.2.55"
criteria = "safe-to-deploy"
[[exemptions.cfg-if]]
version = "1.0.4"
criteria = "safe-to-deploy"
[[exemptions.chacha20]]
version = "0.10.0"
criteria = "safe-to-deploy"
@@ -100,26 +69,14 @@ criteria = "safe-to-deploy"
version = "0.4.43"
criteria = "safe-to-deploy"
[[exemptions.cmake]]
version = "0.1.57"
criteria = "safe-to-deploy"
[[exemptions.cpufeatures]]
version = "0.2.17"
criteria = "safe-to-deploy"
[[exemptions.cpufeatures]]
version = "0.3.0"
criteria = "safe-to-deploy"
[[exemptions.crc32fast]]
version = "1.5.0"
criteria = "safe-to-deploy"
[[exemptions.crossbeam-utils]]
version = "0.8.21"
criteria = "safe-to-deploy"
[[exemptions.crypto-common]]
version = "0.2.0"
criteria = "safe-to-deploy"
@@ -156,10 +113,6 @@ criteria = "safe-to-deploy"
version = "10.0.0"
criteria = "safe-to-deploy"
[[exemptions.deranged]]
version = "0.5.5"
criteria = "safe-to-deploy"
[[exemptions.diesel]]
version = "2.3.6"
criteria = "safe-to-deploy"
@@ -192,10 +145,6 @@ criteria = "safe-to-deploy"
version = "0.2.0"
criteria = "safe-to-deploy"
[[exemptions.dyn-clone]]
version = "1.0.20"
criteria = "safe-to-deploy"
[[exemptions.ed25519]]
version = "3.0.0-rc.4"
criteria = "safe-to-deploy"
@@ -204,10 +153,6 @@ criteria = "safe-to-deploy"
version = "3.0.0-pre.6"
criteria = "safe-to-deploy"
[[exemptions.fiat-crypto]]
version = "0.3.0"
criteria = "safe-to-deploy"
[[exemptions.find-msvc-tools]]
version = "0.1.9"
criteria = "safe-to-deploy"
@@ -216,22 +161,10 @@ criteria = "safe-to-deploy"
version = "0.5.7"
criteria = "safe-to-deploy"
[[exemptions.flate2]]
version = "1.1.9"
criteria = "safe-to-deploy"
[[exemptions.fs_extra]]
version = "1.3.0"
criteria = "safe-to-deploy"
[[exemptions.futures-task]]
version = "0.3.31"
criteria = "safe-to-deploy"
[[exemptions.futures-util]]
version = "0.3.31"
criteria = "safe-to-deploy"
[[exemptions.getrandom]]
version = "0.2.17"
criteria = "safe-to-deploy"
@@ -244,30 +177,10 @@ criteria = "safe-to-deploy"
version = "0.4.1"
criteria = "safe-to-deploy"
[[exemptions.hashbrown]]
version = "0.14.5"
criteria = "safe-to-deploy"
[[exemptions.http]]
version = "1.4.0"
criteria = "safe-to-deploy"
[[exemptions.http-body-util]]
version = "0.1.3"
criteria = "safe-to-deploy"
[[exemptions.httparse]]
version = "1.10.1"
criteria = "safe-to-deploy"
[[exemptions.hybrid-array]]
version = "0.4.7"
criteria = "safe-to-deploy"
[[exemptions.hyper]]
version = "1.8.1"
criteria = "safe-to-deploy"
[[exemptions.hyper-timeout]]
version = "0.5.2"
criteria = "safe-to-deploy"
@@ -276,18 +189,6 @@ criteria = "safe-to-deploy"
version = "0.1.65"
criteria = "safe-to-deploy"
[[exemptions.id-arena]]
version = "2.3.0"
criteria = "safe-to-deploy"
[[exemptions.ident_case]]
version = "1.0.1"
criteria = "safe-to-deploy"
[[exemptions.indexmap]]
version = "2.13.0"
criteria = "safe-to-deploy"
[[exemptions.is_ci]]
version = "1.2.0"
criteria = "safe-to-deploy"
@@ -296,14 +197,6 @@ criteria = "safe-to-deploy"
version = "0.14.0"
criteria = "safe-to-deploy"
[[exemptions.itoa]]
version = "1.0.17"
criteria = "safe-to-deploy"
[[exemptions.jobserver]]
version = "0.1.34"
criteria = "safe-to-deploy"
[[exemptions.js-sys]]
version = "0.3.85"
criteria = "safe-to-deploy"
@@ -320,26 +213,10 @@ criteria = "safe-to-deploy"
version = "0.35.0"
criteria = "safe-to-deploy"
[[exemptions.linux-raw-sys]]
version = "0.11.0"
criteria = "safe-to-deploy"
[[exemptions.lock_api]]
version = "0.4.14"
criteria = "safe-to-deploy"
[[exemptions.log]]
version = "0.4.29"
criteria = "safe-to-deploy"
[[exemptions.matchit]]
version = "0.8.4"
criteria = "safe-to-deploy"
[[exemptions.memchr]]
version = "2.8.0"
criteria = "safe-to-deploy"
[[exemptions.memsafe]]
version = "0.4.0"
criteria = "safe-to-deploy"
@@ -360,34 +237,14 @@ criteria = "safe-to-deploy"
version = "2.3.0"
criteria = "safe-to-deploy"
[[exemptions.mime]]
version = "0.3.17"
criteria = "safe-to-deploy"
[[exemptions.minimal-lexical]]
version = "0.2.1"
criteria = "safe-to-deploy"
[[exemptions.mio]]
version = "1.1.1"
criteria = "safe-to-deploy"
[[exemptions.multimap]]
version = "0.10.1"
criteria = "safe-to-deploy"
[[exemptions.num-bigint]]
version = "0.4.6"
criteria = "safe-to-deploy"
[[exemptions.num-conv]]
version = "0.2.0"
criteria = "safe-to-deploy"
[[exemptions.object]]
version = "0.37.3"
criteria = "safe-to-deploy"
[[exemptions.oid-registry]]
version = "0.8.1"
criteria = "safe-to-deploy"
@@ -400,14 +257,6 @@ criteria = "safe-to-deploy"
version = "4.2.3"
criteria = "safe-to-deploy"
[[exemptions.parking_lot]]
version = "0.12.5"
criteria = "safe-to-deploy"
[[exemptions.parking_lot_core]]
version = "0.9.12"
criteria = "safe-to-deploy"
[[exemptions.pem]]
version = "3.0.6"
criteria = "safe-to-deploy"
@@ -424,58 +273,14 @@ criteria = "safe-to-deploy"
version = "1.1.10"
criteria = "safe-to-deploy"
[[exemptions.portable-atomic]]
version = "1.13.1"
criteria = "safe-to-deploy"
[[exemptions.prettyplease]]
version = "0.2.37"
criteria = "safe-to-deploy"
[[exemptions.proc-macro2]]
version = "1.0.106"
criteria = "safe-to-deploy"
[[exemptions.prost]]
version = "0.14.3"
criteria = "safe-to-deploy"
[[exemptions.prost-build]]
version = "0.14.3"
criteria = "safe-to-deploy"
[[exemptions.prost-derive]]
version = "0.14.3"
criteria = "safe-to-deploy"
[[exemptions.prost-types]]
version = "0.14.3"
criteria = "safe-to-deploy"
[[exemptions.pulldown-cmark]]
version = "0.13.0"
criteria = "safe-to-deploy"
[[exemptions.pulldown-cmark-to-cmark]]
version = "22.0.0"
criteria = "safe-to-deploy"
[[exemptions.quote]]
version = "1.0.44"
criteria = "safe-to-deploy"
[[exemptions.r-efi]]
version = "5.3.0"
criteria = "safe-to-deploy"
[[exemptions.rand]]
version = "0.10.0"
criteria = "safe-to-deploy"
[[exemptions.rand_core]]
version = "0.10.0"
criteria = "safe-to-deploy"
[[exemptions.rcgen]]
version = "0.14.7"
criteria = "safe-to-deploy"
@@ -484,18 +289,6 @@ criteria = "safe-to-deploy"
version = "0.5.18"
criteria = "safe-to-deploy"
[[exemptions.regex]]
version = "1.12.3"
criteria = "safe-to-deploy"
[[exemptions.regex-automata]]
version = "0.4.14"
criteria = "safe-to-deploy"
[[exemptions.regex-syntax]]
version = "0.8.9"
criteria = "safe-to-deploy"
[[exemptions.ring]]
version = "0.17.14"
criteria = "safe-to-deploy"
@@ -504,10 +297,6 @@ criteria = "safe-to-deploy"
version = "0.1.0"
criteria = "safe-to-deploy"
[[exemptions.rustc-demangle]]
version = "0.1.27"
criteria = "safe-to-deploy"
[[exemptions.rusticata-macros]]
version = "4.1.0"
criteria = "safe-to-deploy"
@@ -528,10 +317,6 @@ criteria = "safe-to-deploy"
version = "0.1.4"
criteria = "safe-to-deploy"
[[exemptions.scopeguard]]
version = "1.2.0"
criteria = "safe-to-deploy"
[[exemptions.secrecy]]
version = "0.10.3"
criteria = "safe-to-deploy"
@@ -540,18 +325,6 @@ criteria = "safe-to-deploy"
version = "1.0.27"
criteria = "safe-to-deploy"
[[exemptions.serde]]
version = "1.0.228"
criteria = "safe-to-deploy"
[[exemptions.serde_core]]
version = "1.0.228"
criteria = "safe-to-deploy"
[[exemptions.serde_derive]]
version = "1.0.228"
criteria = "safe-to-deploy"
[[exemptions.sha2]]
version = "0.11.0-rc.5"
criteria = "safe-to-deploy"
@@ -568,10 +341,6 @@ criteria = "safe-to-deploy"
version = "0.3.8"
criteria = "safe-to-deploy"
[[exemptions.slab]]
version = "0.4.12"
criteria = "safe-to-deploy"
[[exemptions.smlang]]
version = "0.8.0"
criteria = "safe-to-deploy"
@@ -580,10 +349,6 @@ criteria = "safe-to-deploy"
version = "0.8.0"
criteria = "safe-to-deploy"
[[exemptions.socket2]]
version = "0.6.2"
criteria = "safe-to-deploy"
[[exemptions.sqlite-wasm-rs]]
version = "0.5.2"
criteria = "safe-to-deploy"
@@ -592,10 +357,6 @@ criteria = "safe-to-deploy"
version = "0.1.0"
criteria = "safe-to-deploy"
[[exemptions.subtle]]
version = "2.6.1"
criteria = "safe-to-deploy"
[[exemptions.supports-color]]
version = "3.0.2"
criteria = "safe-to-deploy"
@@ -620,74 +381,10 @@ criteria = "safe-to-deploy"
version = "0.4.3"
criteria = "safe-to-deploy"
[[exemptions.thiserror]]
version = "2.0.18"
criteria = "safe-to-deploy"
[[exemptions.thiserror-impl]]
version = "2.0.18"
criteria = "safe-to-deploy"
[[exemptions.time]]
version = "0.3.47"
criteria = "safe-to-deploy"
[[exemptions.time-core]]
version = "0.1.8"
criteria = "safe-to-deploy"
[[exemptions.time-macros]]
version = "0.2.27"
criteria = "safe-to-deploy"
[[exemptions.tokio]]
version = "1.49.0"
criteria = "safe-to-deploy"
[[exemptions.tokio-macros]]
version = "2.6.0"
criteria = "safe-to-deploy"
[[exemptions.tokio-rustls]]
version = "0.26.4"
criteria = "safe-to-deploy"
[[exemptions.tokio-stream]]
version = "0.1.18"
criteria = "safe-to-deploy"
[[exemptions.tokio-util]]
version = "0.7.18"
criteria = "safe-to-deploy"
[[exemptions.tonic]]
version = "0.14.3"
criteria = "safe-to-deploy"
[[exemptions.tonic-build]]
version = "0.14.3"
criteria = "safe-to-deploy"
[[exemptions.tonic-prost]]
version = "0.14.4"
criteria = "safe-to-deploy"
[[exemptions.tonic-prost-build]]
version = "0.14.3"
criteria = "safe-to-deploy"
[[exemptions.tower]]
version = "0.5.3"
criteria = "safe-to-deploy"
[[exemptions.tower-layer]]
version = "0.3.3"
criteria = "safe-to-deploy"
[[exemptions.tower-service]]
version = "0.3.3"
criteria = "safe-to-deploy"
[[exemptions.tracing]]
version = "0.1.44"
criteria = "safe-to-deploy"
@@ -708,34 +405,10 @@ criteria = "safe-to-run"
version = "1.19.0"
criteria = "safe-to-deploy"
[[exemptions.unicase]]
version = "2.9.0"
criteria = "safe-to-deploy"
[[exemptions.unicode-ident]]
version = "1.0.23"
criteria = "safe-to-deploy"
[[exemptions.untrusted]]
version = "0.7.1"
criteria = "safe-to-deploy"
[[exemptions.untrusted]]
version = "0.9.0"
criteria = "safe-to-deploy"
[[exemptions.uuid]]
version = "1.20.0"
criteria = "safe-to-deploy"
[[exemptions.wasi]]
version = "0.11.1+wasi-snapshot-preview1"
criteria = "safe-to-deploy"
[[exemptions.wasm-bindgen]]
version = "0.2.108"
criteria = "safe-to-deploy"
[[exemptions.wasm-bindgen-macro]]
version = "0.2.108"
criteria = "safe-to-deploy"
@@ -760,102 +433,6 @@ criteria = "safe-to-deploy"
version = "0.4.0"
criteria = "safe-to-deploy"
[[exemptions.windows-core]]
version = "0.62.2"
criteria = "safe-to-deploy"
[[exemptions.windows-implement]]
version = "0.60.2"
criteria = "safe-to-deploy"
[[exemptions.windows-interface]]
version = "0.59.3"
criteria = "safe-to-deploy"
[[exemptions.windows-result]]
version = "0.4.1"
criteria = "safe-to-deploy"
[[exemptions.windows-strings]]
version = "0.5.1"
criteria = "safe-to-deploy"
[[exemptions.windows-targets]]
version = "0.52.6"
criteria = "safe-to-deploy"
[[exemptions.windows-targets]]
version = "0.53.5"
criteria = "safe-to-deploy"
[[exemptions.windows_aarch64_gnullvm]]
version = "0.52.6"
criteria = "safe-to-deploy"
[[exemptions.windows_aarch64_gnullvm]]
version = "0.53.1"
criteria = "safe-to-deploy"
[[exemptions.windows_aarch64_msvc]]
version = "0.52.6"
criteria = "safe-to-deploy"
[[exemptions.windows_aarch64_msvc]]
version = "0.53.1"
criteria = "safe-to-deploy"
[[exemptions.windows_i686_gnu]]
version = "0.52.6"
criteria = "safe-to-deploy"
[[exemptions.windows_i686_gnu]]
version = "0.53.1"
criteria = "safe-to-deploy"
[[exemptions.windows_i686_gnullvm]]
version = "0.52.6"
criteria = "safe-to-deploy"
[[exemptions.windows_i686_gnullvm]]
version = "0.53.1"
criteria = "safe-to-deploy"
[[exemptions.windows_i686_msvc]]
version = "0.52.6"
criteria = "safe-to-deploy"
[[exemptions.windows_i686_msvc]]
version = "0.53.1"
criteria = "safe-to-deploy"
[[exemptions.windows_x86_64_gnu]]
version = "0.52.6"
criteria = "safe-to-deploy"
[[exemptions.windows_x86_64_gnu]]
version = "0.53.1"
criteria = "safe-to-deploy"
[[exemptions.windows_x86_64_gnullvm]]
version = "0.52.6"
criteria = "safe-to-deploy"
[[exemptions.windows_x86_64_gnullvm]]
version = "0.53.1"
criteria = "safe-to-deploy"
[[exemptions.windows_x86_64_msvc]]
version = "0.52.6"
criteria = "safe-to-deploy"
[[exemptions.windows_x86_64_msvc]]
version = "0.53.1"
criteria = "safe-to-deploy"
[[exemptions.winnow]]
version = "0.7.14"
criteria = "safe-to-deploy"
[[exemptions.x509-parser]]
version = "0.18.1"
criteria = "safe-to-deploy"
@@ -864,10 +441,6 @@ criteria = "safe-to-deploy"
version = "0.5.2"
criteria = "safe-to-deploy"
[[exemptions.zmij]]
version = "1.0.20"
criteria = "safe-to-deploy"
[[exemptions.zstd]]
version = "0.13.3"
criteria = "safe-to-deploy"

File diff suppressed because it is too large Load Diff

View File

@@ -1,31 +0,0 @@
Extension Discovery Cache
=========================
This folder is used by `package:extension_discovery` to cache lists of
packages that contains extensions for other packages.
DO NOT USE THIS FOLDER
----------------------
* Do not read (or rely) the contents of this folder.
* Do write to this folder.
If you're interested in the lists of extensions stored in this folder use the
API offered by package `extension_discovery` to get this information.
If this package doesn't work for your use-case, then don't try to read the
contents of this folder. It may change, and will not remain stable.
Use package `extension_discovery`
---------------------------------
If you want to access information from this folder.
Feel free to delete this folder
-------------------------------
Files in this folder act as a cache, and the cache is discarded if the files
are older than the modification time of `.dart_tool/package_config.json`.
Hence, it should never be necessary to clear this cache manually, if you find a
need to do please file a bug.

View File

@@ -1 +0,0 @@
{"version":2,"entries":[{"package":"arbiter","rootUri":"../","packageUri":"lib/"}]}

View File

@@ -1,172 +0,0 @@
{
"configVersion": 2,
"packages": [
{
"name": "async",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/async-2.13.0",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "boolean_selector",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/boolean_selector-2.1.2",
"packageUri": "lib/",
"languageVersion": "3.1"
},
{
"name": "characters",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/characters-1.4.0",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "clock",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/clock-1.1.2",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "collection",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/collection-1.19.1",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "fake_async",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/fake_async-1.3.3",
"packageUri": "lib/",
"languageVersion": "3.3"
},
{
"name": "flutter",
"rootUri": "file:///Users/kaska/.local/share/mise/installs/flutter/3.38.9-stable/packages/flutter",
"packageUri": "lib/",
"languageVersion": "3.8"
},
{
"name": "flutter_lints",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/flutter_lints-6.0.0",
"packageUri": "lib/",
"languageVersion": "3.8"
},
{
"name": "flutter_test",
"rootUri": "file:///Users/kaska/.local/share/mise/installs/flutter/3.38.9-stable/packages/flutter_test",
"packageUri": "lib/",
"languageVersion": "3.8"
},
{
"name": "leak_tracker",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/leak_tracker-11.0.2",
"packageUri": "lib/",
"languageVersion": "3.2"
},
{
"name": "leak_tracker_flutter_testing",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/leak_tracker_flutter_testing-3.0.10",
"packageUri": "lib/",
"languageVersion": "3.2"
},
{
"name": "leak_tracker_testing",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/leak_tracker_testing-3.0.2",
"packageUri": "lib/",
"languageVersion": "3.2"
},
{
"name": "lints",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/lints-6.1.0",
"packageUri": "lib/",
"languageVersion": "3.8"
},
{
"name": "matcher",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/matcher-0.12.17",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "material_color_utilities",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/material_color_utilities-0.11.1",
"packageUri": "lib/",
"languageVersion": "2.17"
},
{
"name": "meta",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/meta-1.17.0",
"packageUri": "lib/",
"languageVersion": "3.5"
},
{
"name": "path",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/path-1.9.1",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "sky_engine",
"rootUri": "file:///Users/kaska/.local/share/mise/installs/flutter/3.38.9-stable/bin/cache/pkg/sky_engine",
"packageUri": "lib/",
"languageVersion": "3.8"
},
{
"name": "source_span",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/source_span-1.10.2",
"packageUri": "lib/",
"languageVersion": "3.1"
},
{
"name": "stack_trace",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/stack_trace-1.12.1",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "stream_channel",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/stream_channel-2.1.4",
"packageUri": "lib/",
"languageVersion": "3.3"
},
{
"name": "string_scanner",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/string_scanner-1.4.1",
"packageUri": "lib/",
"languageVersion": "3.1"
},
{
"name": "term_glyph",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/term_glyph-1.2.2",
"packageUri": "lib/",
"languageVersion": "3.1"
},
{
"name": "test_api",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/test_api-0.7.7",
"packageUri": "lib/",
"languageVersion": "3.5"
},
{
"name": "vector_math",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/vector_math-2.2.0",
"packageUri": "lib/",
"languageVersion": "3.1"
},
{
"name": "vm_service",
"rootUri": "file:///Users/kaska/.pub-cache/hosted/pub.dev/vm_service-15.0.2",
"packageUri": "lib/",
"languageVersion": "3.5"
},
{
"name": "arbiter",
"rootUri": "../",
"packageUri": "lib/",
"languageVersion": "3.10"
}
],
"generator": "pub",
"generatorVersion": "3.10.8",
"flutterRoot": "file:///Users/kaska/.local/share/mise/installs/flutter/3.38.9-stable",
"flutterVersion": "3.38.9",
"pubCache": "file:///Users/kaska/.pub-cache"
}

View File

@@ -1,224 +0,0 @@
{
"roots": [
"arbiter"
],
"packages": [
{
"name": "arbiter",
"version": "0.1.0",
"dependencies": [
"flutter"
],
"devDependencies": [
"flutter_lints",
"flutter_test"
]
},
{
"name": "flutter_lints",
"version": "6.0.0",
"dependencies": [
"lints"
]
},
{
"name": "flutter_test",
"version": "0.0.0",
"dependencies": [
"clock",
"collection",
"fake_async",
"flutter",
"leak_tracker_flutter_testing",
"matcher",
"meta",
"path",
"stack_trace",
"stream_channel",
"test_api",
"vector_math"
]
},
{
"name": "flutter",
"version": "0.0.0",
"dependencies": [
"characters",
"collection",
"material_color_utilities",
"meta",
"sky_engine",
"vector_math"
]
},
{
"name": "lints",
"version": "6.1.0",
"dependencies": []
},
{
"name": "stream_channel",
"version": "2.1.4",
"dependencies": [
"async"
]
},
{
"name": "meta",
"version": "1.17.0",
"dependencies": []
},
{
"name": "collection",
"version": "1.19.1",
"dependencies": []
},
{
"name": "leak_tracker_flutter_testing",
"version": "3.0.10",
"dependencies": [
"flutter",
"leak_tracker",
"leak_tracker_testing",
"matcher",
"meta"
]
},
{
"name": "vector_math",
"version": "2.2.0",
"dependencies": []
},
{
"name": "stack_trace",
"version": "1.12.1",
"dependencies": [
"path"
]
},
{
"name": "clock",
"version": "1.1.2",
"dependencies": []
},
{
"name": "fake_async",
"version": "1.3.3",
"dependencies": [
"clock",
"collection"
]
},
{
"name": "path",
"version": "1.9.1",
"dependencies": []
},
{
"name": "matcher",
"version": "0.12.17",
"dependencies": [
"async",
"meta",
"stack_trace",
"term_glyph",
"test_api"
]
},
{
"name": "test_api",
"version": "0.7.7",
"dependencies": [
"async",
"boolean_selector",
"collection",
"meta",
"source_span",
"stack_trace",
"stream_channel",
"string_scanner",
"term_glyph"
]
},
{
"name": "sky_engine",
"version": "0.0.0",
"dependencies": []
},
{
"name": "material_color_utilities",
"version": "0.11.1",
"dependencies": [
"collection"
]
},
{
"name": "characters",
"version": "1.4.0",
"dependencies": []
},
{
"name": "async",
"version": "2.13.0",
"dependencies": [
"collection",
"meta"
]
},
{
"name": "leak_tracker_testing",
"version": "3.0.2",
"dependencies": [
"leak_tracker",
"matcher",
"meta"
]
},
{
"name": "leak_tracker",
"version": "11.0.2",
"dependencies": [
"clock",
"collection",
"meta",
"path",
"vm_service"
]
},
{
"name": "term_glyph",
"version": "1.2.2",
"dependencies": []
},
{
"name": "string_scanner",
"version": "1.4.1",
"dependencies": [
"source_span"
]
},
{
"name": "source_span",
"version": "1.10.2",
"dependencies": [
"collection",
"path",
"term_glyph"
]
},
{
"name": "boolean_selector",
"version": "2.1.2",
"dependencies": [
"source_span",
"string_scanner"
]
},
{
"name": "vm_service",
"version": "15.0.2",
"dependencies": []
}
],
"configVersion": 1
}

View File

@@ -1 +0,0 @@
3.38.9

View File

@@ -1,4 +1,4 @@
# app
# useragent
A new Flutter project.

14
useragent/android/.gitignore vendored Normal file
View File

@@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks

View File

@@ -0,0 +1,44 @@
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.example.useragent"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.useragent"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
flutter {
source = "../.."
}

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@@ -0,0 +1,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="useragent"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

View File

@@ -0,0 +1,5 @@
package com.example.useragent
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@@ -0,0 +1,24 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

View File

@@ -0,0 +1,2 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true

View File

@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip

View File

@@ -0,0 +1,26 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.11.1" apply false
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
}
include(":app")

View File

@@ -0,0 +1,56 @@
import 'dart:convert';
class ArbiterUrl {
const ArbiterUrl({
required this.host,
required this.port,
required this.caCert,
this.bootstrapToken,
});
final String host;
final int port;
final List<int> caCert;
final String? bootstrapToken;
static const _scheme = 'arbiter';
static const _certQueryKey = 'cert';
static const _bootstrapTokenQueryKey = 'bootstrap_token';
static ArbiterUrl parse(String value) {
final uri = Uri.tryParse(value);
if (uri == null || uri.scheme != _scheme) {
throw const FormatException("Invalid URL scheme, expected 'arbiter://'");
}
if (uri.host.isEmpty) {
throw const FormatException('Missing host in URL');
}
if (!uri.hasPort) {
throw const FormatException('Missing port in URL');
}
final cert = uri.queryParameters[_certQueryKey];
if (cert == null || cert.isEmpty) {
throw const FormatException("Missing 'cert' query parameter in URL");
}
final decodedCert = _decodeCert(cert);
return ArbiterUrl(
host: uri.host,
port: uri.port,
caCert: decodedCert,
bootstrapToken: uri.queryParameters[_bootstrapTokenQueryKey],
);
}
static List<int> _decodeCert(String cert) {
try {
return base64Url.decode(base64Url.normalize(cert));
} on FormatException catch (error) {
throw FormatException("Invalid base64 in 'cert' query parameter: ${error.message}");
}
}
}

View File

@@ -0,0 +1,103 @@
import 'dart:async';
import 'dart:convert';
import 'package:arbiter/features/connection/connection.dart';
import 'package:arbiter/features/connection/server_info_storage.dart';
import 'package:arbiter/features/identity/pk_manager.dart';
import 'package:arbiter/proto/arbiter.pbgrpc.dart';
import 'package:arbiter/proto/user_agent.pb.dart';
import 'package:grpc/grpc.dart';
import 'package:mtcore/markettakers.dart';
Future<Connection> connectAndAuthorize(
StoredServerInfo serverInfo,
KeyHandle key, {
String? bootstrapToken,
}) async {
try {
final connection = await _connect(serverInfo);
talker.info(
'Connected to server at ${serverInfo.address}:${serverInfo.port}',
);
final pubkey = await key.getPublicKey();
final req = AuthChallengeRequest(
pubkey: pubkey,
bootstrapToken: bootstrapToken,
keyType: switch (key.alg) {
KeyAlgorithm.rsa => KeyType.KEY_TYPE_RSA,
KeyAlgorithm.ecdsa => KeyType.KEY_TYPE_ECDSA_SECP256K1,
KeyAlgorithm.ed25519 => KeyType.KEY_TYPE_ED25519,
},
);
await connection.send(UserAgentRequest(authChallengeRequest: req));
talker.info(
"Sent auth challenge request with pubkey ${base64Encode(pubkey)}",
);
final response = await connection.receive();
talker.info('Received response from server, checking auth flow...');
if (response.hasAuthOk()) {
talker.info('Authentication successful, connection established');
return connection;
}
if (!response.hasAuthChallenge()) {
throw Exception(
'Expected AuthChallengeResponse, got ${response.whichPayload()}',
);
}
final challenge = _formatChallenge(response.authChallenge, pubkey);
talker.info(
'Received auth challenge, signing with key ${base64Encode(pubkey)}',
);
final signature = await key.sign(challenge);
await connection.send(
UserAgentRequest(authChallengeSolution: AuthChallengeSolution(signature: signature)),
);
talker.info('Sent auth challenge solution, waiting for server response...');
final solutionResponse = await connection.receive();
if (!solutionResponse.hasAuthOk()) {
throw Exception(
'Expected AuthChallengeSolutionResponse, got ${solutionResponse.whichPayload()}',
);
}
talker.info('Authentication successful, connection established');
return connection;
} catch (e) {
throw Exception('Failed to connect to server: $e');
}
}
Future<Connection> _connect(StoredServerInfo serverInfo) async {
final channel = ClientChannel(
serverInfo.address,
port: serverInfo.port,
options: ChannelOptions(
connectTimeout: const Duration(seconds: 10),
credentials: ChannelCredentials.secure(
onBadCertificate: (cert, host) {
return true;
},
),
),
);
final client = ArbiterServiceClient(channel);
final tx = StreamController<UserAgentRequest>();
final rx = client.userAgent(tx.stream);
return Connection(channel: channel, tx: tx, rx: rx);
}
List<int> _formatChallenge(AuthChallenge challenge, List<int> pubkey) {
final encodedPubkey = base64Encode(pubkey);
final payload = "${challenge.nonce}:$encodedPubkey";
return utf8.encode(payload);
}

View File

@@ -0,0 +1,37 @@
import 'dart:async';
import 'package:arbiter/proto/user_agent.pb.dart';
import 'package:grpc/grpc.dart';
import 'package:mtcore/markettakers.dart';
class Connection {
final ClientChannel channel;
final StreamController<UserAgentRequest> _tx;
final StreamIterator<UserAgentResponse> _rx;
Connection({
required this.channel,
required StreamController<UserAgentRequest> tx,
required ResponseStream<UserAgentResponse> rx,
}) : _tx = tx,
_rx = StreamIterator(rx);
Future<void> send(UserAgentRequest request) async {
talker.debug('Sending request: ${request.toDebugString()}');
_tx.add(request);
}
Future<UserAgentResponse> receive() async {
final hasValue = await _rx.moveNext();
if (!hasValue) {
throw Exception('Connection closed while waiting for server response.');
}
talker.debug('Received response: ${_rx.current.toDebugString()}');
return _rx.current;
}
Future<void> close() async {
await _tx.close();
await channel.shutdown();
}
}

View File

@@ -0,0 +1,56 @@
import 'package:arbiter/features/connection/connection.dart';
import 'package:arbiter/proto/evm.pb.dart';
import 'package:arbiter/proto/user_agent.pb.dart';
import 'package:protobuf/well_known_types/google/protobuf/empty.pb.dart';
Future<List<WalletEntry>> listEvmWallets(Connection connection) async {
await connection.send(UserAgentRequest(evmWalletList: Empty()));
final response = await connection.receive();
if (!response.hasEvmWalletList()) {
throw Exception(
'Expected EVM wallet list response, got ${response.whichPayload()}',
);
}
final result = response.evmWalletList;
switch (result.whichResult()) {
case WalletListResponse_Result.wallets:
return result.wallets.wallets.toList(growable: false);
case WalletListResponse_Result.error:
throw Exception(_describeEvmError(result.error));
case WalletListResponse_Result.notSet:
throw Exception('EVM wallet list response was empty.');
}
}
Future<void> createEvmWallet(Connection connection) async {
await connection.send(UserAgentRequest(evmWalletCreate: Empty()));
final response = await connection.receive();
if (!response.hasEvmWalletCreate()) {
throw Exception(
'Expected EVM wallet create response, got ${response.whichPayload()}',
);
}
final result = response.evmWalletCreate;
switch (result.whichResult()) {
case WalletCreateResponse_Result.wallet:
return;
case WalletCreateResponse_Result.error:
throw Exception(_describeEvmError(result.error));
case WalletCreateResponse_Result.notSet:
throw Exception('Wallet creation returned no result.');
}
}
String _describeEvmError(EvmError error) {
return switch (error) {
EvmError.EVM_ERROR_VAULT_SEALED =>
'The vault is sealed. Unseal it before using EVM wallets.',
EvmError.EVM_ERROR_INTERNAL || EvmError.EVM_ERROR_UNSPECIFIED =>
'The server failed to process the EVM request.',
_ => 'The server failed to process the EVM request.',
};
}

View File

@@ -0,0 +1,122 @@
import 'package:arbiter/features/connection/connection.dart';
import 'package:arbiter/proto/evm.pb.dart';
import 'package:arbiter/proto/user_agent.pb.dart';
import 'package:fixnum/fixnum.dart';
import 'package:protobuf/well_known_types/google/protobuf/timestamp.pb.dart';
Future<List<GrantEntry>> listEvmGrants(
Connection connection, {
int? walletId,
}) async {
final request = EvmGrantListRequest();
if (walletId != null) {
request.walletId = walletId;
}
await connection.send(UserAgentRequest(evmGrantList: request));
final response = await connection.receive();
if (!response.hasEvmGrantList()) {
throw Exception(
'Expected EVM grant list response, got ${response.whichPayload()}',
);
}
final result = response.evmGrantList;
switch (result.whichResult()) {
case EvmGrantListResponse_Result.grants:
return result.grants.grants.toList(growable: false);
case EvmGrantListResponse_Result.error:
throw Exception(_describeGrantError(result.error));
case EvmGrantListResponse_Result.notSet:
throw Exception('EVM grant list response was empty.');
}
}
Future<int> createEvmGrant(
Connection connection, {
required int clientId,
required int walletId,
required Int64 chainId,
DateTime? validFrom,
DateTime? validUntil,
List<int>? maxGasFeePerGas,
List<int>? maxPriorityFeePerGas,
TransactionRateLimit? rateLimit,
required SpecificGrant specific,
}) async {
await connection.send(
UserAgentRequest(
evmGrantCreate: EvmGrantCreateRequest(
clientId: clientId,
shared: SharedSettings(
walletId: walletId,
chainId: chainId,
validFrom: validFrom == null ? null : _toTimestamp(validFrom),
validUntil: validUntil == null ? null : _toTimestamp(validUntil),
maxGasFeePerGas: maxGasFeePerGas,
maxPriorityFeePerGas: maxPriorityFeePerGas,
rateLimit: rateLimit,
),
specific: specific,
),
),
);
final response = await connection.receive();
if (!response.hasEvmGrantCreate()) {
throw Exception(
'Expected EVM grant create response, got ${response.whichPayload()}',
);
}
final result = response.evmGrantCreate;
switch (result.whichResult()) {
case EvmGrantCreateResponse_Result.grantId:
return result.grantId;
case EvmGrantCreateResponse_Result.error:
throw Exception(_describeGrantError(result.error));
case EvmGrantCreateResponse_Result.notSet:
throw Exception('Grant creation returned no result.');
}
}
Future<void> deleteEvmGrant(Connection connection, int grantId) async {
await connection.send(
UserAgentRequest(evmGrantDelete: EvmGrantDeleteRequest(grantId: grantId)),
);
final response = await connection.receive();
if (!response.hasEvmGrantDelete()) {
throw Exception(
'Expected EVM grant delete response, got ${response.whichPayload()}',
);
}
final result = response.evmGrantDelete;
switch (result.whichResult()) {
case EvmGrantDeleteResponse_Result.ok:
return;
case EvmGrantDeleteResponse_Result.error:
throw Exception(_describeGrantError(result.error));
case EvmGrantDeleteResponse_Result.notSet:
throw Exception('Grant revoke returned no result.');
}
}
Timestamp _toTimestamp(DateTime value) {
final utc = value.toUtc();
return Timestamp()
..seconds = Int64(utc.millisecondsSinceEpoch ~/ 1000)
..nanos = (utc.microsecondsSinceEpoch % 1000000) * 1000;
}
String _describeGrantError(EvmError error) {
return switch (error) {
EvmError.EVM_ERROR_VAULT_SEALED =>
'The vault is sealed. Unseal it before using EVM grants.',
EvmError.EVM_ERROR_INTERNAL || EvmError.EVM_ERROR_UNSPECIFIED =>
'The server failed to process the EVM grant request.',
_ => 'The server failed to process the EVM grant request.',
};
}

View File

@@ -0,0 +1,62 @@
import 'dart:convert';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:json_annotation/json_annotation.dart';
part 'server_info_storage.g.dart';
@JsonSerializable()
class StoredServerInfo {
const StoredServerInfo({
required this.address,
required this.port,
required this.caCertFingerprint,
});
final String address;
final int port;
final String caCertFingerprint;
factory StoredServerInfo.fromJson(Map<String, dynamic> json) => _$StoredServerInfoFromJson(json);
Map<String, dynamic> toJson() => _$StoredServerInfoToJson(this);
}
abstract class ServerInfoStorage {
Future<StoredServerInfo?> load();
Future<void> save(StoredServerInfo serverInfo);
Future<void> clear();
}
class SecureServerInfoStorage implements ServerInfoStorage {
static const _storageKey = 'server_info';
const SecureServerInfoStorage();
static const _storage = FlutterSecureStorage();
@override
Future<StoredServerInfo?> load() async {
return null;
final rawValue = await _storage.read(key: _storageKey);
if (rawValue == null) {
return null;
}
final decoded = jsonDecode(rawValue) as Map<String, dynamic>;
return StoredServerInfo.fromJson(decoded);
}
@override
Future<void> save(StoredServerInfo serverInfo) {
return _storage.write(
key: _storageKey,
value: jsonEncode(serverInfo.toJson()),
);
}
@override
Future<void> clear() {
return _storage.delete(key: _storageKey);
}
}

View File

@@ -0,0 +1,21 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'server_info_storage.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
StoredServerInfo _$StoredServerInfoFromJson(Map<String, dynamic> json) =>
StoredServerInfo(
address: json['address'] as String,
port: (json['port'] as num).toInt(),
caCertFingerprint: json['caCertFingerprint'] as String,
);
Map<String, dynamic> _$StoredServerInfoToJson(StoredServerInfo instance) =>
<String, dynamic>{
'address': instance.address,
'port': instance.port,
'caCertFingerprint': instance.caCertFingerprint,
};

View File

@@ -0,0 +1,107 @@
import 'package:arbiter/features/connection/connection.dart';
import 'package:arbiter/proto/user_agent.pb.dart';
import 'package:cryptography/cryptography.dart';
const _vaultKeyAssociatedData = 'arbiter.vault.password';
Future<BootstrapResult> bootstrapVault(
Connection connection,
String password,
) async {
final encryptedKey = await _encryptVaultKeyMaterial(connection, password);
await connection.send(
UserAgentRequest(
bootstrapEncryptedKey: BootstrapEncryptedKey(
nonce: encryptedKey.nonce,
ciphertext: encryptedKey.ciphertext,
associatedData: encryptedKey.associatedData,
),
),
);
final response = await connection.receive();
if (!response.hasBootstrapResult()) {
throw Exception(
'Expected bootstrap result, got ${response.whichPayload()}',
);
}
return response.bootstrapResult;
}
Future<UnsealResult> unsealVault(Connection connection, String password) async {
final encryptedKey = await _encryptVaultKeyMaterial(connection, password);
await connection.send(
UserAgentRequest(
unsealEncryptedKey: UnsealEncryptedKey(
nonce: encryptedKey.nonce,
ciphertext: encryptedKey.ciphertext,
associatedData: encryptedKey.associatedData,
),
),
);
final response = await connection.receive();
if (!response.hasUnsealResult()) {
throw Exception('Expected unseal result, got ${response.whichPayload()}');
}
return response.unsealResult;
}
Future<_EncryptedVaultKey> _encryptVaultKeyMaterial(
Connection connection,
String password,
) async {
final keyExchange = X25519();
final cipher = Xchacha20.poly1305Aead();
final clientKeyPair = await keyExchange.newKeyPair();
final clientPublicKey = await clientKeyPair.extractPublicKey();
await connection.send(
UserAgentRequest(unsealStart: UnsealStart(clientPubkey: clientPublicKey.bytes)),
);
final handshakeResponse = await connection.receive();
if (!handshakeResponse.hasUnsealStartResponse()) {
throw Exception(
'Expected unseal handshake response, got ${handshakeResponse.whichPayload()}',
);
}
final serverPublicKey = SimplePublicKey(
handshakeResponse.unsealStartResponse.serverPubkey,
type: KeyPairType.x25519,
);
final sharedSecret = await keyExchange.sharedSecretKey(
keyPair: clientKeyPair,
remotePublicKey: serverPublicKey,
);
final secretBox = await cipher.encrypt(
password.codeUnits,
secretKey: sharedSecret,
nonce: cipher.newNonce(),
aad: _vaultKeyAssociatedData.codeUnits,
);
return _EncryptedVaultKey(
nonce: secretBox.nonce,
ciphertext: [...secretBox.cipherText, ...secretBox.mac.bytes],
associatedData: _vaultKeyAssociatedData.codeUnits,
);
}
class _EncryptedVaultKey {
const _EncryptedVaultKey({
required this.nonce,
required this.ciphertext,
required this.associatedData,
});
final List<int> nonce;
final List<int> ciphertext;
final List<int> associatedData;
}

View File

@@ -0,0 +1,16 @@
enum KeyAlgorithm {
rsa, ecdsa, ed25519
}
// The API to handle without storing the private key in memory.
//The implementation will use platform-specific secure storage and signing capabilities.
abstract class KeyHandle {
KeyAlgorithm get alg;
Future<List<int>> sign(List<int> data);
Future<List<int>> getPublicKey();
}
abstract class KeyManager {
Future<KeyHandle?> get();
Future<KeyHandle> create();
}

View File

@@ -0,0 +1,93 @@
import 'dart:convert';
import 'package:cryptography/cryptography.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:arbiter/features/identity/pk_manager.dart';
final storage = FlutterSecureStorage(
aOptions: AndroidOptions.biometric(
enforceBiometrics: true,
biometricPromptTitle: 'Authentication Required',
),
mOptions: MacOsOptions(
accessibility: KeychainAccessibility.unlocked_this_device,
label: "Arbiter",
description: "Confirm your identity to access vault",
synchronizable: false,
accessControlFlags: [
AccessControlFlag.userPresence,
],
usesDataProtectionKeychain: true,
),
);
final processor = Ed25519();
class SimpleEd25519 extends KeyHandle {
final SimpleKeyPair _keyPair;
SimpleEd25519({required SimpleKeyPair keyPair}) : _keyPair = keyPair;
@override
KeyAlgorithm get alg => KeyAlgorithm.ed25519;
@override
Future<List<int>> getPublicKey() async {
final publicKey = await _keyPair.extractPublicKey();
return publicKey.bytes;
}
@override
Future<List<int>> sign(List<int> data) async {
final signature = await processor.sign(data, keyPair: _keyPair);
return signature.bytes;
}
}
class SimpleEd25519Manager extends KeyManager {
static const _storageKey = "ed25519_identity";
static const _storagePublicKey = "ed25519_public_key";
@override
Future<KeyHandle> create() async {
final storedKey = await get();
if (storedKey != null) {
return storedKey;
}
final newKey = await processor.newKeyPair();
final rawKey = await newKey.extract();
final keyData = base64Encode(rawKey.bytes);
await storage.write(key: _storageKey, value: keyData);
final publicKeyData = base64Encode(rawKey.publicKey.bytes);
await storage.write(key: _storagePublicKey, value: publicKeyData);
return SimpleEd25519(keyPair: newKey);
}
@override
Future<KeyHandle?> get() async {
final storedKeyPair = await storage.read(key: _storageKey);
if (storedKeyPair == null) {
return null;
}
final publicKeyData = await storage.read(key: _storagePublicKey);
final publicKeyRaw = base64Decode(publicKeyData!);
final publicKey = SimplePublicKey(
publicKeyRaw,
type: processor.keyPairType,
);
final keyBytes = base64Decode(storedKeyPair);
final keypair = SimpleKeyPairData(
keyBytes,
publicKey: publicKey,
type: processor.keyPairType,
);
return SimpleEd25519(keyPair: keypair);
}
}

View File

@@ -1,122 +1,35 @@
import 'package:flutter/material.dart';
import 'package:arbiter/router.dart';
import 'package:flutter/material.dart' hide Router;
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:sizer/sizer.dart';
void main() {
runApp(const MyApp());
WidgetsFlutterBinding.ensureInitialized();
runApp(const ProviderScope(child: App()));
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
// This is the theme of your application.
//
// TRY THIS: Try running your application with "flutter run". You'll see
// the application has a purple toolbar. Then, without quitting the app,
// try changing the seedColor in the colorScheme below to Colors.green
// and then invoke "hot reload" (save your changes or press the "hot
// reload" button in a Flutter-supported IDE, or press "r" if you used
// the command line to start the app).
//
// Notice that the counter didn't reset back to zero; the application
// state is not lost during the reload. To reset the state, use hot
// restart instead.
//
// This works for code too, not just values: Most code changes can be
// tested with just a hot reload.
colorScheme: .fromSeed(seedColor: Colors.deepPurple),
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
// This widget is the home page of your application. It is stateful, meaning
// that it has a State object (defined below) that contains fields that affect
// how it looks.
// This class is the configuration for the state. It holds the values (in this
// case the title) provided by the parent (in this case the App widget) and
// used by the build method of the State. Fields in a Widget subclass are
// always marked "final".
final String title;
class App extends StatefulWidget {
const App({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
State<App> createState() => _AppState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
class _AppState extends State<App> {
late final Router _router;
void _incrementCounter() {
setState(() {
// This call to setState tells the Flutter framework that something has
// changed in this State, which causes it to rerun the build method below
// so that the display can reflect the updated values. If we changed
// _counter without calling setState(), then the build method would not be
// called again, and so nothing would appear to happen.
_counter++;
});
@override
void initState() {
super.initState();
_router = Router();
}
@override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
// TRY THIS: Try changing the color here to a specific color (to
// Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
// change color while the other colors stay the same.
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: Column(
// Column is also a layout widget. It takes a list of children and
// arranges them vertically. By default, it sizes itself to fit its
// children horizontally, and tries to be as tall as its parent.
//
// Column has various properties to control how it sizes itself and
// how it positions its children. Here we use mainAxisAlignment to
// center the children vertically; the main axis here is the vertical
// axis because Columns are vertical (the cross axis would be
// horizontal).
//
// TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint"
// action in the IDE, or press "p" in the console), to see the
// wireframe for each widget.
mainAxisAlignment: .center,
children: [
const Text('You have pushed the button this many times:'),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
return Sizer(
builder: (context, orientation, deviceType) {
return MaterialApp.router(routerConfig: _router.config());
},
);
}
}

View File

@@ -0,0 +1,88 @@
// This is a generated file - do not edit.
//
// Generated from arbiter.proto.
// @dart = 3.3
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names
// ignore_for_file: curly_braces_in_flow_control_structures
// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_relative_imports
import 'dart:core' as $core;
import 'package:protobuf/protobuf.dart' as $pb;
export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions;
class ServerInfo extends $pb.GeneratedMessage {
factory ServerInfo({
$core.String? version,
$core.List<$core.int>? certPublicKey,
}) {
final result = create();
if (version != null) result.version = version;
if (certPublicKey != null) result.certPublicKey = certPublicKey;
return result;
}
ServerInfo._();
factory ServerInfo.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory ServerInfo.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'ServerInfo',
package: const $pb.PackageName(_omitMessageNames ? '' : 'arbiter'),
createEmptyInstance: create)
..aOS(1, _omitFieldNames ? '' : 'version')
..a<$core.List<$core.int>>(
2, _omitFieldNames ? '' : 'certPublicKey', $pb.PbFieldType.OY)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
ServerInfo clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
ServerInfo copyWith(void Function(ServerInfo) updates) =>
super.copyWith((message) => updates(message as ServerInfo)) as ServerInfo;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static ServerInfo create() => ServerInfo._();
@$core.override
ServerInfo createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static ServerInfo getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<ServerInfo>(create);
static ServerInfo? _defaultInstance;
@$pb.TagNumber(1)
$core.String get version => $_getSZ(0);
@$pb.TagNumber(1)
set version($core.String value) => $_setString(0, value);
@$pb.TagNumber(1)
$core.bool hasVersion() => $_has(0);
@$pb.TagNumber(1)
void clearVersion() => $_clearField(1);
@$pb.TagNumber(2)
$core.List<$core.int> get certPublicKey => $_getN(1);
@$pb.TagNumber(2)
set certPublicKey($core.List<$core.int> value) => $_setBytes(1, value);
@$pb.TagNumber(2)
$core.bool hasCertPublicKey() => $_has(1);
@$pb.TagNumber(2)
void clearCertPublicKey() => $_clearField(2);
}
const $core.bool _omitFieldNames =
$core.bool.fromEnvironment('protobuf.omit_field_names');
const $core.bool _omitMessageNames =
$core.bool.fromEnvironment('protobuf.omit_message_names');

View File

@@ -0,0 +1,11 @@
// This is a generated file - do not edit.
//
// Generated from arbiter.proto.
// @dart = 3.3
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names
// ignore_for_file: curly_braces_in_flow_control_structures
// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_relative_imports

View File

@@ -0,0 +1,90 @@
// This is a generated file - do not edit.
//
// Generated from arbiter.proto.
// @dart = 3.3
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names
// ignore_for_file: curly_braces_in_flow_control_structures
// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_relative_imports
import 'dart:async' as $async;
import 'dart:core' as $core;
import 'package:grpc/service_api.dart' as $grpc;
import 'package:protobuf/protobuf.dart' as $pb;
import 'client.pb.dart' as $0;
import 'user_agent.pb.dart' as $1;
export 'arbiter.pb.dart';
@$pb.GrpcServiceName('arbiter.ArbiterService')
class ArbiterServiceClient extends $grpc.Client {
/// The hostname for this service.
static const $core.String defaultHost = '';
/// OAuth scopes needed for the client.
static const $core.List<$core.String> oauthScopes = [
'',
];
ArbiterServiceClient(super.channel, {super.options, super.interceptors});
$grpc.ResponseStream<$0.ClientResponse> client(
$async.Stream<$0.ClientRequest> request, {
$grpc.CallOptions? options,
}) {
return $createStreamingCall(_$client, request, options: options);
}
$grpc.ResponseStream<$1.UserAgentResponse> userAgent(
$async.Stream<$1.UserAgentRequest> request, {
$grpc.CallOptions? options,
}) {
return $createStreamingCall(_$userAgent, request, options: options);
}
// method descriptors
static final _$client =
$grpc.ClientMethod<$0.ClientRequest, $0.ClientResponse>(
'/arbiter.ArbiterService/Client',
($0.ClientRequest value) => value.writeToBuffer(),
$0.ClientResponse.fromBuffer);
static final _$userAgent =
$grpc.ClientMethod<$1.UserAgentRequest, $1.UserAgentResponse>(
'/arbiter.ArbiterService/UserAgent',
($1.UserAgentRequest value) => value.writeToBuffer(),
$1.UserAgentResponse.fromBuffer);
}
@$pb.GrpcServiceName('arbiter.ArbiterService')
abstract class ArbiterServiceBase extends $grpc.Service {
$core.String get $name => 'arbiter.ArbiterService';
ArbiterServiceBase() {
$addMethod($grpc.ServiceMethod<$0.ClientRequest, $0.ClientResponse>(
'Client',
client,
true,
true,
($core.List<$core.int> value) => $0.ClientRequest.fromBuffer(value),
($0.ClientResponse value) => value.writeToBuffer()));
$addMethod($grpc.ServiceMethod<$1.UserAgentRequest, $1.UserAgentResponse>(
'UserAgent',
userAgent,
true,
true,
($core.List<$core.int> value) => $1.UserAgentRequest.fromBuffer(value),
($1.UserAgentResponse value) => value.writeToBuffer()));
}
$async.Stream<$0.ClientResponse> client(
$grpc.ServiceCall call, $async.Stream<$0.ClientRequest> request);
$async.Stream<$1.UserAgentResponse> userAgent(
$grpc.ServiceCall call, $async.Stream<$1.UserAgentRequest> request);
}

View File

@@ -0,0 +1,30 @@
// This is a generated file - do not edit.
//
// Generated from arbiter.proto.
// @dart = 3.3
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names
// ignore_for_file: curly_braces_in_flow_control_structures
// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_relative_imports
// ignore_for_file: unused_import
import 'dart:convert' as $convert;
import 'dart:core' as $core;
import 'dart:typed_data' as $typed_data;
@$core.Deprecated('Use serverInfoDescriptor instead')
const ServerInfo$json = {
'1': 'ServerInfo',
'2': [
{'1': 'version', '3': 1, '4': 1, '5': 9, '10': 'version'},
{'1': 'cert_public_key', '3': 2, '4': 1, '5': 12, '10': 'certPublicKey'},
],
};
/// Descriptor for `ServerInfo`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List serverInfoDescriptor = $convert.base64Decode(
'CgpTZXJ2ZXJJbmZvEhgKB3ZlcnNpb24YASABKAlSB3ZlcnNpb24SJgoPY2VydF9wdWJsaWNfa2'
'V5GAIgASgMUg1jZXJ0UHVibGljS2V5');

View File

@@ -0,0 +1,551 @@
// This is a generated file - do not edit.
//
// Generated from client.proto.
// @dart = 3.3
// ignore_for_file: annotate_overrides, camel_case_types, comment_references
// ignore_for_file: constant_identifier_names
// ignore_for_file: curly_braces_in_flow_control_structures
// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes
// ignore_for_file: non_constant_identifier_names, prefer_relative_imports
import 'dart:core' as $core;
import 'package:protobuf/protobuf.dart' as $pb;
import 'client.pbenum.dart';
import 'evm.pb.dart' as $0;
export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions;
export 'client.pbenum.dart';
class AuthChallengeRequest extends $pb.GeneratedMessage {
factory AuthChallengeRequest({
$core.List<$core.int>? pubkey,
}) {
final result = create();
if (pubkey != null) result.pubkey = pubkey;
return result;
}
AuthChallengeRequest._();
factory AuthChallengeRequest.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory AuthChallengeRequest.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'AuthChallengeRequest',
package: const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.client'),
createEmptyInstance: create)
..a<$core.List<$core.int>>(
1, _omitFieldNames ? '' : 'pubkey', $pb.PbFieldType.OY)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
AuthChallengeRequest clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
AuthChallengeRequest copyWith(void Function(AuthChallengeRequest) updates) =>
super.copyWith((message) => updates(message as AuthChallengeRequest))
as AuthChallengeRequest;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static AuthChallengeRequest create() => AuthChallengeRequest._();
@$core.override
AuthChallengeRequest createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static AuthChallengeRequest getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<AuthChallengeRequest>(create);
static AuthChallengeRequest? _defaultInstance;
@$pb.TagNumber(1)
$core.List<$core.int> get pubkey => $_getN(0);
@$pb.TagNumber(1)
set pubkey($core.List<$core.int> value) => $_setBytes(0, value);
@$pb.TagNumber(1)
$core.bool hasPubkey() => $_has(0);
@$pb.TagNumber(1)
void clearPubkey() => $_clearField(1);
}
class AuthChallenge extends $pb.GeneratedMessage {
factory AuthChallenge({
$core.List<$core.int>? pubkey,
$core.int? nonce,
}) {
final result = create();
if (pubkey != null) result.pubkey = pubkey;
if (nonce != null) result.nonce = nonce;
return result;
}
AuthChallenge._();
factory AuthChallenge.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory AuthChallenge.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'AuthChallenge',
package: const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.client'),
createEmptyInstance: create)
..a<$core.List<$core.int>>(
1, _omitFieldNames ? '' : 'pubkey', $pb.PbFieldType.OY)
..aI(2, _omitFieldNames ? '' : 'nonce')
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
AuthChallenge clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
AuthChallenge copyWith(void Function(AuthChallenge) updates) =>
super.copyWith((message) => updates(message as AuthChallenge))
as AuthChallenge;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static AuthChallenge create() => AuthChallenge._();
@$core.override
AuthChallenge createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static AuthChallenge getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<AuthChallenge>(create);
static AuthChallenge? _defaultInstance;
@$pb.TagNumber(1)
$core.List<$core.int> get pubkey => $_getN(0);
@$pb.TagNumber(1)
set pubkey($core.List<$core.int> value) => $_setBytes(0, value);
@$pb.TagNumber(1)
$core.bool hasPubkey() => $_has(0);
@$pb.TagNumber(1)
void clearPubkey() => $_clearField(1);
@$pb.TagNumber(2)
$core.int get nonce => $_getIZ(1);
@$pb.TagNumber(2)
set nonce($core.int value) => $_setSignedInt32(1, value);
@$pb.TagNumber(2)
$core.bool hasNonce() => $_has(1);
@$pb.TagNumber(2)
void clearNonce() => $_clearField(2);
}
class AuthChallengeSolution extends $pb.GeneratedMessage {
factory AuthChallengeSolution({
$core.List<$core.int>? signature,
}) {
final result = create();
if (signature != null) result.signature = signature;
return result;
}
AuthChallengeSolution._();
factory AuthChallengeSolution.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory AuthChallengeSolution.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'AuthChallengeSolution',
package: const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.client'),
createEmptyInstance: create)
..a<$core.List<$core.int>>(
1, _omitFieldNames ? '' : 'signature', $pb.PbFieldType.OY)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
AuthChallengeSolution clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
AuthChallengeSolution copyWith(
void Function(AuthChallengeSolution) updates) =>
super.copyWith((message) => updates(message as AuthChallengeSolution))
as AuthChallengeSolution;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static AuthChallengeSolution create() => AuthChallengeSolution._();
@$core.override
AuthChallengeSolution createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static AuthChallengeSolution getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<AuthChallengeSolution>(create);
static AuthChallengeSolution? _defaultInstance;
@$pb.TagNumber(1)
$core.List<$core.int> get signature => $_getN(0);
@$pb.TagNumber(1)
set signature($core.List<$core.int> value) => $_setBytes(0, value);
@$pb.TagNumber(1)
$core.bool hasSignature() => $_has(0);
@$pb.TagNumber(1)
void clearSignature() => $_clearField(1);
}
class AuthOk extends $pb.GeneratedMessage {
factory AuthOk() => create();
AuthOk._();
factory AuthOk.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory AuthOk.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'AuthOk',
package: const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.client'),
createEmptyInstance: create)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
AuthOk clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
AuthOk copyWith(void Function(AuthOk) updates) =>
super.copyWith((message) => updates(message as AuthOk)) as AuthOk;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static AuthOk create() => AuthOk._();
@$core.override
AuthOk createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static AuthOk getDefault() =>
_defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<AuthOk>(create);
static AuthOk? _defaultInstance;
}
enum ClientRequest_Payload {
authChallengeRequest,
authChallengeSolution,
notSet
}
class ClientRequest extends $pb.GeneratedMessage {
factory ClientRequest({
AuthChallengeRequest? authChallengeRequest,
AuthChallengeSolution? authChallengeSolution,
}) {
final result = create();
if (authChallengeRequest != null)
result.authChallengeRequest = authChallengeRequest;
if (authChallengeSolution != null)
result.authChallengeSolution = authChallengeSolution;
return result;
}
ClientRequest._();
factory ClientRequest.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory ClientRequest.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static const $core.Map<$core.int, ClientRequest_Payload>
_ClientRequest_PayloadByTag = {
1: ClientRequest_Payload.authChallengeRequest,
2: ClientRequest_Payload.authChallengeSolution,
0: ClientRequest_Payload.notSet
};
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'ClientRequest',
package: const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.client'),
createEmptyInstance: create)
..oo(0, [1, 2])
..aOM<AuthChallengeRequest>(
1, _omitFieldNames ? '' : 'authChallengeRequest',
subBuilder: AuthChallengeRequest.create)
..aOM<AuthChallengeSolution>(
2, _omitFieldNames ? '' : 'authChallengeSolution',
subBuilder: AuthChallengeSolution.create)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
ClientRequest clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
ClientRequest copyWith(void Function(ClientRequest) updates) =>
super.copyWith((message) => updates(message as ClientRequest))
as ClientRequest;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static ClientRequest create() => ClientRequest._();
@$core.override
ClientRequest createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static ClientRequest getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<ClientRequest>(create);
static ClientRequest? _defaultInstance;
@$pb.TagNumber(1)
@$pb.TagNumber(2)
ClientRequest_Payload whichPayload() =>
_ClientRequest_PayloadByTag[$_whichOneof(0)]!;
@$pb.TagNumber(1)
@$pb.TagNumber(2)
void clearPayload() => $_clearField($_whichOneof(0));
@$pb.TagNumber(1)
AuthChallengeRequest get authChallengeRequest => $_getN(0);
@$pb.TagNumber(1)
set authChallengeRequest(AuthChallengeRequest value) => $_setField(1, value);
@$pb.TagNumber(1)
$core.bool hasAuthChallengeRequest() => $_has(0);
@$pb.TagNumber(1)
void clearAuthChallengeRequest() => $_clearField(1);
@$pb.TagNumber(1)
AuthChallengeRequest ensureAuthChallengeRequest() => $_ensure(0);
@$pb.TagNumber(2)
AuthChallengeSolution get authChallengeSolution => $_getN(1);
@$pb.TagNumber(2)
set authChallengeSolution(AuthChallengeSolution value) =>
$_setField(2, value);
@$pb.TagNumber(2)
$core.bool hasAuthChallengeSolution() => $_has(1);
@$pb.TagNumber(2)
void clearAuthChallengeSolution() => $_clearField(2);
@$pb.TagNumber(2)
AuthChallengeSolution ensureAuthChallengeSolution() => $_ensure(1);
}
class ClientConnectError extends $pb.GeneratedMessage {
factory ClientConnectError({
ClientConnectError_Code? code,
}) {
final result = create();
if (code != null) result.code = code;
return result;
}
ClientConnectError._();
factory ClientConnectError.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory ClientConnectError.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'ClientConnectError',
package: const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.client'),
createEmptyInstance: create)
..aE<ClientConnectError_Code>(1, _omitFieldNames ? '' : 'code',
enumValues: ClientConnectError_Code.values)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
ClientConnectError clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
ClientConnectError copyWith(void Function(ClientConnectError) updates) =>
super.copyWith((message) => updates(message as ClientConnectError))
as ClientConnectError;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static ClientConnectError create() => ClientConnectError._();
@$core.override
ClientConnectError createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static ClientConnectError getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<ClientConnectError>(create);
static ClientConnectError? _defaultInstance;
@$pb.TagNumber(1)
ClientConnectError_Code get code => $_getN(0);
@$pb.TagNumber(1)
set code(ClientConnectError_Code value) => $_setField(1, value);
@$pb.TagNumber(1)
$core.bool hasCode() => $_has(0);
@$pb.TagNumber(1)
void clearCode() => $_clearField(1);
}
enum ClientResponse_Payload {
authChallenge,
authOk,
evmSignTransaction,
evmAnalyzeTransaction,
clientConnectError,
notSet
}
class ClientResponse extends $pb.GeneratedMessage {
factory ClientResponse({
AuthChallenge? authChallenge,
AuthOk? authOk,
$0.EvmSignTransactionResponse? evmSignTransaction,
$0.EvmAnalyzeTransactionResponse? evmAnalyzeTransaction,
ClientConnectError? clientConnectError,
}) {
final result = create();
if (authChallenge != null) result.authChallenge = authChallenge;
if (authOk != null) result.authOk = authOk;
if (evmSignTransaction != null)
result.evmSignTransaction = evmSignTransaction;
if (evmAnalyzeTransaction != null)
result.evmAnalyzeTransaction = evmAnalyzeTransaction;
if (clientConnectError != null)
result.clientConnectError = clientConnectError;
return result;
}
ClientResponse._();
factory ClientResponse.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory ClientResponse.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static const $core.Map<$core.int, ClientResponse_Payload>
_ClientResponse_PayloadByTag = {
1: ClientResponse_Payload.authChallenge,
2: ClientResponse_Payload.authOk,
3: ClientResponse_Payload.evmSignTransaction,
4: ClientResponse_Payload.evmAnalyzeTransaction,
5: ClientResponse_Payload.clientConnectError,
0: ClientResponse_Payload.notSet
};
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'ClientResponse',
package: const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.client'),
createEmptyInstance: create)
..oo(0, [1, 2, 3, 4, 5])
..aOM<AuthChallenge>(1, _omitFieldNames ? '' : 'authChallenge',
subBuilder: AuthChallenge.create)
..aOM<AuthOk>(2, _omitFieldNames ? '' : 'authOk', subBuilder: AuthOk.create)
..aOM<$0.EvmSignTransactionResponse>(
3, _omitFieldNames ? '' : 'evmSignTransaction',
subBuilder: $0.EvmSignTransactionResponse.create)
..aOM<$0.EvmAnalyzeTransactionResponse>(
4, _omitFieldNames ? '' : 'evmAnalyzeTransaction',
subBuilder: $0.EvmAnalyzeTransactionResponse.create)
..aOM<ClientConnectError>(5, _omitFieldNames ? '' : 'clientConnectError',
subBuilder: ClientConnectError.create)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
ClientResponse clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
ClientResponse copyWith(void Function(ClientResponse) updates) =>
super.copyWith((message) => updates(message as ClientResponse))
as ClientResponse;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static ClientResponse create() => ClientResponse._();
@$core.override
ClientResponse createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static ClientResponse getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<ClientResponse>(create);
static ClientResponse? _defaultInstance;
@$pb.TagNumber(1)
@$pb.TagNumber(2)
@$pb.TagNumber(3)
@$pb.TagNumber(4)
@$pb.TagNumber(5)
ClientResponse_Payload whichPayload() =>
_ClientResponse_PayloadByTag[$_whichOneof(0)]!;
@$pb.TagNumber(1)
@$pb.TagNumber(2)
@$pb.TagNumber(3)
@$pb.TagNumber(4)
@$pb.TagNumber(5)
void clearPayload() => $_clearField($_whichOneof(0));
@$pb.TagNumber(1)
AuthChallenge get authChallenge => $_getN(0);
@$pb.TagNumber(1)
set authChallenge(AuthChallenge value) => $_setField(1, value);
@$pb.TagNumber(1)
$core.bool hasAuthChallenge() => $_has(0);
@$pb.TagNumber(1)
void clearAuthChallenge() => $_clearField(1);
@$pb.TagNumber(1)
AuthChallenge ensureAuthChallenge() => $_ensure(0);
@$pb.TagNumber(2)
AuthOk get authOk => $_getN(1);
@$pb.TagNumber(2)
set authOk(AuthOk value) => $_setField(2, value);
@$pb.TagNumber(2)
$core.bool hasAuthOk() => $_has(1);
@$pb.TagNumber(2)
void clearAuthOk() => $_clearField(2);
@$pb.TagNumber(2)
AuthOk ensureAuthOk() => $_ensure(1);
@$pb.TagNumber(3)
$0.EvmSignTransactionResponse get evmSignTransaction => $_getN(2);
@$pb.TagNumber(3)
set evmSignTransaction($0.EvmSignTransactionResponse value) =>
$_setField(3, value);
@$pb.TagNumber(3)
$core.bool hasEvmSignTransaction() => $_has(2);
@$pb.TagNumber(3)
void clearEvmSignTransaction() => $_clearField(3);
@$pb.TagNumber(3)
$0.EvmSignTransactionResponse ensureEvmSignTransaction() => $_ensure(2);
@$pb.TagNumber(4)
$0.EvmAnalyzeTransactionResponse get evmAnalyzeTransaction => $_getN(3);
@$pb.TagNumber(4)
set evmAnalyzeTransaction($0.EvmAnalyzeTransactionResponse value) =>
$_setField(4, value);
@$pb.TagNumber(4)
$core.bool hasEvmAnalyzeTransaction() => $_has(3);
@$pb.TagNumber(4)
void clearEvmAnalyzeTransaction() => $_clearField(4);
@$pb.TagNumber(4)
$0.EvmAnalyzeTransactionResponse ensureEvmAnalyzeTransaction() => $_ensure(3);
@$pb.TagNumber(5)
ClientConnectError get clientConnectError => $_getN(4);
@$pb.TagNumber(5)
set clientConnectError(ClientConnectError value) => $_setField(5, value);
@$pb.TagNumber(5)
$core.bool hasClientConnectError() => $_has(4);
@$pb.TagNumber(5)
void clearClientConnectError() => $_clearField(5);
@$pb.TagNumber(5)
ClientConnectError ensureClientConnectError() => $_ensure(4);
}
const $core.bool _omitFieldNames =
$core.bool.fromEnvironment('protobuf.omit_field_names');
const $core.bool _omitMessageNames =
$core.bool.fromEnvironment('protobuf.omit_message_names');

Some files were not shown because too many files have changed in this diff Show More