1 Commits

Author SHA1 Message Date
hdbg
8c4c63f51e WIP: kameo::messages wiring for transport generalization 2026-04-14 15:31:20 +02:00
106 changed files with 2113 additions and 2834 deletions

View File

@@ -100,27 +100,6 @@ diesel migration generate <name> --migration-dir crates/arbiter-server/migration
diesel migration run --migration-dir crates/arbiter-server/migrations diesel migration run --migration-dir crates/arbiter-server/migrations
``` ```
### Code Conventions
**`#[must_use]` Attribute:**
Apply the `#[must_use]` attribute to return types of functions where the return value is critical and should not be accidentally ignored. This is commonly used for:
- Methods that return `bool` indicating success/failure or validation state
- Any function where ignoring the return value indicates a logic error
Do not apply `#[must_use]` redundantly to items (types or functions) that are already annotated with `#[must_use]`.
Example:
```rust
#[must_use]
pub fn verify(&self, nonce: i32, context: &[u8], signature: &Signature) -> bool {
// verification logic
}
```
This forces callers to either use the return value or explicitly ignore it with `let _ = ...;`, preventing silent failures.
## User Agent (Flutter + Rinf at `useragent/`) ## 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. 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.

View File

@@ -100,27 +100,6 @@ diesel migration generate <name> --migration-dir crates/arbiter-server/migration
diesel migration run --migration-dir crates/arbiter-server/migrations diesel migration run --migration-dir crates/arbiter-server/migrations
``` ```
### Code Conventions
**`#[must_use]` Attribute:**
Apply the `#[must_use]` attribute to return types of functions where the return value is critical and should not be accidentally ignored. This is commonly used for:
- Methods that return `bool` indicating success/failure or validation state
- Any function where ignoring the return value indicates a logic error
Do not apply `#[must_use]` redundantly to items (types or functions) that are already annotated with `#[must_use]`.
Example:
```rust
#[must_use]
pub fn verify(&self, nonce: i32, context: &[u8], signature: &Signature) -> bool {
// verification logic
}
```
This forces callers to either use the return value or explicitly ignore it with `let _ = ...;`, preventing silent failures.
## User Agent (Flutter + Rinf at `useragent/`) ## 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. 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.

View File

@@ -29,23 +29,38 @@ flowchart TD
A([Client connects]) --> B[Receive AuthChallengeRequest] A([Client connects]) --> B[Receive AuthChallengeRequest]
B --> C{pubkey in DB?} B --> C{pubkey in DB?}
C -- yes --> G[Generate AuthChallenge] C -- yes --> D[Read nonce\nIncrement nonce in DB]
D --> G
C -- no --> E[Ask all UserAgents:\nClientConnectionRequest] C -- no --> E[Ask all UserAgents:\nClientConnectionRequest]
E --> F{First response} E --> F{First response}
F -- denied --> Z([Reject connection]) F -- denied --> Z([Reject connection])
F -- approved --> F2[Cancel remaining\nUserAgent requests] F -- approved --> F2[Cancel remaining\nUserAgent requests]
F2 --> F3[INSERT client] F2 --> F3[INSERT client\nnonce = 1]
F3 --> G F3 --> G[Send AuthChallenge\nwith nonce]
G --> H[Send AuthChallenge\ntimestamp + random bytes] G --> H[Receive AuthChallengeSolution]
H --> I[Receive AuthChallengeSolution] H --> I{Signature valid?}
I --> K{Signature valid?} I -- no --> Z
K -- no --> Z I -- yes --> J([Session started])
K -- yes --> J([Session started])
``` ```
Auth challenges are generated from fresh random bytes plus a timestamp. They are signed as the canonical challenge payload and are not persisted in `program_client`. ### 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.
--- ---

View File

@@ -4,7 +4,7 @@
"cargo:cargo-vet" = "0.10.2" "cargo:cargo-vet" = "0.10.2"
flutter = "3.38.9-stable" flutter = "3.38.9-stable"
protoc = "29.6" protoc = "29.6"
"rust" = {version = "1.93.0", components = "clippy,rust-analyzer"} "rust" = {version = "1.93.0", components = "clippy"}
"cargo:cargo-features-manager" = "0.11.1" "cargo:cargo-features-manager" = "0.11.1"
"cargo:cargo-nextest" = "0.9.126" "cargo:cargo-nextest" = "0.9.126"
"cargo:cargo-shear" = "latest" "cargo:cargo-shear" = "latest"

View File

@@ -10,8 +10,8 @@ message AuthChallengeRequest {
} }
message AuthChallenge { message AuthChallenge {
uint64 timestamp_nanos = 1; bytes pubkey = 1;
bytes random = 2; int32 nonce = 2;
} }
message AuthChallengeSolution { message AuthChallengeSolution {

View File

@@ -8,8 +8,7 @@ message AuthChallengeRequest {
} }
message AuthChallenge { message AuthChallenge {
uint64 timestamp_nanos = 1; int32 nonce = 1;
bytes random = 2;
} }
message AuthChallengeSolution { message AuthChallengeSolution {

206
server/Cargo.lock generated
View File

@@ -44,9 +44,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]] [[package]]
name = "alloy" name = "alloy"
version = "2.0.0" version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85805c194576017df6c11057504e1d60b36f3913f8e365945486931f6ee81e40" checksum = "50ab0cd8afe573d1f7dc2353698a51b1f93aec362c8211e28cfd3948c6adba39"
dependencies = [ dependencies = [
"alloy-consensus", "alloy-consensus",
"alloy-contract", "alloy-contract",
@@ -78,9 +78,9 @@ dependencies = [
[[package]] [[package]]
name = "alloy-consensus" name = "alloy-consensus"
version = "2.0.0" version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8dbe4e5e9107bf6854e7550b666ca654ff2027eabf8153913e2e31ac4b089779" checksum = "7f16daaf7e1f95f62c6c3bf8a3fc3d78b08ae9777810c0bb5e94966c7cd57ef0"
dependencies = [ dependencies = [
"alloy-eips", "alloy-eips",
"alloy-primitives", "alloy-primitives",
@@ -95,7 +95,7 @@ dependencies = [
"either", "either",
"k256", "k256",
"once_cell", "once_cell",
"rand 0.8.6", "rand 0.8.5",
"secp256k1", "secp256k1",
"serde", "serde",
"serde_json", "serde_json",
@@ -105,9 +105,9 @@ dependencies = [
[[package]] [[package]]
name = "alloy-consensus-any" name = "alloy-consensus-any"
version = "2.0.0" version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88fc7bbfb98cf5605a35aadf0ba43a7d9f1608d6f220d05e4fbd5144d3b0b625" checksum = "118998d9015332ab1b4720ae1f1e3009491966a0349938a1f43ff45a8a4c6299"
dependencies = [ dependencies = [
"alloy-consensus", "alloy-consensus",
"alloy-eips", "alloy-eips",
@@ -119,9 +119,9 @@ dependencies = [
[[package]] [[package]]
name = "alloy-contract" name = "alloy-contract"
version = "2.0.0" version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4c16fa30b623e40a5b216da00f3b61870f5cbe863b59816ac1ecc2489515a40" checksum = "7ac9e0c34dc6bce643b182049cdfcca1b8ce7d9c260cbdd561f511873b7e26cd"
dependencies = [ dependencies = [
"alloy-consensus", "alloy-consensus",
"alloy-dyn-abi", "alloy-dyn-abi",
@@ -221,9 +221,9 @@ dependencies = [
[[package]] [[package]]
name = "alloy-eips" name = "alloy-eips"
version = "2.0.0" version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afb4919fa34b268842f434bfafa9c09136ab7b1a87ce0dd40a61befa35b5408c" checksum = "e6ef28c9fdad22d4eec52d894f5f2673a0895f1e5ef196734568e68c0f6caca8"
dependencies = [ dependencies = [
"alloy-eip2124", "alloy-eip2124",
"alloy-eip2930", "alloy-eip2930",
@@ -244,9 +244,9 @@ dependencies = [
[[package]] [[package]]
name = "alloy-genesis" name = "alloy-genesis"
version = "2.0.0" version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e111e22c1a2133e9ebfd9051ea0eaf63559594d2f50d43cbc6762fbb95fc3c2" checksum = "bbf9480307b09d22876efb67d30cadd9013134c21f3a17ec9f93fd7536d38024"
dependencies = [ dependencies = [
"alloy-eips", "alloy-eips",
"alloy-primitives", "alloy-primitives",
@@ -271,9 +271,9 @@ dependencies = [
[[package]] [[package]]
name = "alloy-json-rpc" name = "alloy-json-rpc"
version = "2.0.0" version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31b6af6f374c1eeef8ab8dc26232cd440db167322a4207a3debd3d1ee565ca47" checksum = "422d110f1c40f1f8d0e5562b0b649c35f345fccb7093d9f02729943dcd1eef71"
dependencies = [ dependencies = [
"alloy-primitives", "alloy-primitives",
"alloy-sol-types", "alloy-sol-types",
@@ -286,9 +286,9 @@ dependencies = [
[[package]] [[package]]
name = "alloy-network" name = "alloy-network"
version = "2.0.0" version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0a3f5a7f3678b71d33fcc45b714fab8928dbc647d5aff2145e72032d5c849bb" checksum = "7197a66d94c4de1591cdc16a9bcea5f8cccd0da81b865b49aef97b1b4016e0fa"
dependencies = [ dependencies = [
"alloy-consensus", "alloy-consensus",
"alloy-consensus-any", "alloy-consensus-any",
@@ -312,9 +312,9 @@ dependencies = [
[[package]] [[package]]
name = "alloy-network-primitives" name = "alloy-network-primitives"
version = "2.0.0" version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb50dc1fb0e0b2c8748d5bee1aa7acdd18f9e036311bc93a71d97be624030317" checksum = "eb82711d59a43fdfd79727c99f270b974c784ec4eb5728a0d0d22f26716c87ef"
dependencies = [ dependencies = [
"alloy-consensus", "alloy-consensus",
"alloy-eips", "alloy-eips",
@@ -352,9 +352,9 @@ dependencies = [
[[package]] [[package]]
name = "alloy-provider" name = "alloy-provider"
version = "2.0.0" version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2ba5468f78c8893be2d68a7f2fda61753336e5653f006af19781001b5f99e6c" checksum = "bf6b18b929ef1d078b834c3631e9c925177f3b23ddc6fa08a722d13047205876"
dependencies = [ dependencies = [
"alloy-chains", "alloy-chains",
"alloy-consensus", "alloy-consensus",
@@ -413,9 +413,9 @@ dependencies = [
[[package]] [[package]]
name = "alloy-rpc-client" name = "alloy-rpc-client"
version = "2.0.0" version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "222fd4efff0fb9a25184684742c44fe9fa9a16c4ab5bf97583e71c86598ef8f0" checksum = "94fcc9604042ca80bd37aa5e232ea1cd851f337e31e2babbbb345bc0b1c30de3"
dependencies = [ dependencies = [
"alloy-json-rpc", "alloy-json-rpc",
"alloy-primitives", "alloy-primitives",
@@ -436,9 +436,9 @@ dependencies = [
[[package]] [[package]]
name = "alloy-rpc-types" name = "alloy-rpc-types"
version = "2.0.0" version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "974df1e56405c27cb8242381f45d8b212ba9df5006046ccf704764a2a4634366" checksum = "4faad925d3a669ffc15f43b3deec7fbdf2adeb28a4d6f9cf4bc661698c0f8f4b"
dependencies = [ dependencies = [
"alloy-primitives", "alloy-primitives",
"alloy-rpc-types-eth", "alloy-rpc-types-eth",
@@ -448,24 +448,20 @@ dependencies = [
[[package]] [[package]]
name = "alloy-rpc-types-any" name = "alloy-rpc-types-any"
version = "2.0.0" version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "949c0f16a94ae33cdb1139b8dbf9e34d7f26ebfe97962e2a4d620b5f65f48fe4" checksum = "3823026d1ed239a40f12364fac50726c8daf1b6ab8077a97212c5123910429ed"
dependencies = [ dependencies = [
"alloy-consensus-any", "alloy-consensus-any",
"alloy-network-primitives",
"alloy-primitives",
"alloy-rpc-types-eth", "alloy-rpc-types-eth",
"alloy-serde", "alloy-serde",
"serde",
"serde_json",
] ]
[[package]] [[package]]
name = "alloy-rpc-types-eth" name = "alloy-rpc-types-eth"
version = "2.0.0" version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc280a41931bd419af86e9e859dd9726b73313aaa2e479b33c0e344f4b892ddb" checksum = "59c095f92c4e1ff4981d89e9aa02d5f98c762a1980ab66bec49c44be11349da2"
dependencies = [ dependencies = [
"alloy-consensus", "alloy-consensus",
"alloy-consensus-any", "alloy-consensus-any",
@@ -484,9 +480,9 @@ dependencies = [
[[package]] [[package]]
name = "alloy-serde" name = "alloy-serde"
version = "2.0.0" version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4848831ff994c88b1c32b7df9c4c1c3eedea4b535bde5eb3c421ef0bdc5ac052" checksum = "11ece63b89294b8614ab3f483560c08d016930f842bf36da56bf0b764a15c11e"
dependencies = [ dependencies = [
"alloy-primitives", "alloy-primitives",
"serde", "serde",
@@ -495,9 +491,9 @@ dependencies = [
[[package]] [[package]]
name = "alloy-signer" name = "alloy-signer"
version = "2.0.0" version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84b8ad9890b212e224291024b1aecfeef72127d27a2f6eebc5e347c40275c4bf" checksum = "43f447aefab0f1c0649f71edc33f590992d4e122bc35fb9cdbbf67d4421ace85"
dependencies = [ dependencies = [
"alloy-primitives", "alloy-primitives",
"async-trait", "async-trait",
@@ -510,9 +506,9 @@ dependencies = [
[[package]] [[package]]
name = "alloy-signer-local" name = "alloy-signer-local"
version = "2.0.0" version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c67d2372aada343130d41e249b59a3cef29b1678dcd3fd80f1c2c4d6b5318f2" checksum = "f721f4bf2e4812e5505aaf5de16ef3065a8e26b9139ac885862d00b5a55a659a"
dependencies = [ dependencies = [
"alloy-consensus", "alloy-consensus",
"alloy-network", "alloy-network",
@@ -520,7 +516,7 @@ dependencies = [
"alloy-signer", "alloy-signer",
"async-trait", "async-trait",
"k256", "k256",
"rand 0.8.6", "rand 0.8.5",
"thiserror 2.0.18", "thiserror 2.0.18",
] ]
@@ -599,9 +595,9 @@ dependencies = [
[[package]] [[package]]
name = "alloy-transport" name = "alloy-transport"
version = "2.0.0" version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32b7b755e64ae6b5de0d762ed2c780e072167ea5e542076a559e00314352a0bf" checksum = "8098f965442a9feb620965ba4b4be5e2b320f4ec5a3fff6bfa9e1ff7ef42bed1"
dependencies = [ dependencies = [
"alloy-json-rpc", "alloy-json-rpc",
"auto_impl", "auto_impl",
@@ -622,9 +618,9 @@ dependencies = [
[[package]] [[package]]
name = "alloy-transport-http" name = "alloy-transport-http"
version = "2.0.0" version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a29980e69119444ed26b75e7ee5bed2043870f904a64318297e55800db686564" checksum = "e8597d36d546e1dab822345ad563243ec3920e199322cb554ce56c8ef1a1e2e7"
dependencies = [ dependencies = [
"alloy-json-rpc", "alloy-json-rpc",
"alloy-transport", "alloy-transport",
@@ -654,9 +650,9 @@ dependencies = [
[[package]] [[package]]
name = "alloy-tx-macros" name = "alloy-tx-macros"
version = "2.0.0" version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d8228b9236479ff16b03041b64b86c2bd4e53da1caa45d59b5868cd1571131e" checksum = "d69722eddcdf1ce096c3ab66cf8116999363f734eb36fe94a148f4f71c85da84"
dependencies = [ dependencies = [
"darling 0.23.0", "darling 0.23.0",
"proc-macro2", "proc-macro2",
@@ -687,7 +683,6 @@ dependencies = [
"arbiter-crypto", "arbiter-crypto",
"arbiter-proto", "arbiter-proto",
"async-trait", "async-trait",
"chrono",
"http", "http",
"rand 0.10.1", "rand 0.10.1",
"rustls-webpki", "rustls-webpki",
@@ -701,26 +696,13 @@ dependencies = [
name = "arbiter-crypto" name = "arbiter-crypto"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"alloy", "base64",
"chrono",
"hmac 0.13.0",
"memsafe", "memsafe",
"ml-dsa", "ml-dsa",
"rand 0.10.1", "rand 0.10.1",
"thiserror 2.0.18",
"x-wing", "x-wing",
] ]
[[package]]
name = "arbiter-macros"
version = "0.1.0"
dependencies = [
"arbiter-crypto",
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]] [[package]]
name = "arbiter-proto" name = "arbiter-proto"
version = "0.1.0" version = "0.1.0"
@@ -755,7 +737,6 @@ dependencies = [
"alloy", "alloy",
"anyhow", "anyhow",
"arbiter-crypto", "arbiter-crypto",
"arbiter-macros",
"arbiter-proto", "arbiter-proto",
"arbiter-tokens-registry", "arbiter-tokens-registry",
"argon2", "argon2",
@@ -769,7 +750,7 @@ dependencies = [
"ed25519-dalek", "ed25519-dalek",
"fatality", "fatality",
"futures", "futures",
"hmac 0.13.0", "hmac",
"insta", "insta",
"k256", "k256",
"kameo", "kameo",
@@ -787,9 +768,9 @@ dependencies = [
"rustls", "rustls",
"secrecy", "secrecy",
"serde_with", "serde_with",
"sha2 0.11.0", "sha2 0.10.9",
"smlang", "smlang",
"spki 0.8.0", "spki 0.7.3",
"strum 0.28.0", "strum 0.28.0",
"subtle", "subtle",
"test-log", "test-log",
@@ -989,7 +970,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1df2c09229cbc5a028b1d70e00fdb2acee28b1055dfb5ca73eea49c5a25c4e7c" checksum = "1df2c09229cbc5a028b1d70e00fdb2acee28b1055dfb5ca73eea49c5a25c4e7c"
dependencies = [ dependencies = [
"num-traits", "num-traits",
"rand 0.8.6", "rand 0.8.5",
] ]
[[package]] [[package]]
@@ -999,7 +980,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185"
dependencies = [ dependencies = [
"num-traits", "num-traits",
"rand 0.8.6", "rand 0.8.5",
] ]
[[package]] [[package]]
@@ -1009,7 +990,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a"
dependencies = [ dependencies = [
"num-traits", "num-traits",
"rand 0.8.6", "rand 0.8.5",
] ]
[[package]] [[package]]
@@ -1115,9 +1096,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]] [[package]]
name = "aws-lc-rs" name = "aws-lc-rs"
version = "1.16.3" version = "1.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f" checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc"
dependencies = [ dependencies = [
"aws-lc-sys", "aws-lc-sys",
"untrusted 0.7.1", "untrusted 0.7.1",
@@ -1126,9 +1107,9 @@ dependencies = [
[[package]] [[package]]
name = "aws-lc-sys" name = "aws-lc-sys"
version = "0.40.0" version = "0.39.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7" checksum = "83a25cf98105baa966497416dbd42565ce3a8cf8dbfd59803ec9ad46f3126399"
dependencies = [ dependencies = [
"cc", "cc",
"cmake", "cmake",
@@ -1266,9 +1247,9 @@ dependencies = [
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.11.1" version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
[[package]] [[package]]
name = "bitvec" name = "bitvec"
@@ -1539,12 +1520,11 @@ checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
[[package]] [[package]]
name = "const_format" name = "const_format"
version = "0.2.36" version = "0.2.35"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e" checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad"
dependencies = [ dependencies = [
"const_format_proc_macros", "const_format_proc_macros",
"konst",
] ]
[[package]] [[package]]
@@ -1982,9 +1962,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c"
dependencies = [ dependencies = [
"block-buffer 0.12.0", "block-buffer 0.12.0",
"const-oid 0.10.2",
"crypto-common 0.2.1", "crypto-common 0.2.1",
"ctutils",
] ]
[[package]] [[package]]
@@ -2263,7 +2241,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534"
dependencies = [ dependencies = [
"byteorder", "byteorder",
"rand 0.8.6", "rand 0.8.5",
"rustc-hex", "rustc-hex",
"static_assertions", "static_assertions",
] ]
@@ -2602,15 +2580,6 @@ dependencies = [
"digest 0.10.7", "digest 0.10.7",
] ]
[[package]]
name = "hmac"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f"
dependencies = [
"digest 0.11.2",
]
[[package]] [[package]]
name = "http" name = "http"
version = "1.4.0" version = "1.4.0"
@@ -2691,9 +2660,9 @@ dependencies = [
[[package]] [[package]]
name = "hyper-rustls" name = "hyper-rustls"
version = "0.27.9" version = "0.27.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" checksum = "c2b52f86d1d4bc0d6b4e6826d960b1b333217e07d36b882dca570a5e1c48895b"
dependencies = [ dependencies = [
"http", "http",
"hyper", "hyper",
@@ -3156,21 +3125,6 @@ dependencies = [
"rand_core 0.10.1", "rand_core 0.10.1",
] ]
[[package]]
name = "konst"
version = "0.2.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb"
dependencies = [
"konst_macro_rules",
]
[[package]]
name = "konst_macro_rules"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37"
[[package]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.5.0" version = "1.5.0"
@@ -4107,9 +4061,9 @@ checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
[[package]] [[package]]
name = "rand" name = "rand"
version = "0.8.6" version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [ dependencies = [
"libc", "libc",
"rand_chacha 0.3.1", "rand_chacha 0.3.1",
@@ -4336,7 +4290,7 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2"
dependencies = [ dependencies = [
"hmac 0.12.1", "hmac",
"subtle", "subtle",
] ]
@@ -4422,7 +4376,7 @@ dependencies = [
"parity-scale-codec", "parity-scale-codec",
"primitive-types", "primitive-types",
"proptest", "proptest",
"rand 0.8.6", "rand 0.8.5",
"rand 0.9.4", "rand 0.9.4",
"rlp", "rlp",
"ruint-macro", "ruint-macro",
@@ -4561,9 +4515,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
[[package]] [[package]]
name = "rustls-webpki" name = "rustls-webpki"
version = "0.103.12" version = "0.103.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8279bb85272c9f10811ae6a6c547ff594d6a7f3c6c6b02ee9726d1d0dcfcdd06" checksum = "20a6af516fea4b20eccceaf166e8aa666ac996208e8a644ce3ef5aa783bc7cd4"
dependencies = [ dependencies = [
"aws-lc-rs", "aws-lc-rs",
"ring", "ring",
@@ -4668,7 +4622,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252" checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252"
dependencies = [ dependencies = [
"bitcoin_hashes", "bitcoin_hashes",
"rand 0.8.6", "rand 0.8.5",
"secp256k1-sys", "secp256k1-sys",
"serde", "serde",
] ]
@@ -5341,9 +5295,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.52.1" version = "1.51.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" checksum = "f66bf9585cda4b724d3e78ab34b73fb2bbaba9011b9bfdf69dc836382ea13b8c"
dependencies = [ dependencies = [
"bytes", "bytes",
"libc", "libc",
@@ -5757,9 +5711,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.23.1" version = "1.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",
@@ -5819,11 +5773,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]] [[package]]
name = "wasip2" name = "wasip2"
version = "1.0.3+wasi-0.2.9" version = "1.0.2+wasi-0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
dependencies = [ dependencies = [
"wit-bindgen 0.57.1", "wit-bindgen",
] ]
[[package]] [[package]]
@@ -5832,7 +5786,7 @@ version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
dependencies = [ dependencies = [
"wit-bindgen 0.51.0", "wit-bindgen",
] ]
[[package]] [[package]]
@@ -5960,9 +5914,9 @@ dependencies = [
[[package]] [[package]]
name = "webpki-root-certs" name = "webpki-root-certs"
version = "1.0.7" version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca"
dependencies = [ dependencies = [
"rustls-pki-types", "rustls-pki-types",
] ]
@@ -6306,12 +6260,6 @@ dependencies = [
"wit-bindgen-rust-macro", "wit-bindgen-rust-macro",
] ]
[[package]]
name = "wit-bindgen"
version = "0.57.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
[[package]] [[package]]
name = "wit-bindgen-core" name = "wit-bindgen-core"
version = "0.51.0" version = "0.51.0"

View File

@@ -4,170 +4,48 @@ members = [
] ]
resolver = "3" resolver = "3"
[workspace.lints.clippy]
disallowed-methods = "deny"
[workspace.dependencies] [workspace.dependencies]
alloy = "2.0.0" tonic = { version = "0.14.5", features = [
async-trait = "0.1.89" "deflate",
base64 = "0.22.1" "gzip",
chrono = { version = "0.4.44", features = ["serde"] } "tls-connect-info",
"zstd",
] }
tracing = "0.1.44"
tokio = { version = "1.50.0", features = ["full"] }
ed25519-dalek = { version = "3.0.0-pre.6", features = ["rand_core"] } ed25519-dalek = { version = "3.0.0-pre.6", features = ["rand_core"] }
chrono = { version = "0.4.44", features = ["serde"] }
rand = "0.10.0"
rustls = { version = "0.23.37", features = ["aws-lc-rs", "logging", "prefer-post-quantum", "std"], default-features = false }
smlang = "0.8.0"
thiserror = "2.0.18"
async-trait = "0.1.89"
futures = "0.3.32" futures = "0.3.32"
tokio-stream = { version = "0.1.18", features = ["full"] }
prost-types = { version = "0.14.3", features = ["chrono"] }
x25519-dalek = { version = "2.0.1", features = ["getrandom"] }
rstest = "0.26.1"
rustls-pki-types = "1.14.0"
alloy = "1.7.3"
rcgen = { version = "0.14.7", features = [
"aws_lc_rs",
"pem",
"x509-parser",
"zeroize",
], default-features = false }
k256 = { version = "0.13.4", features = ["ecdsa", "pkcs8"] } k256 = { version = "0.13.4", features = ["ecdsa", "pkcs8"] }
rsa = { version = "0.9", features = ["sha2"] }
sha2 = "0.10"
spki = "0.7"
prost = "0.14.3"
miette = { version = "7.6.0", features = ["fancy", "serde"] }
mutants = "0.0.4"
ml-dsa = { version = "0.1.0-rc.8", features = ["zeroize"] }
base64 = "0.22.1"
kameo = {git = "https://github.com/hdbg/kameo.git", rev = "805b417"} kameo = {git = "https://github.com/hdbg/kameo.git", rev = "805b417"}
kameo_actors = {git = "https://github.com/hdbg/kameo.git", rev = "805b417"} kameo_actors = {git = "https://github.com/hdbg/kameo.git", rev = "805b417"}
hmac = "0.13.0"
miette = { version = "7.6.0", features = ["fancy", "serde"] }
ml-dsa = { version = "0.1.0-rc.8", features = ["zeroize"] }
mutants = "0.0.4"
prost = "0.14.3"
prost-types = { version = "0.14.3", features = ["chrono"] }
rand = "0.10.1"
rcgen = { version = "0.14.7", features = [ "aws_lc_rs", "pem", "x509-parser", "zeroize" ], default-features = false }
rsa = { version = "0.9", features = ["sha2"] }
rstest = "0.26.1"
rustls = { version = "0.23.38", features = ["aws-lc-rs", "logging", "prefer-post-quantum", "std"], default-features = false }
rustls-pki-types = "1.14.0"
sha2 = "0.11"
smlang = "0.8.0"
spki = "0.8"
thiserror = "2.0.18"
tokio = { version = "1.52.1", features = ["full"] }
tokio-stream = { version = "0.1.18", features = ["full"] }
tonic = { version = "0.14.5", features = [ "deflate", "gzip", "tls-connect-info", "zstd" ] }
tracing = "0.1.44"
x25519-dalek = { version = "2.0.1", features = ["getrandom"] }
[workspace.lints.rust]
missing_unsafe_on_extern = "deny"
unsafe_attr_outside_unsafe = "deny"
unsafe_op_in_unsafe_fn = "deny"
unstable_features = "deny"
deprecated_safe_2024 = "warn"
ffi_unwind_calls = "warn"
linker_messages = "warn"
elided_lifetimes_in_paths = "warn"
explicit_outlives_requirements = "warn"
impl-trait-overcaptures = "warn"
impl-trait-redundant-captures = "warn"
redundant_lifetimes = "warn"
single_use_lifetimes = "warn"
unused_lifetimes = "warn"
macro_use_extern_crate = "warn"
redundant_imports = "warn"
unused_import_braces = "warn"
unused_macro_rules = "warn"
unused_qualifications = "warn"
unit_bindings = "warn"
# missing_docs = "warn" # ENABLE BY THE FIRST MAJOR VERSION!!
unnameable_types = "warn"
[workspace.lints.clippy]
derive_partial_eq_without_eq = "allow"
future_not_send = "allow"
inconsistent_struct_constructor = "allow"
inline_always = "allow"
missing_errors_doc = "allow"
missing_fields_in_debug = "allow"
missing_panics_doc = "allow"
must_use_candidate = "allow"
needless_pass_by_ref_mut = "allow"
pub_underscore_fields = "allow"
redundant_pub_crate = "allow"
uninhabited_references = "allow" # safe with unsafe_code = "forbid" and standard uninhabited pattern (match *self {})
# restriction lints
alloc_instead_of_core = "warn"
allow_attributes_without_reason = "warn"
as_conversions = "warn"
assertions_on_result_states = "warn"
cfg_not_test = "warn"
clone_on_ref_ptr = "warn"
cognitive_complexity = "warn"
create_dir = "warn"
dbg_macro = "warn"
decimal_literal_representation = "warn"
default_union_representation = "warn"
deref_by_slicing = "warn"
disallowed_script_idents = "warn"
doc_include_without_cfg = "warn"
empty_drop = "warn"
empty_enum_variants_with_brackets = "warn"
empty_structs_with_brackets = "warn"
exit = "warn"
filetype_is_file = "warn"
float_arithmetic = "warn"
float_cmp_const = "warn"
fn_to_numeric_cast_any = "warn"
get_unwrap = "warn"
if_then_some_else_none = "warn"
indexing_slicing = "warn"
infinite_loop = "warn"
inline_asm_x86_att_syntax = "warn"
inline_asm_x86_intel_syntax = "warn"
integer_division = "warn"
large_include_file = "warn"
lossy_float_literal = "warn"
map_with_unused_argument_over_ranges = "warn"
mem_forget = "warn"
missing_assert_message = "warn"
mixed_read_write_in_expression = "warn"
modulo_arithmetic = "warn"
multiple_unsafe_ops_per_block = "warn"
mutex_atomic = "warn"
mutex_integer = "warn"
needless_raw_strings = "warn"
non_ascii_literal = "warn"
non_zero_suggestions = "warn"
pathbuf_init_then_push = "warn"
pointer_format = "warn"
precedence_bits = "warn"
pub_without_shorthand = "warn"
rc_buffer = "warn"
rc_mutex = "warn"
redundant_test_prefix = "warn"
redundant_type_annotations = "warn"
ref_patterns = "warn"
renamed_function_params = "warn"
rest_pat_in_fully_bound_structs = "warn"
return_and_then = "warn"
semicolon_inside_block = "warn"
str_to_string = "warn"
string_add = "warn"
string_lit_chars_any = "warn"
string_slice = "warn"
suspicious_xor_used_as_pow = "warn"
try_err = "warn"
undocumented_unsafe_blocks = "warn"
uninlined_format_args = "warn"
unnecessary_safety_comment = "warn"
unnecessary_safety_doc = "warn"
unnecessary_self_imports = "warn"
unneeded_field_pattern = "warn"
unused_result_ok = "warn"
verbose_file_reads = "warn"
# cargo lints
negative_feature_names = "warn"
redundant_feature_names = "warn"
wildcard_dependencies = "warn"
# ENABLE BY THE FIRST MAJOR VERSION!!
# todo = "warn"
# unimplemented = "warn"
# panic = "warn"
# panic_in_result_fn = "warn"
#
# cargo_common_metadata = "warn"
# multiple_crate_versions = "warn" # a controversial option since it's really difficult to maintain
disallowed_methods = "deny"
nursery = { level = "warn", priority = -1 }
pedantic = { level = "warn", priority = -1 }
type_repetition_in_bounds = "allow" # sometimes, it's better for readability this way

View File

@@ -7,22 +7,3 @@ disallowed-methods = [
{ 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::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." }, { 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." },
] ]
allow-indexing-slicing-in-tests = true
allow-panic-in-tests = true
check-inconsistent-struct-field-initializers = true
suppress-restriction-lint-in-const = true
allow-renamed-params-for = [
"core::convert::From",
"core::convert::TryFrom",
"core::str::FromStr",
"kameo::actor::Actor",
]
module-items-ordered-within-groupings = ["UPPER_SNAKE_CASE"]
source-item-ordering = ["enum"]
trait-assoc-item-kinds-order = [
"const",
"type",
"fn",
] # community tested standard

View File

@@ -21,7 +21,6 @@ tokio.workspace = true
tokio-stream.workspace = true tokio-stream.workspace = true
thiserror.workspace = true thiserror.workspace = true
http = "1.4.0" http = "1.4.0"
rustls-webpki = { version = "0.103.12", features = ["aws-lc-rs"] } rustls-webpki = { version = "0.103.10", features = ["aws-lc-rs"] }
async-trait.workspace = true async-trait.workspace = true
rand.workspace = true rand.workspace = true
chrono.workspace = true

View File

@@ -1,8 +1,4 @@
use crate::{ use arbiter_crypto::authn::{CLIENT_CONTEXT, SigningKey, format_challenge};
storage::StorageError,
transport::{ClientTransport, next_request_id},
};
use arbiter_crypto::authn::{self, CLIENT_CONTEXT, SigningKey};
use arbiter_proto::{ use arbiter_proto::{
ClientMetadata, ClientMetadata,
proto::{ proto::{
@@ -20,25 +16,27 @@ use arbiter_proto::{
}, },
}; };
use chrono::DateTime; use crate::{
storage::StorageError,
transport::{ClientTransport, next_request_id},
};
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum AuthError { pub enum AuthError {
#[error("Server sent invalid auth challenge")]
InvalidChallenge,
#[error("Client approval denied by User Agent")]
ApprovalDenied,
#[error("Auth challenge was not returned by server")] #[error("Auth challenge was not returned by server")]
MissingAuthChallenge, MissingAuthChallenge,
#[error("Client approval denied by User Agent")]
ApprovalDenied,
#[error("No User Agents online to approve client")] #[error("No User Agents online to approve client")]
NoUserAgentsOnline, NoUserAgentsOnline,
#[error("Signing key storage error")]
Storage(#[from] StorageError),
#[error("Unexpected auth response payload")] #[error("Unexpected auth response payload")]
UnexpectedAuthResponse, UnexpectedAuthResponse,
#[error("Signing key storage error")]
Storage(#[from] StorageError),
} }
fn map_auth_result(code: i32) -> AuthError { fn map_auth_result(code: i32) -> AuthError {
@@ -57,7 +55,7 @@ async fn send_auth_challenge_request(
transport: &mut ClientTransport, transport: &mut ClientTransport,
metadata: ClientMetadata, metadata: ClientMetadata,
key: &SigningKey, key: &SigningKey,
) -> Result<(), AuthError> { ) -> std::result::Result<(), AuthError> {
transport transport
.send(ClientRequest { .send(ClientRequest {
request_id: next_request_id(), request_id: next_request_id(),
@@ -78,7 +76,7 @@ async fn send_auth_challenge_request(
async fn receive_auth_challenge( async fn receive_auth_challenge(
transport: &mut ClientTransport, transport: &mut ClientTransport,
) -> Result<AuthChallenge, AuthError> { ) -> std::result::Result<AuthChallenge, AuthError> {
let response = transport let response = transport
.recv() .recv()
.await .await
@@ -99,16 +97,8 @@ async fn send_auth_challenge_solution(
transport: &mut ClientTransport, transport: &mut ClientTransport,
key: &SigningKey, key: &SigningKey,
challenge: AuthChallenge, challenge: AuthChallenge,
) -> Result<(), AuthError> { ) -> std::result::Result<(), AuthError> {
let timestamp = DateTime::from_timestamp_nanos(challenge.timestamp_nanos as i64); let challenge_payload = format_challenge(challenge.nonce, &challenge.pubkey);
let challenge = authn::AuthChallenge {
nonce: *challenge
.random
.as_array()
.ok_or(AuthError::InvalidChallenge)?,
timestamp,
};
let challenge_payload: Vec<u8> = challenge.format();
let signature = key let signature = key
.sign_message(&challenge_payload, CLIENT_CONTEXT) .sign_message(&challenge_payload, CLIENT_CONTEXT)
.map_err(|_| AuthError::UnexpectedAuthResponse)? .map_err(|_| AuthError::UnexpectedAuthResponse)?
@@ -127,7 +117,9 @@ async fn send_auth_challenge_solution(
.map_err(|_| AuthError::UnexpectedAuthResponse) .map_err(|_| AuthError::UnexpectedAuthResponse)
} }
async fn receive_auth_confirmation(transport: &mut ClientTransport) -> Result<(), AuthError> { async fn receive_auth_confirmation(
transport: &mut ClientTransport,
) -> std::result::Result<(), AuthError> {
let response = transport let response = transport
.recv() .recv()
.await .await
@@ -148,11 +140,11 @@ async fn receive_auth_confirmation(transport: &mut ClientTransport) -> Result<()
} }
} }
pub async fn authenticate( pub(crate) async fn authenticate(
transport: &mut ClientTransport, transport: &mut ClientTransport,
metadata: ClientMetadata, metadata: ClientMetadata,
key: &SigningKey, key: &SigningKey,
) -> Result<(), AuthError> { ) -> std::result::Result<(), AuthError> {
send_auth_challenge_request(transport, metadata, key).await?; send_auth_challenge_request(transport, metadata, key).await?;
let challenge = receive_auth_challenge(transport).await?; let challenge = receive_auth_challenge(transport).await?;
send_auth_challenge_solution(transport, key, challenge).await?; send_auth_challenge_solution(transport, key, challenge).await?;

View File

@@ -1,8 +1,8 @@
use std::io::{self, Write};
use arbiter_client::ArbiterClient; use arbiter_client::ArbiterClient;
use arbiter_proto::{ClientMetadata, url::ArbiterUrl}; use arbiter_proto::{ClientMetadata, url::ArbiterUrl};
use std::io::{self, Write};
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
println!("Testing connection to Arbiter server..."); println!("Testing connection to Arbiter server...");
@@ -29,16 +29,16 @@ async fn main() {
} }
}; };
println!("{url:#?}"); println!("{:#?}", url);
let metadata = ClientMetadata { let metadata = ClientMetadata {
name: "arbiter-client test_connect".to_owned(), name: "arbiter-client test_connect".to_string(),
description: Some("Manual connection smoke test".to_owned()), description: Some("Manual connection smoke test".to_string()),
version: Some(env!("CARGO_PKG_VERSION").to_owned()), version: Some(env!("CARGO_PKG_VERSION").to_string()),
}; };
match ArbiterClient::connect(url, metadata).await { match ArbiterClient::connect(url, metadata).await {
Ok(_) => println!("Connected and authenticated successfully."), Ok(_) => println!("Connected and authenticated successfully."),
Err(err) => eprintln!("Failed to connect: {err:#?}"), Err(err) => eprintln!("Failed to connect: {:#?}", err),
} }
} }

View File

@@ -1,55 +1,50 @@
#[cfg(feature = "evm")] use arbiter_crypto::authn::SigningKey;
use crate::wallets::evm::ArbiterEvmWallet; use arbiter_proto::{
ClientMetadata, proto::arbiter_service_client::ArbiterServiceClient, url::ArbiterUrl,
};
use std::sync::Arc;
use tokio::sync::{Mutex, mpsc};
use tokio_stream::wrappers::ReceiverStream;
use tonic::transport::ClientTlsConfig;
use crate::{ use crate::{
StorageError, StorageError,
auth::{AuthError, authenticate}, auth::{AuthError, authenticate},
storage::{FileSigningKeyStorage, SigningKeyStorage}, storage::{FileSigningKeyStorage, SigningKeyStorage},
transport::{BUFFER_LENGTH, ClientTransport}, transport::{BUFFER_LENGTH, ClientTransport},
}; };
use arbiter_crypto::authn::SigningKey;
use arbiter_proto::{
ClientMetadata, proto::arbiter_service_client::ArbiterServiceClient, url::ArbiterUrl,
};
use std::sync::Arc; #[cfg(feature = "evm")]
use tokio::sync::{Mutex, mpsc}; use crate::wallets::evm::ArbiterEvmWallet;
use tokio_stream::wrappers::ReceiverStream;
use tonic::transport::ClientTlsConfig;
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum ArbiterClientError { pub enum Error {
#[error("Authentication error")] #[error("gRPC error")]
Authentication(#[from] AuthError), Grpc(#[from] tonic::Status),
#[error("Could not establish connection")] #[error("Could not establish connection")]
Connection(#[from] tonic::transport::Error), Connection(#[from] tonic::transport::Error),
#[error("gRPC error")] #[error("Invalid server URI")]
Grpc(#[from] tonic::Status), InvalidUri(#[from] http::uri::InvalidUri),
#[error("Invalid CA certificate")] #[error("Invalid CA certificate")]
InvalidCaCert(#[from] webpki::Error), InvalidCaCert(#[from] webpki::Error),
#[error("Invalid server URI")] #[error("Authentication error")]
InvalidUri(#[from] http::uri::InvalidUri), Authentication(#[from] AuthError),
#[error("Storage error")] #[error("Storage error")]
Storage(#[from] StorageError), Storage(#[from] StorageError),
} }
pub struct ArbiterClient { pub struct ArbiterClient {
#[expect( #[allow(dead_code)]
dead_code,
reason = "transport will be used in future methods for sending requests and receiving responses"
)]
transport: Arc<Mutex<ClientTransport>>, transport: Arc<Mutex<ClientTransport>>,
} }
impl ArbiterClient { impl ArbiterClient {
pub async fn connect( pub async fn connect(url: ArbiterUrl, metadata: ClientMetadata) -> Result<Self, Error> {
url: ArbiterUrl,
metadata: ClientMetadata,
) -> Result<Self, ArbiterClientError> {
let storage = FileSigningKeyStorage::from_default_location()?; let storage = FileSigningKeyStorage::from_default_location()?;
Self::connect_with_storage(url, metadata, &storage).await Self::connect_with_storage(url, metadata, &storage).await
} }
@@ -58,7 +53,7 @@ impl ArbiterClient {
url: ArbiterUrl, url: ArbiterUrl,
metadata: ClientMetadata, metadata: ClientMetadata,
storage: &S, storage: &S,
) -> Result<Self, ArbiterClientError> { ) -> Result<Self, Error> {
let key = storage.load_or_create()?; let key = storage.load_or_create()?;
Self::connect_with_key(url, metadata, key).await Self::connect_with_key(url, metadata, key).await
} }
@@ -67,7 +62,7 @@ impl ArbiterClient {
url: ArbiterUrl, url: ArbiterUrl,
metadata: ClientMetadata, metadata: ClientMetadata,
key: SigningKey, key: SigningKey,
) -> Result<Self, ArbiterClientError> { ) -> Result<Self, Error> {
let anchor = webpki::anchor_from_trusted_cert(&url.ca_cert)?.to_owned(); let anchor = webpki::anchor_from_trusted_cert(&url.ca_cert)?.to_owned();
let tls = ClientTlsConfig::new().trust_anchor(anchor); let tls = ClientTlsConfig::new().trust_anchor(anchor);
@@ -94,8 +89,7 @@ impl ArbiterClient {
} }
#[cfg(feature = "evm")] #[cfg(feature = "evm")]
#[expect(clippy::unused_async, reason = "false positive")] pub async fn evm_wallets(&self) -> Result<Vec<ArbiterEvmWallet>, Error> {
pub async fn evm_wallets(&self) -> Result<Vec<ArbiterEvmWallet>, ArbiterClientError> {
todo!("fetch EVM wallet list from server") todo!("fetch EVM wallet list from server")
} }
} }

View File

@@ -5,7 +5,7 @@ mod transport;
pub mod wallets; pub mod wallets;
pub use auth::AuthError; pub use auth::AuthError;
pub use client::{ArbiterClient, ArbiterClientError}; pub use client::{ArbiterClient, Error};
pub use storage::{FileSigningKeyStorage, SigningKeyStorage, StorageError}; pub use storage::{FileSigningKeyStorage, SigningKeyStorage, StorageError};
#[cfg(feature = "evm")] #[cfg(feature = "evm")]

View File

@@ -1,19 +1,18 @@
use arbiter_crypto::authn::SigningKey; use arbiter_crypto::authn::SigningKey;
use arbiter_proto::home_path; use arbiter_proto::home_path;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum StorageError { pub enum StorageError {
#[error("Invalid signing key length in storage: expected {expected} bytes, got {actual} bytes")]
InvalidKeyLength { expected: usize, actual: usize },
#[error("I/O error")] #[error("I/O error")]
Io(#[from] std::io::Error), Io(#[from] std::io::Error),
#[error("Invalid signing key length in storage: expected {expected} bytes, got {actual} bytes")]
InvalidKeyLength { expected: usize, actual: usize },
} }
pub trait SigningKeyStorage { pub trait SigningKeyStorage {
fn load_or_create(&self) -> Result<SigningKey, StorageError>; fn load_or_create(&self) -> std::result::Result<SigningKey, StorageError>;
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -28,11 +27,11 @@ impl FileSigningKeyStorage {
Self { path: path.into() } Self { path: path.into() }
} }
pub fn from_default_location() -> Result<Self, StorageError> { pub fn from_default_location() -> std::result::Result<Self, StorageError> {
Ok(Self::new(home_path()?.join(Self::DEFAULT_FILE_NAME))) Ok(Self::new(home_path()?.join(Self::DEFAULT_FILE_NAME)))
} }
fn read_key(path: &Path) -> Result<SigningKey, StorageError> { fn read_key(path: &Path) -> std::result::Result<SigningKey, StorageError> {
let bytes = std::fs::read(path)?; let bytes = std::fs::read(path)?;
let raw: [u8; 32] = let raw: [u8; 32] =
bytes bytes
@@ -46,7 +45,7 @@ impl FileSigningKeyStorage {
} }
impl SigningKeyStorage for FileSigningKeyStorage { impl SigningKeyStorage for FileSigningKeyStorage {
fn load_or_create(&self) -> Result<SigningKey, StorageError> { fn load_or_create(&self) -> std::result::Result<SigningKey, StorageError> {
if let Some(parent) = self.path.parent() { if let Some(parent) = self.path.parent() {
std::fs::create_dir_all(parent)?; std::fs::create_dir_all(parent)?;
} }
@@ -126,7 +125,7 @@ mod tests {
assert_eq!(expected, 32); assert_eq!(expected, 32);
assert_eq!(actual, 31); assert_eq!(actual, 31);
} }
other @ StorageError::Io(_) => panic!("unexpected error: {other:?}"), other => panic!("unexpected error: {other:?}"),
} }
std::fs::remove_file(path).expect("temp key file should be removable"); std::fs::remove_file(path).expect("temp key file should be removable");

View File

@@ -1,17 +1,16 @@
use arbiter_proto::proto::client::{ClientRequest, ClientResponse}; use arbiter_proto::proto::client::{ClientRequest, ClientResponse};
use std::sync::atomic::{AtomicI32, Ordering}; use std::sync::atomic::{AtomicI32, Ordering};
use tokio::sync::mpsc; use tokio::sync::mpsc;
pub const BUFFER_LENGTH: usize = 16; pub(crate) const BUFFER_LENGTH: usize = 16;
static NEXT_REQUEST_ID: AtomicI32 = AtomicI32::new(1); static NEXT_REQUEST_ID: AtomicI32 = AtomicI32::new(1);
pub fn next_request_id() -> i32 { pub(crate) fn next_request_id() -> i32 {
NEXT_REQUEST_ID.fetch_add(1, Ordering::Relaxed) NEXT_REQUEST_ID.fetch_add(1, Ordering::Relaxed)
} }
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum ClientSignError { pub(crate) enum ClientSignError {
#[error("Transport channel closed")] #[error("Transport channel closed")]
ChannelClosed, ChannelClosed,
@@ -19,23 +18,27 @@ pub enum ClientSignError {
ConnectionClosed, ConnectionClosed,
} }
pub struct ClientTransport { pub(crate) struct ClientTransport {
pub(crate) sender: mpsc::Sender<ClientRequest>, pub(crate) sender: mpsc::Sender<ClientRequest>,
pub(crate) receiver: tonic::Streaming<ClientResponse>, pub(crate) receiver: tonic::Streaming<ClientResponse>,
} }
impl ClientTransport { impl ClientTransport {
pub(crate) async fn send(&mut self, request: ClientRequest) -> Result<(), ClientSignError> { pub(crate) async fn send(
&mut self,
request: ClientRequest,
) -> std::result::Result<(), ClientSignError> {
self.sender self.sender
.send(request) .send(request)
.await .await
.map_err(|_| ClientSignError::ChannelClosed) .map_err(|_| ClientSignError::ChannelClosed)
} }
pub(crate) async fn recv(&mut self) -> Result<ClientResponse, ClientSignError> { pub(crate) async fn recv(&mut self) -> std::result::Result<ClientResponse, ClientSignError> {
match self.receiver.message().await { match self.receiver.message().await {
Ok(Some(resp)) => Ok(resp), Ok(Some(resp)) => Ok(resp),
Ok(None) | Err(_) => Err(ClientSignError::ConnectionClosed), Ok(None) => Err(ClientSignError::ConnectionClosed),
Err(_) => Err(ClientSignError::ConnectionClosed),
} }
} }
} }

View File

@@ -1,4 +1,13 @@
use crate::transport::{ClientTransport, next_request_id}; use alloy::{
consensus::SignableTransaction,
network::TxSigner,
primitives::{Address, B256, ChainId, Signature},
signers::{Error, Result, Signer},
};
use async_trait::async_trait;
use std::sync::Arc;
use tokio::sync::Mutex;
use arbiter_proto::proto::{ use arbiter_proto::proto::{
client::{ client::{
ClientRequest, ClientRequest,
@@ -16,15 +25,7 @@ use arbiter_proto::proto::{
shared::evm::TransactionEvalError, shared::evm::TransactionEvalError,
}; };
use alloy::{ use crate::transport::{ClientTransport, next_request_id};
consensus::SignableTransaction,
network::TxSigner,
primitives::{Address, B256, ChainId, Signature},
signers::{Error, Result, Signer},
};
use async_trait::async_trait;
use std::sync::Arc;
use tokio::sync::Mutex;
/// A typed error payload returned by [`ArbiterEvmWallet`] transaction signing. /// A typed error payload returned by [`ArbiterEvmWallet`] transaction signing.
/// ///
@@ -58,11 +59,7 @@ pub struct ArbiterEvmWallet {
} }
impl ArbiterEvmWallet { impl ArbiterEvmWallet {
#[expect( pub(crate) fn new(transport: Arc<Mutex<ClientTransport>>, address: Address) -> Self {
dead_code,
reason = "new will be used in future methods for creating wallets with different parameters"
)]
pub(crate) const fn new(transport: Arc<Mutex<ClientTransport>>, address: Address) -> Self {
Self { Self {
transport, transport,
address, address,
@@ -70,12 +67,11 @@ impl ArbiterEvmWallet {
} }
} }
pub const fn address(&self) -> Address { pub fn address(&self) -> Address {
self.address self.address
} }
#[must_use] pub fn with_chain_id(mut self, chain_id: ChainId) -> Self {
pub const fn with_chain_id(mut self, chain_id: ChainId) -> Self {
self.chain_id = Some(chain_id); self.chain_id = Some(chain_id);
self self
} }
@@ -150,7 +146,6 @@ impl TxSigner<Signature> for ArbiterEvmWallet {
.recv() .recv()
.await .await
.map_err(|_| Error::other("failed to receive evm sign transaction response"))?; .map_err(|_| Error::other("failed to receive evm sign transaction response"))?;
drop(transport);
if response.request_id != Some(request_id) { if response.request_id != Some(request_id) {
return Err(Error::other( return Err(Error::other(

View File

@@ -6,17 +6,14 @@ edition = "2024"
[dependencies] [dependencies]
ml-dsa = {workspace = true, optional = true } ml-dsa = {workspace = true, optional = true }
rand = {workspace = true, optional = true} rand = {workspace = true, optional = true}
base64 = {workspace = true, optional = true }
memsafe = {version = "0.4.0", optional = true} memsafe = {version = "0.4.0", optional = true}
hmac.workspace = true
alloy.workspace = true
x-wing = { version = "0.1.0-rc.0", features = ["zeroize"] } x-wing = { version = "0.1.0-rc.0", features = ["zeroize"] }
chrono.workspace = true
thiserror.workspace = true
[lints] [lints]
workspace = true workspace = true
[features] [features]
default = ["authn", "safecell"] default = ["authn", "safecell"]
authn = ["dep:ml-dsa", "dep:rand"] authn = ["dep:ml-dsa", "dep:rand", "dep:base64"]
safecell = ["dep:memsafe"] safecell = ["dep:memsafe"]

View File

@@ -1,65 +1,17 @@
use chrono::{DateTime, Utc}; use std::hash::Hash;
use hmac::digest::Digest;
use base64::{Engine as _, prelude::BASE64_STANDARD};
use ml_dsa::{ use ml_dsa::{
EncodedVerifyingKey, Error, KeyGen, MlDsa87, Seed, Signature as MlDsaSignature, EncodedVerifyingKey, Error, KeyGen, MlDsa87, Seed, Signature as MlDsaSignature,
SigningKey as MlDsaSigningKey, VerifyingKey as MlDsaVerifyingKey, signature::Keypair as _, SigningKey as MlDsaSigningKey, VerifyingKey as MlDsaVerifyingKey, signature::Keypair as _,
}; };
use rand::RngExt;
pub static CLIENT_CONTEXT: &[u8] = b"arbiter_client"; pub static CLIENT_CONTEXT: &[u8] = b"arbiter_client";
pub static USERAGENT_CONTEXT: &[u8] = b"arbiter_user_agent"; pub static USERAGENT_CONTEXT: &[u8] = b"arbiter_user_agent";
const NONCE_SIZE: usize = 32; pub fn format_challenge(nonce: i32, pubkey: &[u8]) -> Vec<u8> {
let concat_form = format!("{}:{}", nonce, BASE64_STANDARD.encode(pubkey));
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] concat_form.into_bytes()
#[error("invalid length: expected {expected} bytes, got {actual} bytes")]
pub struct InvalidLength {
pub expected: usize,
pub actual: usize,
}
#[derive(Debug, Clone)]
pub struct AuthChallenge {
pub nonce: [u8; NONCE_SIZE],
pub timestamp: DateTime<Utc>,
}
impl AuthChallenge {
pub fn generate(rng: &mut impl rand::CryptoRng) -> Self {
let timestamp = Utc::now();
let nonce = {
let mut array = [0; NONCE_SIZE];
rng.fill(&mut array);
array
};
Self { nonce, timestamp }
}
pub fn format(&self) -> Vec<u8> {
{
let mut buffer = Vec::from(self.nonce);
let stamp = self
.timestamp
.timestamp_nanos_opt()
.expect("We would be long dead by the time this triggers :)");
buffer.extend_from_slice(stamp.to_be_bytes().as_slice());
buffer
}
}
pub fn from_parts(nonce: &[u8], timestamp: i64) -> Result<Self, InvalidLength> {
let random_nonce = nonce.as_array().ok_or(InvalidLength {
expected: NONCE_SIZE,
actual: nonce.len(),
})?;
Ok(Self {
nonce: *random_nonce,
timestamp: DateTime::from_timestamp_nanos(timestamp),
})
}
} }
pub type KeyParams = MlDsa87; pub type KeyParams = MlDsa87;
@@ -67,9 +19,9 @@ pub type KeyParams = MlDsa87;
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct PublicKey(Box<MlDsaVerifyingKey<KeyParams>>); pub struct PublicKey(Box<MlDsaVerifyingKey<KeyParams>>);
impl crate::hashing::Hashable for PublicKey { impl Hash for PublicKey {
fn hash<H: Digest>(&self, hasher: &mut H) { fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
hasher.update(self.to_bytes()); self.to_bytes().hash(state);
} }
} }
@@ -81,20 +33,21 @@ pub struct SigningKey(Box<MlDsaSigningKey<KeyParams>>);
impl PublicKey { impl PublicKey {
pub fn to_bytes(&self) -> Vec<u8> { pub fn to_bytes(&self) -> Vec<u8> {
self.0.encode().0.to_vec() self.0.encode().to_vec()
} }
#[must_use] pub fn verify(&self, nonce: i32, context: &[u8], signature: &Signature) -> bool {
pub fn verify(&self, challenge: &AuthChallenge, context: &[u8], signature: &Signature) -> bool { self.0.verify_with_context(
let challenge = challenge.format(); &format_challenge(nonce, &self.to_bytes()),
self.0 context,
.verify_with_context(&challenge, context, &signature.0) &signature.0,
)
} }
} }
impl Signature { impl Signature {
pub fn to_bytes(&self) -> Vec<u8> { pub fn to_bytes(&self) -> Vec<u8> {
self.0.encode().0.to_vec() self.0.encode().to_vec()
} }
} }
@@ -122,14 +75,11 @@ impl SigningKey {
.map(Into::into) .map(Into::into)
} }
pub fn sign_challenge( pub fn sign_challenge(&self, nonce: i32, context: &[u8]) -> Result<Signature, Error> {
&self, self.sign_message(
challenge: &AuthChallenge, &format_challenge(nonce, &self.public_key().to_bytes()),
context: &[u8], context,
) -> Result<Signature, Error> { )
let challenge = challenge.format();
self.sign_message(&challenge, context)
} }
} }
@@ -190,8 +140,6 @@ impl TryFrom<&'_ [u8]> for Signature {
mod tests { mod tests {
use ml_dsa::{KeyGen, MlDsa87, signature::Keypair as _}; use ml_dsa::{KeyGen, MlDsa87, signature::Keypair as _};
use crate::authn::AuthChallenge;
use super::{CLIENT_CONTEXT, PublicKey, Signature, SigningKey, USERAGENT_CONTEXT}; use super::{CLIENT_CONTEXT, PublicKey, Signature, SigningKey, USERAGENT_CONTEXT};
#[test] #[test]
@@ -221,13 +169,13 @@ mod tests {
fn challenge_verification_uses_context_and_canonical_key_bytes() { fn challenge_verification_uses_context_and_canonical_key_bytes() {
let key = SigningKey::generate(); let key = SigningKey::generate();
let public_key = key.public_key(); let public_key = key.public_key();
let challenge = AuthChallenge::generate(&mut rand::rng()); let nonce = 17;
let signature = key let signature = key
.sign_challenge(&challenge, CLIENT_CONTEXT) .sign_challenge(nonce, CLIENT_CONTEXT)
.expect("signature should be created"); .expect("signature should be created");
assert!(public_key.verify(&challenge, CLIENT_CONTEXT, &signature)); assert!(public_key.verify(nonce, CLIENT_CONTEXT, &signature));
assert!(!public_key.verify(&challenge, USERAGENT_CONTEXT, &signature)); assert!(!public_key.verify(nonce, USERAGENT_CONTEXT, &signature));
} }
#[test] #[test]
@@ -237,16 +185,10 @@ mod tests {
assert_eq!(restored.public_key(), original.public_key()); assert_eq!(restored.public_key(), original.public_key());
let challenge = AuthChallenge::generate(&mut rand::rng());
let signature = restored let signature = restored
.sign_challenge(&challenge, CLIENT_CONTEXT) .sign_challenge(9, CLIENT_CONTEXT)
.expect("signature should be created"); .expect("signature should be created");
assert!( assert!(restored.public_key().verify(9, CLIENT_CONTEXT, &signature));
restored
.public_key()
.verify(&challenge, CLIENT_CONTEXT, &signature)
);
} }
} }

View File

@@ -1,6 +1,6 @@
#[cfg(feature = "authn")] #[cfg(feature = "authn")]
pub mod authn; pub mod authn;
pub mod hashing;
#[cfg(feature = "safecell")] #[cfg(feature = "safecell")]
pub mod safecell; pub mod safecell;

View File

@@ -1,9 +1,7 @@
use std::ops::{Deref, DerefMut};
use std::{any::type_name, fmt};
use memsafe::MemSafe; use memsafe::MemSafe;
use std::{
any::type_name,
fmt,
ops::{Deref, DerefMut},
};
pub trait SafeCellHandle<T> { pub trait SafeCellHandle<T> {
type CellRead<'a>: Deref<Target = T> type CellRead<'a>: Deref<Target = T>
@@ -31,7 +29,7 @@ pub trait SafeCellHandle<T> {
let mut cell = Self::new(T::default()); let mut cell = Self::new(T::default());
{ {
let mut handle = cell.write(); let mut handle = cell.write();
f(&mut *handle); f(handle.deref_mut());
} }
cell cell
} }

View File

@@ -1,18 +0,0 @@
[package]
name = "arbiter-macros"
version = "0.1.0"
edition = "2024"
[lib]
proc-macro = true
[dependencies]
proc-macro2 = "1.0"
quote = "1.0"
syn = { version = "2.0", features = ["derive", "fold", "full", "visit-mut"] }
[dev-dependencies]
arbiter-crypto = { path = "../arbiter-crypto" }
[lints]
workspace = true

View File

@@ -1,131 +0,0 @@
use crate::utils::{HASHABLE_TRAIT_PATH, HMAC_DIGEST_PATH};
use proc_macro2::{Span, TokenStream, TokenTree};
use quote::quote;
use syn::{DataStruct, DeriveInput, Fields, Generics, Index, parse_quote, spanned::Spanned};
pub(crate) fn derive(input: &DeriveInput) -> TokenStream {
match &input.data {
syn::Data::Struct(struct_data) => hashable_struct(input, struct_data),
syn::Data::Enum(_) => {
syn::Error::new_spanned(input, "Hashable can currently be derived only for structs")
.to_compile_error()
}
syn::Data::Union(_) => {
syn::Error::new_spanned(input, "Hashable cannot be derived for unions")
.to_compile_error()
}
}
}
fn hashable_struct(input: &DeriveInput, struct_data: &DataStruct) -> TokenStream {
let ident = &input.ident;
let hashable_trait = HASHABLE_TRAIT_PATH.to_path();
let hmac_digest = HMAC_DIGEST_PATH.to_path();
let generics = add_hashable_bounds(input.generics.clone(), &hashable_trait);
let field_accesses = collect_field_accesses(struct_data);
let hash_calls = build_hash_calls(&field_accesses, &hashable_trait);
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
quote! {
#[automatically_derived]
impl #impl_generics #hashable_trait for #ident #ty_generics #where_clause {
fn hash<H: #hmac_digest>(&self, hasher: &mut H) {
#(#hash_calls)*
}
}
}
}
fn add_hashable_bounds(mut generics: Generics, hashable_trait: &syn::Path) -> Generics {
for type_param in generics.type_params_mut() {
type_param.bounds.push(parse_quote!(#hashable_trait));
}
generics
}
struct FieldAccess {
access: TokenStream,
span: Span,
}
fn collect_field_accesses(struct_data: &DataStruct) -> Vec<FieldAccess> {
match &struct_data.fields {
Fields::Named(fields) => {
// Keep deterministic alphabetical order for named fields.
// Do not remove this sort, because it keeps hash output stable regardless of source order.
let mut named_fields = fields
.named
.iter()
.map(|field| {
let name = field
.ident
.as_ref()
.expect("Fields::Named(fields) must have names")
.clone();
(name.to_string(), name)
})
.collect::<Vec<_>>();
named_fields.sort_by(|a, b| a.0.cmp(&b.0));
named_fields
.into_iter()
.map(|(_, name)| FieldAccess {
access: quote! { #name },
span: name.span(),
})
.collect()
}
Fields::Unnamed(fields) => fields
.unnamed
.iter()
.enumerate()
.map(|(i, field)| FieldAccess {
access: {
let index = Index::from(i);
quote! { #index }
},
span: field.ty.span(),
})
.collect(),
Fields::Unit => Vec::new(),
}
}
fn build_hash_calls(
field_accesses: &[FieldAccess],
hashable_trait: &syn::Path,
) -> Vec<TokenStream> {
field_accesses
.iter()
.map(|field| {
let access = &field.access;
let call = quote! {
#hashable_trait::hash(&self.#access, hasher);
};
respan(call, field.span)
})
.collect()
}
/// Recursively set span on all tokens, including interpolated ones.
fn respan(tokens: TokenStream, span: Span) -> TokenStream {
tokens
.into_iter()
.map(|tt| match tt {
TokenTree::Group(g) => {
let mut new = proc_macro2::Group::new(g.delimiter(), respan(g.stream(), span));
new.set_span(span);
TokenTree::Group(new)
}
mut other => {
other.set_span(span);
other
}
})
.collect()
}

View File

@@ -1,10 +0,0 @@
use syn::{DeriveInput, parse_macro_input};
mod hashable;
mod utils;
#[proc_macro_derive(Hashable)]
pub fn derive_hashable(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input = parse_macro_input!(input as DeriveInput);
hashable::derive(&input).into()
}

View File

@@ -1,24 +0,0 @@
pub(crate) struct ToPath(pub &'static str);
impl ToPath {
pub(crate) fn to_path(&self) -> syn::Path {
syn::parse_str(self.0).expect("Invalid path")
}
}
macro_rules! ensure_path {
($path:path as $name:ident) => {
const _: () = {
#[cfg(test)]
#[expect(
unused_imports,
reason = "Ensures the path is valid and will cause a compile error if not"
)]
use $path as _;
};
pub(crate) const $name: ToPath = ToPath(stringify!($path));
};
}
ensure_path!(::arbiter_crypto::hashing::Hashable as HASHABLE_TRAIT_PATH);
ensure_path!(::arbiter_crypto::hashing::Digest as HMAC_DIGEST_PATH);

View File

@@ -54,10 +54,11 @@
//! as a closed outbound channel. //! as a closed outbound channel.
//! - [`Bi::recv`] returns `None` when the underlying transport closes. //! - [`Bi::recv`] returns `None` when the underlying transport closes.
//! - Message translation is intentionally out of scope for this module. //! - Message translation is intentionally out of scope for this module.
use async_trait::async_trait;
use kameo::{error::Infallible, prelude::*};
use std::marker::PhantomData; use std::marker::PhantomData;
use async_trait::async_trait;
/// Errors returned by transport adapters implementing [`Bi`]. /// Errors returned by transport adapters implementing [`Bi`].
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
pub enum Error { pub enum Error {
@@ -190,29 +191,3 @@ where
} }
pub mod grpc; pub mod grpc;
#[derive(thiserror::Error, Debug)]
pub enum ForwardError<I> {
#[error("Transport error: {0}")]
Transport(#[from] Error),
#[error("Actor delivery error: {0}")]
Actor(SendError<I>),
}
pub async fn forward_to_actor<Transport, Inbound, Outbound, Handler>(
transport: &mut Transport,
actor: &ActorRef<Handler>,
) -> Result<(), ForwardError<Inbound>>
where
Transport: Bi<Inbound, <Outbound as Reply>::Ok>,
Handler: Actor + Message<Inbound, Reply = Outbound>,
Inbound: Send + 'static,
Outbound: Send + 'static + Reply<Error = Infallible>, // `Infallible` to enforce contract that `Outbound` carries handler-level error
{
while let Some(request) = transport.recv().await {
let resp = actor.ask(request).await.map_err(ForwardError::Actor)?;
transport.send(resp).await?
}
Err(Error::ChannelClosed.into())
}

View File

@@ -1,10 +1,10 @@
use super::{Bi, Receiver, Sender};
use async_trait::async_trait; use async_trait::async_trait;
use futures::StreamExt; use futures::StreamExt;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tokio_stream::wrappers::ReceiverStream; use tokio_stream::wrappers::ReceiverStream;
use super::{Bi, Receiver, Sender};
pub struct GrpcSender<Outbound> { pub struct GrpcSender<Outbound> {
tx: mpsc::Sender<Result<Outbound, tonic::Status>>, tx: mpsc::Sender<Result<Outbound, tonic::Status>>,
} }

View File

@@ -1,6 +1,7 @@
use std::fmt::Display;
use base64::{Engine as _, prelude::BASE64_URL_SAFE}; use base64::{Engine as _, prelude::BASE64_URL_SAFE};
use rustls_pki_types::CertificateDer; use rustls_pki_types::CertificateDer;
use std::fmt::Display;
const ARBITER_URL_SCHEME: &str = "arbiter"; const ARBITER_URL_SCHEME: &str = "arbiter";
const CERT_QUERY_KEY: &str = "cert"; const CERT_QUERY_KEY: &str = "cert";
@@ -104,7 +105,7 @@ mod tests {
#[rstest] #[rstest]
fn parsing_correctness( fn test_parsing_correctness(
#[values("127.0.0.1", "localhost", "192.168.1.1", "some.domain.com")] host: &str, #[values("127.0.0.1", "localhost", "192.168.1.1", "some.domain.com")] host: &str,
#[values(None, Some("token123".to_string()))] bootstrap_token: Option<String>, #[values(None, Some("token123".to_string()))] bootstrap_token: Option<String>,

View File

@@ -18,7 +18,6 @@ diesel-async = { version = "0.8.0", features = [
] } ] }
arbiter-proto.path = "../arbiter-proto" arbiter-proto.path = "../arbiter-proto"
arbiter-crypto.path = "../arbiter-crypto" arbiter-crypto.path = "../arbiter-crypto"
arbiter-macros.path = "../arbiter-macros"
tracing.workspace = true tracing.workspace = true
tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tonic.workspace = true tonic.workspace = true
@@ -45,7 +44,7 @@ restructed = "0.2.2"
strum = { version = "0.28.0", features = ["derive"] } strum = { version = "0.28.0", features = ["derive"] }
pem = "3.0.6" pem = "3.0.6"
sha2.workspace = true sha2.workspace = true
hmac.workspace = true hmac = "0.12"
spki.workspace = true spki.workspace = true
alloy.workspace = true alloy.workspace = true
prost-types.workspace = true prost-types.workspace = true
@@ -62,7 +61,7 @@ k256.workspace = true
kameo_actors.workspace = true kameo_actors.workspace = true
[dev-dependencies] [dev-dependencies]
insta = "1.47.2" insta = "1.46.3"
proptest = "1.11.0" proptest = "1.11.0"
rstest.workspace = true rstest.workspace = true
test-log = { version = "0.2", default-features = false, features = ["trace"] } test-log = { version = "0.2", default-features = false, features = ["trace"] }

View File

@@ -45,11 +45,13 @@ insert into arbiter_settings (id) values (1) on conflict do nothing;
create table if not exists useragent_client ( create table if not exists useragent_client (
id integer not null primary key, id integer not null primary key,
nonce integer not null default(1), -- used for auth challenge
public_key blob not null, public_key blob not null,
key_type integer not null default(1),
created_at integer not null default(unixepoch ('now')), created_at integer not null default(unixepoch ('now')),
updated_at integer not null default(unixepoch ('now')) updated_at integer not null default(unixepoch ('now'))
) STRICT; ) STRICT;
create unique index if not exists uniq_useragent_client_public_key on useragent_client (public_key); create unique index if not exists uniq_useragent_client_public_key on useragent_client (public_key, key_type);
create table if not exists client_metadata ( create table if not exists client_metadata (
id integer not null primary key, id integer not null primary key,
@@ -71,6 +73,7 @@ create unique index if not exists uniq_metadata_binding_client on client_metadat
create table if not exists program_client ( create table if not exists program_client (
id integer not null primary key, id integer not null primary key,
nonce integer not null default(1), -- used for auth challenge
public_key blob not null, public_key blob not null,
metadata_id integer not null references client_metadata (id) on delete cascade, metadata_id integer not null references client_metadata (id) on delete cascade,
created_at integer not null default(unixepoch ('now')), created_at integer not null default(unixepoch ('now')),

View File

@@ -1,20 +1,20 @@
use crate::db::{self, DatabasePool, schema};
use arbiter_proto::{BOOTSTRAP_PATH, home_path}; use arbiter_proto::{BOOTSTRAP_PATH, home_path};
use diesel::QueryDsl; use diesel::QueryDsl;
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
use kameo::{Actor, messages}; use kameo::{Actor, messages};
use rand::{RngExt, distr::Alphanumeric, make_rng, rngs::StdRng}; use rand::{RngExt, distr::Alphanumeric, make_rng, rngs::StdRng};
use subtle::ConstantTimeEq as _; use subtle::ConstantTimeEq as _;
use thiserror::Error; use thiserror::Error;
use crate::db::{self, DatabasePool, schema};
const TOKEN_LENGTH: usize = 64; const TOKEN_LENGTH: usize = 64;
pub async fn generate_token() -> Result<String, std::io::Error> { pub async fn generate_token() -> Result<String, std::io::Error> {
let rng: StdRng = make_rng(); let rng: StdRng = make_rng();
let token = rng.sample_iter(Alphanumeric).take(TOKEN_LENGTH).fold( let token: String = rng.sample_iter(Alphanumeric).take(TOKEN_LENGTH).fold(
String::default(), Default::default(),
|mut accum, char| { |mut accum, char| {
accum += char.to_string().as_str(); accum += char.to_string().as_str();
accum accum
@@ -31,11 +31,11 @@ pub enum Error {
#[error("Database error: {0}")] #[error("Database error: {0}")]
Database(#[from] db::PoolError), Database(#[from] db::PoolError),
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("Database query error: {0}")] #[error("Database query error: {0}")]
Query(#[from] diesel::result::Error), Query(#[from] diesel::result::Error),
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
} }
#[derive(Actor)] #[derive(Actor)]
@@ -69,13 +69,16 @@ impl Bootstrapper {
impl Bootstrapper { impl Bootstrapper {
#[message] #[message]
pub fn is_correct_token(&self, token: String) -> bool { pub fn is_correct_token(&self, token: String) -> bool {
self.token.as_ref().is_some_and(|expected| { match &self.token {
Some(expected) => {
let expected_bytes = expected.as_bytes(); let expected_bytes = expected.as_bytes();
let token_bytes = token.as_bytes(); let token_bytes = token.as_bytes();
let choice = expected_bytes.ct_eq(token_bytes); let choice = expected_bytes.ct_eq(token_bytes);
bool::from(choice) bool::from(choice)
}) }
None => false,
}
} }
#[message] #[message]

View File

@@ -1,3 +1,11 @@
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 rand::{SeedableRng, rng, rngs::StdRng};
use crate::{ use crate::{
actors::vault::{CreateNew, Decrypt, Vault}, actors::vault::{CreateNew, Decrypt, Vault},
crypto::integrity, crypto::integrity,
@@ -16,16 +24,6 @@ use crate::{
}; };
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _}; use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
use alloy::{
consensus::TxEip1559, network::TxSignerSync as _, 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 rand::{SeedableRng, rng, rngs::StdRng};
pub use crate::evm::safe_signer; pub use crate::evm::safe_signer;
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
@@ -160,7 +158,6 @@ impl EvmActor {
} }
#[message] #[message]
#[expect(clippy::unused_async, reason = "reserved for impl")]
pub async fn useragent_delete_grant(&mut self, _grant_id: i32) -> Result<(), Error> { pub async fn useragent_delete_grant(&mut self, _grant_id: i32) -> Result<(), Error> {
// let mut conn = self.db.get().await.map_err(DatabaseError::from)?; // let mut conn = self.db.get().await.map_err(DatabaseError::from)?;
// let vault = self.vault.clone(); // let vault = self.vault.clone();
@@ -270,6 +267,7 @@ impl EvmActor {
.evaluate_transaction(wallet_access, transaction.clone(), RunKind::Execution) .evaluate_transaction(wallet_access, transaction.clone(), RunKind::Execution)
.await?; .await?;
use alloy::network::TxSignerSync as _;
Ok(signer.sign_transaction_sync(&mut transaction)?) Ok(signer.sign_transaction_sync(&mut transaction)?)
} }
} }

View File

@@ -1,3 +1,11 @@
use std::ops::ControlFlow;
use kameo::{
Actor, messages,
prelude::{ActorId, ActorRef, ActorStopReason, Context, WeakActorRef},
reply::ReplySender,
};
use crate::{ use crate::{
actors::flow_coordinator::ApprovalError, actors::flow_coordinator::ApprovalError,
peers::{ peers::{
@@ -6,13 +14,6 @@ use crate::{
}, },
}; };
use kameo::{
Actor, messages,
prelude::{ActorId, ActorRef, ActorStopReason, Context, WeakActorRef},
reply::ReplySender,
};
use std::ops::ControlFlow;
pub struct Args { pub struct Args {
pub client: ClientProfile, pub client: ClientProfile,
pub user_agents: Vec<ActorRef<UserAgentSession>>, pub user_agents: Vec<ActorRef<UserAgentSession>>,
@@ -42,7 +43,7 @@ impl Actor for ClientApprovalController {
async fn on_start( async fn on_start(
Args { Args {
client, client,
user_agents, mut user_agents,
reply, reply,
}: Self::Args, }: Self::Args,
actor_ref: ActorRef<Self>, actor_ref: ActorRef<Self>,
@@ -53,9 +54,8 @@ impl Actor for ClientApprovalController {
reply: Some(reply), reply: Some(reply),
}; };
for user_agent in user_agents { for user_agent in user_agents.drain(..) {
actor_ref.link(&user_agent).await; actor_ref.link(&user_agent).await;
let _ = user_agent let _ = user_agent
.tell(BeginNewClientApproval { .tell(BeginNewClientApproval {
client: client.clone(), client: client.clone(),
@@ -87,7 +87,7 @@ impl Actor for ClientApprovalController {
#[messages] #[messages]
impl ClientApprovalController { impl ClientApprovalController {
#[message(ctx)] #[message(ctx)]
pub fn client_approval_answer(&mut self, approved: bool, ctx: &mut Context<Self, ()>) { pub async fn client_approval_answer(&mut self, approved: bool, ctx: &mut Context<Self, ()>) {
if !approved { if !approved {
// Denial wins immediately regardless of other pending responses. // Denial wins immediately regardless of other pending responses.
self.send_reply(Ok(false)); self.send_reply(Ok(false));

View File

@@ -1,10 +1,4 @@
use crate::{ use std::{collections::HashMap, ops::ControlFlow};
actors::{
flow_coordinator::client_connect_approval::ClientApprovalController,
useragent_registry::{GetConnected, UserAgentRegistry},
},
peers::client::{ClientProfile, session::ClientSession},
};
use kameo::{ use kameo::{
Actor, Actor,
@@ -13,23 +7,22 @@ use kameo::{
prelude::{ActorStopReason, Context, WeakActorRef}, prelude::{ActorStopReason, Context, WeakActorRef},
reply::DelegatedReply, reply::DelegatedReply,
}; };
use std::{collections::HashMap, ops::ControlFlow};
use tracing::info; use tracing::info;
use crate::{
actors::flow_coordinator::client_connect_approval::ClientApprovalController,
peers::{
client::{ClientProfile, session::ClientSession},
user_agent::UserAgentSession,
},
};
pub mod client_connect_approval; pub mod client_connect_approval;
#[derive(Default)]
pub struct FlowCoordinator { pub struct FlowCoordinator {
pub user_agents: HashMap<ActorId, ActorRef<UserAgentSession>>,
pub clients: HashMap<ActorId, ActorRef<ClientSession>>, pub clients: HashMap<ActorId, ActorRef<ClientSession>>,
useragent_registry: ActorRef<UserAgentRegistry>,
}
impl FlowCoordinator {
pub fn new(useragent_registry: ActorRef<UserAgentRegistry>) -> Self {
Self {
clients: HashMap::default(),
useragent_registry,
}
}
} }
impl Actor for FlowCoordinator { impl Actor for FlowCoordinator {
@@ -47,7 +40,13 @@ impl Actor for FlowCoordinator {
id: ActorId, id: ActorId,
_: ActorStopReason, _: ActorStopReason,
) -> Result<ControlFlow<ActorStopReason>, Self::Error> { ) -> Result<ControlFlow<ActorStopReason>, Self::Error> {
if self.clients.remove(&id).is_some() { if self.user_agents.remove(&id).is_some() {
info!(
?id,
actor = "FlowCoordinator",
event = "useragent.disconnected"
);
} else if self.clients.remove(&id).is_some() {
info!( info!(
?id, ?id,
actor = "FlowCoordinator", actor = "FlowCoordinator",
@@ -72,6 +71,17 @@ pub enum ApprovalError {
#[messages] #[messages]
impl FlowCoordinator { impl FlowCoordinator {
#[message(ctx)]
pub async fn register_user_agent(
&mut self,
actor: ActorRef<UserAgentSession>,
ctx: &mut Context<Self, ()>,
) {
info!(id = %actor.id(), actor = "FlowCoordinator", event = "useragent.connected");
ctx.actor_ref().link(&actor).await;
self.user_agents.insert(actor.id(), actor);
}
#[message(ctx)] #[message(ctx)]
pub async fn register_client( pub async fn register_client(
&mut self, &mut self,
@@ -93,11 +103,7 @@ impl FlowCoordinator {
unreachable!("Expected `request_client_approval` to have callback channel"); unreachable!("Expected `request_client_approval` to have callback channel");
}; };
let Ok(refs) = self.useragent_registry.ask(GetConnected).await else { let refs: Vec<_> = self.user_agents.values().cloned().collect();
reply_sender.send(Err(ApprovalError::NoUserAgentsConnected));
return reply;
};
if refs.is_empty() { if refs.is_empty() {
reply_sender.send(Err(ApprovalError::NoUserAgentsConnected)); reply_sender.send(Err(ApprovalError::NoUserAgentsConnected));
return reply; return reply;

View File

@@ -1,20 +1,19 @@
use crate::{
actors::{
bootstrap::Bootstrapper, evm::EvmActor, flow_coordinator::FlowCoordinator,
useragent_registry::UserAgentRegistry, vault::Vault,
},
db,
};
use kameo::actor::{ActorRef, Spawn}; use kameo::actor::{ActorRef, Spawn};
use kameo_actors::{DeliveryStrategy, message_bus::MessageBus}; use kameo_actors::{DeliveryStrategy, message_bus::MessageBus};
use thiserror::Error; use thiserror::Error;
use crate::{
actors::{
bootstrap::Bootstrapper, evm::EvmActor, flow_coordinator::FlowCoordinator, vault::Vault,
},
db,
};
pub mod bootstrap; pub mod bootstrap;
pub mod evm; pub mod evm;
pub mod flow_coordinator; pub mod flow_coordinator;
pub mod useragent_registry;
pub mod vault; pub mod vault;
pub mod useragent_registry;
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum SpawnError { pub enum SpawnError {
@@ -31,7 +30,6 @@ pub struct GlobalActors {
pub vault: ActorRef<Vault>, pub vault: ActorRef<Vault>,
pub bootstrapper: ActorRef<Bootstrapper>, pub bootstrapper: ActorRef<Bootstrapper>,
pub flow_coordinator: ActorRef<FlowCoordinator>, pub flow_coordinator: ActorRef<FlowCoordinator>,
pub useragent_registry: ActorRef<UserAgentRegistry>,
pub evm: ActorRef<EvmActor>, pub evm: ActorRef<EvmActor>,
pub events: ActorRef<MessageBus>, pub events: ActorRef<MessageBus>,
} }
@@ -44,15 +42,11 @@ impl GlobalActors {
pub async fn spawn(db: db::DatabasePool) -> Result<Self, SpawnError> { pub async fn spawn(db: db::DatabasePool) -> Result<Self, SpawnError> {
let message_bus = Self::spawn_message_bus(); let message_bus = Self::spawn_message_bus();
let key_holder = Vault::spawn(Vault::new(db.clone(), message_bus.clone()).await?); let key_holder = Vault::spawn(Vault::new(db.clone(), message_bus.clone()).await?);
let useragent_registry = UserAgentRegistry::spawn(UserAgentRegistry::default());
Ok(Self { Ok(Self {
bootstrapper: Bootstrapper::spawn(Bootstrapper::new(&db).await?), bootstrapper: Bootstrapper::spawn(Bootstrapper::new(&db).await?),
evm: EvmActor::spawn(EvmActor::new(key_holder.clone(), db)), evm: EvmActor::spawn(EvmActor::new(key_holder.clone(), db)),
vault: key_holder, vault: key_holder,
flow_coordinator: FlowCoordinator::spawn(FlowCoordinator::new( flow_coordinator: FlowCoordinator::spawn(FlowCoordinator::default()),
useragent_registry.clone(),
)),
useragent_registry,
events: message_bus, events: message_bus,
}) })
} }

View File

@@ -1,61 +1,57 @@
use crate::peers::user_agent::UserAgentSession; use alloy::primitives::map::HashMap;
use arbiter_crypto::authn;
use kameo::{error::Infallible, prelude::*};
use kameo::{ use crate::{db::DatabasePool, peers::user_agent::{Credentials, UserAgentSession}};
Actor,
actor::{ActorId, ActorRef},
error::Infallible,
messages,
prelude::{ActorStopReason, Context, WeakActorRef},
};
use std::{collections::HashMap, ops::ControlFlow};
use tracing::info;
#[derive(Default)] use super::vault::{Vault, events as vault_events};
pub struct UserAgentRegistry {
connected: HashMap<ActorId, ActorRef<UserAgentSession>>, pub struct Args {
pub vault: ActorRef<Vault>,
pub pool: DatabasePool,
} }
pub struct UserAgentRegistry {
vault: ActorRef<Vault>,
pool: DatabasePool,
connected: HashMap<Credentials, ActorRef<UserAgentSession>>,
}
impl Message<vault_events::Bootstrapped> for UserAgentRegistry {
type Reply = ();
async fn handle(
&mut self,
msg: vault_events::Bootstrapped,
ctx: &mut Context<Self, Self::Reply>,
) -> Self::Reply {
todo!()
}
}
impl Message<vault_events::Unsealed> for UserAgentRegistry {
type Reply = ();
async fn handle(
&mut self,
msg: vault_events::Unsealed,
ctx: &mut Context<Self, Self::Reply>,
) -> Self::Reply {
todo!()
}
}
impl Actor for UserAgentRegistry { impl Actor for UserAgentRegistry {
type Args = Self; type Args = Args;
type Error = Infallible; type Error = Infallible;
async fn on_start(args: Self::Args, _: ActorRef<Self>) -> Result<Self, Self::Error> { async fn on_start(args: Self::Args, actor_ref: ActorRef<Self>) -> Result<Self, Self::Error> {
Ok(args) Ok(Self {
vault: args.vault,
pool: args.pool,
connected: HashMap::default(),
})
} }
async fn on_link_died(
&mut self,
_: WeakActorRef<Self>,
id: ActorId,
_: ActorStopReason,
) -> Result<ControlFlow<ActorStopReason>, Self::Error> {
if self.connected.remove(&id).is_some() {
info!(
?id,
actor = "UserAgentRegistry",
event = "useragent.disconnected"
);
}
Ok(ControlFlow::Continue(()))
}
}
#[messages]
impl UserAgentRegistry {
#[message(ctx)]
pub async fn connect_useragent(
&mut self,
actor: ActorRef<UserAgentSession>,
ctx: &mut Context<Self, ()>,
) {
info!(id = %actor.id(), actor = "UserAgentRegistry", event = "useragent.connected");
ctx.actor_ref().link(&actor).await;
self.connected.insert(actor.id(), actor);
}
#[message]
pub fn get_connected(&self) -> Vec<ActorRef<UserAgentSession>> {
self.connected.values().cloned().collect()
}
} }

View File

@@ -1,29 +1,27 @@
use crate::{
crypto::{
KeyCell, derive_key,
encryption::v1::{self, Nonce},
integrity::v1::HmacSha256,
},
db::{
self,
models::{self, RootKeyHistory},
schema::{self},
},
};
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
use chrono::Utc; use chrono::Utc;
use diesel::{ use diesel::{
ExpressionMethods as _, OptionalExtension, QueryDsl, SelectableHelper, ExpressionMethods as _, OptionalExtension, QueryDsl, SelectableHelper,
dsl::{insert_into, update}, dsl::{insert_into, update},
}; };
use diesel_async::{AsyncConnection, RunQueryDsl}; use diesel_async::{AsyncConnection, RunQueryDsl};
use hmac::{KeyInit as _, Mac as _}; use hmac::Mac as _;
use kameo::{Actor, Reply, actor::ActorRef, messages}; use kameo::{Actor, Reply, actor::ActorRef, messages};
use kameo_actors::message_bus::{MessageBus, Publish}; use kameo_actors::message_bus::{MessageBus, Publish};
use strum::{EnumDiscriminants, IntoDiscriminant}; use strum::{EnumDiscriminants, IntoDiscriminant};
use tracing::{error, info}; use tracing::{error, info};
use crate::crypto::{
KeyCell, derive_key,
encryption::v1::{self, Nonce},
integrity::v1::HmacSha256,
};
use crate::db::{
self,
models::{self, RootKeyHistory},
schema::{self},
};
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
pub mod events { pub mod events {
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
@@ -80,7 +78,6 @@ enum State {
} }
/// Manages vault root key and tracks current state of the vault (bootstrapped/unbootstrapped, sealed/unsealed). /// Manages vault root key and tracks current state of the vault (bootstrapped/unbootstrapped, sealed/unsealed).
///
/// Provides API for encrypting and decrypting data using the vault root key. /// Provides API for encrypting and decrypting data using the vault root key.
/// Abstraction over database to make sure nonces are never reused and encryption keys are never exposed in plaintext outside of this actor. /// Abstraction over database to make sure nonces are never reused and encryption keys are never exposed in plaintext outside of this actor.
#[derive(Actor)] #[derive(Actor)]
@@ -127,7 +124,7 @@ impl Vault {
.first(conn) .first(conn)
.await?; .await?;
let mut nonce = Nonce::try_from(current_nonce.as_slice()).map_err(|()| { let mut nonce = Nonce::try_from(current_nonce.as_slice()).map_err(|_| {
error!( error!(
"Broken database: invalid nonce for root key history id={}", "Broken database: invalid nonce for root key history id={}",
root_key_id root_key_id
@@ -150,7 +147,7 @@ impl Vault {
Ok(nonce) Ok(nonce)
} }
const fn expect_unsealed(state: &mut State) -> Result<&mut Unsealed, Error> { fn expect_unsealed(state: &mut State) -> Result<&mut Unsealed, Error> {
match state { match state {
State::Unsealed(unsealed) => Ok(unsealed), State::Unsealed(unsealed) => Ok(unsealed),
State::Unbootstrapped => Err(Error::NotBootstrapped), State::Unbootstrapped => Err(Error::NotBootstrapped),
@@ -216,7 +213,7 @@ impl Vault {
}); });
info!("Vault bootstrapped successfully"); info!("Vault bootstrapped successfully");
let _ = self.events.tell(Publish(events::Bootstrapped)).await; self.events.tell(Publish(events::Bootstrapped)).await;
Ok(()) Ok(())
} }
@@ -249,11 +246,12 @@ impl Vault {
let mut root_key = SafeCell::new(current_key.ciphertext.clone()); let mut root_key = SafeCell::new(current_key.ciphertext.clone());
let nonce = let nonce = v1::Nonce::try_from(current_key.root_key_encryption_nonce.as_slice()).map_err(
Nonce::try_from(current_key.root_key_encryption_nonce.as_slice()).map_err(|()| { |_| {
error!("Broken database: invalid nonce for root key"); error!("Broken database: invalid nonce for root key");
Error::BrokenDatabase Error::BrokenDatabase
})?; },
)?;
seal_key seal_key
.decrypt_in_place(&nonce, v1::ROOT_KEY_TAG, &mut root_key) .decrypt_in_place(&nonce, v1::ROOT_KEY_TAG, &mut root_key)
@@ -271,7 +269,7 @@ impl Vault {
}); });
info!("Vault unsealed successfully"); info!("Vault unsealed successfully");
let _ = self.events.tell(Publish(events::Unsealed)).await; self.events.tell(Publish(events::Unsealed)).await;
Ok(()) Ok(())
} }
@@ -291,7 +289,7 @@ impl Vault {
.ok_or(Error::NotFound)? .ok_or(Error::NotFound)?
}; };
let nonce = Nonce::try_from(row.current_nonce.as_slice()).map_err(|()| { let nonce = v1::Nonce::try_from(row.current_nonce.as_slice()).map_err(|_| {
error!( error!(
"Broken database: invalid nonce for aead_encrypted id={}", "Broken database: invalid nonce for aead_encrypted id={}",
aead_id aead_id
@@ -401,15 +399,22 @@ impl Vault {
self.state = State::Sealed { self.state = State::Sealed {
root_key_history_id: *root_key_history_id, root_key_history_id: *root_key_history_id,
}; };
let _ = self.events.tell(Publish(events::VaultResealed)).await; self.events.tell(Publish(events::VaultResealed)).await;
Ok(()) Ok(())
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::actors::GlobalActors; use diesel::SelectableHelper;
use arbiter_crypto::safecell::SafeCellHandle as _;
use diesel_async::RunQueryDsl;
use crate::{
actors::GlobalActors,
db::{self},
};
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
use super::*; use super::*;

View File

@@ -1,12 +1,13 @@
use std::sync::Arc;
use thiserror::Error;
use crate::{ use crate::{
actors::GlobalActors, actors::GlobalActors,
context::tls::TlsManager, context::tls::TlsManager,
db::{self}, db::{self},
}; };
use std::sync::Arc;
use thiserror::Error;
pub mod tls; pub mod tls;
#[derive(Error, Debug)] #[derive(Error, Debug)]
@@ -30,16 +31,16 @@ pub enum InitError {
Io(#[from] std::io::Error), Io(#[from] std::io::Error),
} }
pub struct __ServerContextInner { pub struct _ServerContextInner {
pub db: db::DatabasePool, pub db: db::DatabasePool,
pub tls: TlsManager, pub tls: TlsManager,
pub actors: GlobalActors, pub actors: GlobalActors,
} }
#[derive(Clone)] #[derive(Clone)]
pub struct ServerContext(Arc<__ServerContextInner>); pub struct ServerContext(Arc<_ServerContextInner>);
impl std::ops::Deref for ServerContext { impl std::ops::Deref for ServerContext {
type Target = __ServerContextInner; type Target = _ServerContextInner;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
&self.0 &self.0
@@ -48,7 +49,7 @@ impl std::ops::Deref for ServerContext {
impl ServerContext { impl ServerContext {
pub async fn new(db: db::DatabasePool) -> Result<Self, InitError> { pub async fn new(db: db::DatabasePool) -> Result<Self, InitError> {
Ok(Self(Arc::new(__ServerContextInner { Ok(Self(Arc::new(_ServerContextInner {
actors: GlobalActors::spawn(db.clone()).await?, actors: GlobalActors::spawn(db.clone()).await?,
tls: TlsManager::new(db.clone()).await?, tls: TlsManager::new(db.clone()).await?,
db, db,

View File

@@ -1,3 +1,17 @@
use std::{net::Ipv4Addr, string::FromUtf8Error};
use diesel::{ExpressionMethods as _, QueryDsl, SelectableHelper as _};
use diesel_async::{AsyncConnection, RunQueryDsl};
use pem::Pem;
use rcgen::{
BasicConstraints, Certificate, CertificateParams, CertifiedIssuer, DistinguishedName, DnType,
IsCa, Issuer, KeyPair, KeyUsagePurpose, SanType,
};
use rustls::pki_types::pem::PemObject;
use thiserror::Error;
use tonic::transport::CertificateDer;
use crate::db::{ use crate::db::{
self, self,
models::{NewTlsHistory, TlsHistory}, models::{NewTlsHistory, TlsHistory},
@@ -7,23 +21,10 @@ use crate::db::{
}, },
}; };
use diesel::{ExpressionMethods as _, QueryDsl, SelectableHelper as _};
use diesel_async::{AsyncConnection, RunQueryDsl};
use pem::Pem;
use rcgen::{
BasicConstraints, Certificate, CertificateParams, CertifiedIssuer, DistinguishedName, DnType,
IsCa, Issuer, KeyPair, KeyUsagePurpose, SanType,
};
use rustls::pki_types::pem::PemObject;
use std::{net::Ipv4Addr, string::FromUtf8Error};
use thiserror::Error;
use tonic::transport::CertificateDer;
const ENCODE_CONFIG: pem::EncodeConfig = { const ENCODE_CONFIG: pem::EncodeConfig = {
let line_ending = if cfg!(target_family = "windows") { let line_ending = match cfg!(target_family = "windows") {
pem::LineEnding::CRLF true => pem::LineEnding::CRLF,
} else { false => pem::LineEnding::LF,
pem::LineEnding::LF
}; };
pem::EncodeConfig::new().set_line_ending(line_ending) pem::EncodeConfig::new().set_line_ending(line_ending)
}; };
@@ -51,14 +52,11 @@ pub enum InitError {
pub type PemCert = String; pub type PemCert = String;
pub fn encode_cert_to_pem(cert: &CertificateDer<'_>) -> PemCert { 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)
} }
#[expect( #[allow(unused)]
unused,
reason = "may be needed for future cert rotation implementation"
)]
struct SerializedTls { struct SerializedTls {
cert_pem: PemCert, cert_pem: PemCert,
cert_key_pem: String, cert_key_pem: String,
@@ -87,7 +85,7 @@ impl TlsCa {
let cert_key_pem = certified_issuer.key().serialize_pem(); let cert_key_pem = certified_issuer.key().serialize_pem();
#[expect( #[allow(
clippy::unwrap_used, clippy::unwrap_used,
reason = "Broken cert couldn't bootstrap server anyway" reason = "Broken cert couldn't bootstrap server anyway"
)] )]
@@ -126,11 +124,7 @@ impl TlsCa {
}) })
} }
#[expect( #[allow(unused)]
unused,
clippy::unnecessary_wraps,
reason = "may be needed for future cert rotation implementation"
)]
fn serialize(&self) -> Result<SerializedTls, InitError> { fn serialize(&self) -> Result<SerializedTls, InitError> {
let cert_key_pem = self.issuer.key().serialize_pem(); let cert_key_pem = self.issuer.key().serialize_pem();
Ok(SerializedTls { Ok(SerializedTls {
@@ -139,10 +133,7 @@ impl TlsCa {
}) })
} }
#[expect( #[allow(unused)]
unused,
reason = "may be needed for future cert rotation implementation"
)]
fn try_deserialize(cert_pem: &str, cert_key_pem: &str) -> Result<Self, InitError> { fn try_deserialize(cert_pem: &str, cert_key_pem: &str) -> Result<Self, InitError> {
let keypair = let keypair =
KeyPair::from_pem(cert_key_pem).map_err(InitError::KeyDeserializationError)?; KeyPair::from_pem(cert_key_pem).map_err(InitError::KeyDeserializationError)?;
@@ -243,10 +234,10 @@ impl TlsManager {
} }
} }
pub const fn cert(&self) -> &CertificateDer<'static> { pub fn cert(&self) -> &CertificateDer<'static> {
&self.cert &self.cert
} }
pub const fn ca_cert(&self) -> &CertificateDer<'static> { pub fn ca_cert(&self) -> &CertificateDer<'static> {
&self.ca_cert &self.ca_cert
} }

View File

@@ -1,11 +1,12 @@
use argon2::password_hash::Salt as ArgonSalt; use argon2::password_hash::Salt as ArgonSalt;
use rand::{ use rand::{
Rng as _, SeedableRng, Rng as _, SeedableRng,
rngs::{StdRng, SysRng}, rngs::{StdRng, SysRng},
}; };
pub const ROOT_KEY_TAG: &[u8] = b"arbiter/seal/v1"; pub const ROOT_KEY_TAG: &[u8] = "arbiter/seal/v1".as_bytes();
pub const TAG: &[u8] = b"arbiter/private-key/v1"; pub const TAG: &[u8] = "arbiter/private-key/v1".as_bytes();
pub const NONCE_LENGTH: usize = 24; pub const NONCE_LENGTH: usize = 24;
@@ -14,16 +15,14 @@ pub struct Nonce(pub [u8; NONCE_LENGTH]);
impl Nonce { impl Nonce {
pub fn increment(&mut self) { pub fn increment(&mut self) {
for i in (0..self.0.len()).rev() { for i in (0..self.0.len()).rev() {
if let Some(byte) = self.0.get_mut(i) { if self.0[i] == 0xFF {
if *byte == 0xFF { self.0[i] = 0;
*byte = 0;
} else { } else {
*byte += 1; self.0[i] += 1;
break; break;
} }
} }
} }
}
pub fn to_vec(&self) -> Vec<u8> { pub fn to_vec(&self) -> Vec<u8> {
self.0.to_vec() self.0.to_vec()
@@ -46,20 +45,25 @@ pub type Salt = [u8; ArgonSalt::RECOMMENDED_LENGTH];
pub fn generate_salt() -> Salt { pub fn generate_salt() -> Salt {
let mut salt = Salt::default(); let mut salt = Salt::default();
let mut rng = #[allow(
StdRng::try_from_rng(&mut SysRng).expect("Rng failure is unrecoverable and should panic"); clippy::unwrap_used,
reason = "Rng failure is unrecoverable and should panic"
)]
let mut rng = StdRng::try_from_rng(&mut SysRng).unwrap();
rng.fill_bytes(&mut salt); rng.fill_bytes(&mut salt);
salt salt
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::ops::Deref as _;
use super::*; use super::*;
use crate::crypto::derive_key; use crate::crypto::derive_key;
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _}; use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
#[test] #[test]
fn derive_seal_key_deterministic() { pub fn derive_seal_key_deterministic() {
static PASSWORD: &[u8] = b"password"; static PASSWORD: &[u8] = b"password";
let password = SafeCell::new(PASSWORD.to_vec()); let password = SafeCell::new(PASSWORD.to_vec());
let password2 = SafeCell::new(PASSWORD.to_vec()); let password2 = SafeCell::new(PASSWORD.to_vec());
@@ -71,24 +75,25 @@ mod tests {
let key1_reader = key1.0.read(); let key1_reader = key1.0.read();
let key2_reader = key2.0.read(); let key2_reader = key2.0.read();
assert_eq!(&*key1_reader, &*key2_reader); assert_eq!(key1_reader.deref(), key2_reader.deref());
} }
#[test] #[test]
fn successful_derive() { pub fn successful_derive() {
static PASSWORD: &[u8] = b"password"; static PASSWORD: &[u8] = b"password";
let password = SafeCell::new(PASSWORD.to_vec()); let password = SafeCell::new(PASSWORD.to_vec());
let salt = generate_salt(); let salt = generate_salt();
let mut key = derive_key(password, &salt); let mut key = derive_key(password, &salt);
let key_reader = key.0.read(); let key_reader = key.0.read();
let key_ref = key_reader.deref();
assert_ne!(key_reader.as_slice(), &[0u8; 32][..]); assert_ne!(key_ref.as_slice(), &[0u8; 32][..]);
} }
#[test] #[test]
// We should fuzz this // We should fuzz this
pub fn nonce_increment() { pub fn test_nonce_increment() {
let mut nonce = Nonce([0u8; NONCE_LENGTH]); let mut nonce = Nonce([0u8; NONCE_LENGTH]);
nonce.increment(); nonce.increment();

View File

@@ -1,18 +1,25 @@
use crate::{ use crate::{
actors::vault::{self, GetState, SignIntegrity, Vault, VerifyIntegrity}, actors::vault::{self, GetState},
crypto::integrity::hashing::Hashable,
};
use hmac::Hmac;
use sha2::Sha256;
use diesel::{ExpressionMethods as _, QueryDsl, dsl::insert_into, sqlite::Sqlite};
use diesel_async::{AsyncConnection, RunQueryDsl};
use kameo::{actor::ActorRef, error::SendError};
use sha2::Digest as _;
pub mod hashing;
use crate::{
actors::vault::{SignIntegrity, Vault, VerifyIntegrity},
db::{ db::{
self, self,
models::{IntegrityEnvelope, NewIntegrityEnvelope}, models::{IntegrityEnvelope, NewIntegrityEnvelope},
schema::integrity_envelope, schema::integrity_envelope,
}, },
}; };
use arbiter_crypto::hashing::Hashable;
use diesel::{ExpressionMethods as _, QueryDsl, dsl::insert_into, sqlite::Sqlite};
use diesel_async::{AsyncConnection, RunQueryDsl};
use hmac::Hmac;
use kameo::{actor::ActorRef, error::SendError};
use sha2::{Digest as _, Sha256};
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum Error { pub enum Error {
@@ -64,11 +71,6 @@ fn payload_hash(payload: &impl Hashable) -> [u8; 32] {
} }
fn push_len_prefixed(out: &mut Vec<u8>, bytes: &[u8]) { fn push_len_prefixed(out: &mut Vec<u8>, bytes: &[u8]) {
#[expect(
clippy::cast_possible_truncation,
clippy::as_conversions,
reason = "fixme! #85"
)]
out.extend_from_slice(&(bytes.len() as u32).to_be_bytes()); out.extend_from_slice(&(bytes.len() as u32).to_be_bytes());
out.extend_from_slice(bytes); out.extend_from_slice(bytes);
} }
@@ -120,7 +122,7 @@ pub async fn sign_entity<E: Integrable>(
.ask(SignIntegrity { mac_input }) .ask(SignIntegrity { mac_input })
.await .await
.map_err(|err| match err { .map_err(|err| match err {
SendError::HandlerError(inner) => Error::Vault(inner), kameo::error::SendError::HandlerError(inner) => Error::Vault(inner),
_ => Error::VaultSend, _ => Error::VaultSend,
})?; })?;
@@ -130,7 +132,7 @@ pub async fn sign_entity<E: Integrable>(
entity_id, entity_id,
payload_version: E::VERSION, payload_version: E::VERSION,
key_version, key_version,
mac: mac.clone(), mac: mac.to_vec(),
}) })
.on_conflict(( .on_conflict((
integrity_envelope::entity_id, integrity_envelope::entity_id,
@@ -192,7 +194,9 @@ pub async fn verify_entity<E: Integrable>(
Ok(false) => Err(Error::MacMismatch { Ok(false) => Err(Error::MacMismatch {
entity_kind: E::KIND, entity_kind: E::KIND,
}), }),
Err(SendError::HandlerError(vault::Error::Sealed)) => Ok(AttestationStatus::Unavailable), Err(SendError::HandlerError(vault::Error::Sealed)) => {
Ok(AttestationStatus::Unavailable)
}
Err(_) => Err(Error::VaultSend), Err(_) => Err(Error::VaultSend),
} }
} }
@@ -208,6 +212,8 @@ mod tests {
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
use kameo::{actor::ActorRef, prelude::Spawn}; use kameo::{actor::ActorRef, prelude::Spawn};
use sha2::Digest;
use crate::{ use crate::{
actors::{ actors::{
GlobalActors, GlobalActors,
@@ -217,12 +223,21 @@ mod tests {
}; };
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _}; use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
use super::hashing::Hashable;
use super::{Error, Integrable, sign_entity, verify_entity}; use super::{Error, Integrable, sign_entity, verify_entity};
#[derive(Clone, arbiter_macros::Hashable)]
#[derive(Clone)]
struct DummyEntity { struct DummyEntity {
payload_version: i32, payload_version: i32,
payload: Vec<u8>, payload: Vec<u8>,
} }
impl Hashable for DummyEntity {
fn hash<H: Digest>(&self, hasher: &mut H) {
self.payload_version.hash(hasher);
self.payload.hash(hasher);
}
}
impl Integrable for DummyEntity { impl Integrable for DummyEntity {
const KIND: &'static str = "dummy_entity"; const KIND: &'static str = "dummy_entity";
} }
@@ -244,12 +259,12 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn sign_writes_envelope_and_verify_passes() { async fn sign_writes_envelope_and_verify_passes() {
const ENTITY_ID: &[u8] = b"entity-id-7";
let db = db::create_test_pool().await; let db = db::create_test_pool().await;
let vault = bootstrapped_vault(&db).await; let vault = bootstrapped_vault(&db).await;
let mut conn = db.get().await.unwrap(); let mut conn = db.get().await.unwrap();
const ENTITY_ID: &[u8] = b"entity-id-7";
let entity = DummyEntity { let entity = DummyEntity {
payload_version: 1, payload_version: 1,
payload: b"payload-v1".to_vec(), payload: b"payload-v1".to_vec(),
@@ -275,12 +290,12 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn tampered_mac_fails_verification() { async fn tampered_mac_fails_verification() {
const ENTITY_ID: &[u8] = b"entity-id-11";
let db = db::create_test_pool().await; let db = db::create_test_pool().await;
let vault = bootstrapped_vault(&db).await; let vault = bootstrapped_vault(&db).await;
let mut conn = db.get().await.unwrap(); let mut conn = db.get().await.unwrap();
const ENTITY_ID: &[u8] = b"entity-id-11";
let entity = DummyEntity { let entity = DummyEntity {
payload_version: 1, payload_version: 1,
payload: b"payload-v1".to_vec(), payload: b"payload-v1".to_vec(),
@@ -306,12 +321,12 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn changed_payload_fails_verification() { async fn changed_payload_fails_verification() {
const ENTITY_ID: &[u8] = b"entity-id-21";
let db = db::create_test_pool().await; let db = db::create_test_pool().await;
let vault = bootstrapped_vault(&db).await; let vault = bootstrapped_vault(&db).await;
let mut conn = db.get().await.unwrap(); let mut conn = db.get().await.unwrap();
const ENTITY_ID: &[u8] = b"entity-id-21";
let entity = DummyEntity { let entity = DummyEntity {
payload_version: 1, payload_version: 1,
payload: b"payload-v1".to_vec(), payload: b"payload-v1".to_vec(),

View File

@@ -1,12 +1,7 @@
use hmac::digest::Digest;
use std::collections::HashSet; use std::collections::HashSet;
pub use hmac::digest::Digest;
/// Deterministically hash a value by feeding its fields into the hasher in a consistent order. /// Deterministically hash a value by feeding its fields into the hasher in a consistent order.
#[diagnostic::on_unimplemented(
note = "for local types consider adding `#[derive(arbiter_macros::Hashable)]` to your `{Self}` type",
note = "for types from other crates check whether the crate offers a `Hashable` implementation"
)]
pub trait Hashable { pub trait Hashable {
fn hash<H: Digest>(&self, hasher: &mut H); fn hash<H: Digest>(&self, hasher: &mut H);
} }
@@ -50,7 +45,7 @@ impl<T: Hashable + PartialOrd> Hashable for Vec<T> {
} }
} }
impl<T: Hashable + PartialOrd, S: std::hash::BuildHasher> Hashable for HashSet<T, S> { impl<T: Hashable + PartialOrd> Hashable for HashSet<T> {
fn hash<H: Digest>(&self, hasher: &mut H) { fn hash<H: Digest>(&self, hasher: &mut H) {
let ref_sorted = { let ref_sorted = {
let mut sorted = self.iter().collect::<Vec<_>>(); let mut sorted = self.iter().collect::<Vec<_>>();

View File

@@ -1,5 +1,4 @@
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _}; use std::ops::Deref as _;
use encryption::v1::{Nonce, Salt};
use argon2::{Algorithm, Argon2}; use argon2::{Algorithm, Argon2};
use chacha20poly1305::{ use chacha20poly1305::{
@@ -11,9 +10,13 @@ use rand::{
rngs::{StdRng, SysRng}, rngs::{StdRng, SysRng},
}; };
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
pub mod encryption; pub mod encryption;
pub mod integrity; pub mod integrity;
use encryption::v1::{Nonce, Salt};
pub struct KeyCell(pub SafeCell<Key>); pub struct KeyCell(pub SafeCell<Key>);
impl From<SafeCell<Key>> for KeyCell { impl From<SafeCell<Key>> for KeyCell {
fn from(value: SafeCell<Key>) -> Self { fn from(value: SafeCell<Key>) -> Self {
@@ -38,8 +41,11 @@ impl TryFrom<SafeCell<Vec<u8>>> for KeyCell {
impl KeyCell { impl KeyCell {
pub fn new_secure_random() -> Self { pub fn new_secure_random() -> Self {
let key = SafeCell::new_inline(|key_buffer: &mut Key| { let key = SafeCell::new_inline(|key_buffer: &mut Key| {
let mut rng = StdRng::try_from_rng(&mut SysRng) #[allow(
.expect("Rng failure is unrecoverable and should panic"); clippy::unwrap_used,
reason = "Rng failure is unrecoverable and should panic"
)]
let mut rng = StdRng::try_from_rng(&mut SysRng).unwrap();
rng.fill_bytes(key_buffer); rng.fill_bytes(key_buffer);
}); });
@@ -53,7 +59,8 @@ impl KeyCell {
mut buffer: impl AsMut<Vec<u8>>, mut buffer: impl AsMut<Vec<u8>>,
) -> Result<(), Error> { ) -> Result<(), Error> {
let key_reader = self.0.read(); let key_reader = self.0.read();
let cipher = XChaCha20Poly1305::new(&key_reader); let key_ref = key_reader.deref();
let cipher = XChaCha20Poly1305::new(key_ref);
let nonce = XNonce::from_slice(nonce.0.as_ref()); let nonce = XNonce::from_slice(nonce.0.as_ref());
let buffer = buffer.as_mut(); let buffer = buffer.as_mut();
cipher.encrypt_in_place(nonce, associated_data, buffer) cipher.encrypt_in_place(nonce, associated_data, buffer)
@@ -65,7 +72,8 @@ impl KeyCell {
buffer: &mut SafeCell<Vec<u8>>, buffer: &mut SafeCell<Vec<u8>>,
) -> Result<(), Error> { ) -> Result<(), Error> {
let key_reader = self.0.read(); let key_reader = self.0.read();
let cipher = XChaCha20Poly1305::new(&key_reader); let key_ref = key_reader.deref();
let cipher = XChaCha20Poly1305::new(key_ref);
let nonce = XNonce::from_slice(nonce.0.as_ref()); let nonce = XNonce::from_slice(nonce.0.as_ref());
let mut buffer = buffer.write(); let mut buffer = buffer.write();
let buffer: &mut Vec<u8> = buffer.as_mut(); let buffer: &mut Vec<u8> = buffer.as_mut();
@@ -79,7 +87,8 @@ impl KeyCell {
plaintext: impl AsRef<[u8]>, plaintext: impl AsRef<[u8]>,
) -> Result<Vec<u8>, Error> { ) -> Result<Vec<u8>, Error> {
let key_reader = self.0.read(); let key_reader = self.0.read();
let mut cipher = XChaCha20Poly1305::new(&key_reader); let key_ref = key_reader.deref();
let mut cipher = XChaCha20Poly1305::new(key_ref);
let nonce = XNonce::from_slice(nonce.0.as_ref()); let nonce = XNonce::from_slice(nonce.0.as_ref());
let ciphertext = cipher.encrypt( let ciphertext = cipher.encrypt(
@@ -107,15 +116,20 @@ pub fn derive_key(mut password: SafeCell<Vec<u8>>, salt: &Salt) -> KeyCell {
} }
}; };
#[allow(clippy::unwrap_used)]
let hasher = Argon2::new(Algorithm::Argon2id, argon2::Version::V0x13, params); let hasher = Argon2::new(Algorithm::Argon2id, argon2::Version::V0x13, params);
let mut key = SafeCell::new(Key::default()); let mut key = SafeCell::new(Key::default());
password.read_inline(|password_source| { password.read_inline(|password_source| {
let mut key_buffer = key.write(); let mut key_buffer = key.write();
let key_buffer: &mut [u8] = key_buffer.as_mut(); let key_buffer: &mut [u8] = key_buffer.as_mut();
#[allow(
clippy::unwrap_used,
reason = "Better fail completely than return a weak key"
)]
hasher hasher
.hash_password_into(password_source, salt, key_buffer) .hash_password_into(password_source.deref(), salt, key_buffer)
.expect("Better fail completely than return a weak key"); .unwrap();
}); });
key.into() key.into()
@@ -130,7 +144,7 @@ mod tests {
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _}; use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
#[test] #[test]
fn encrypt_decrypt() { pub fn encrypt_decrypt() {
static PASSWORD: &[u8] = b"password"; static PASSWORD: &[u8] = b"password";
let password = SafeCell::new(PASSWORD.to_vec()); let password = SafeCell::new(PASSWORD.to_vec());
let salt = generate_salt(); let salt = generate_salt();

View File

@@ -5,6 +5,7 @@ use diesel_async::{
sync_connection_wrapper::SyncConnectionWrapper, sync_connection_wrapper::SyncConnectionWrapper,
}; };
use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations}; use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations};
use thiserror::Error; use thiserror::Error;
use tracing::info; use tracing::info;
@@ -22,14 +23,14 @@ const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations");
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum DatabaseSetupError { pub enum DatabaseSetupError {
#[error(transparent)] #[error("Failed to determine home directory")]
ConcurrencySetup(diesel::result::Error), HomeDir(std::io::Error),
#[error(transparent)] #[error(transparent)]
Connection(diesel::ConnectionError), Connection(diesel::ConnectionError),
#[error("Failed to determine home directory")] #[error(transparent)]
HomeDir(std::io::Error), ConcurrencySetup(diesel::result::Error),
#[error(transparent)] #[error(transparent)]
Migration(Box<dyn std::error::Error + Send + Sync>), Migration(Box<dyn std::error::Error + Send + Sync>),
@@ -40,11 +41,10 @@ pub enum DatabaseSetupError {
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum DatabaseError { pub enum DatabaseError {
#[error("Database query error")]
Connection(#[from] diesel::result::Error),
#[error("Database connection error")] #[error("Database connection error")]
Pool(#[from] PoolError), Pool(#[from] PoolError),
#[error("Database query error")]
Connection(#[from] diesel::result::Error),
} }
#[tracing::instrument(level = "info")] #[tracing::instrument(level = "info")]
@@ -93,16 +93,13 @@ fn initialize_database(url: &str) -> Result<(), DatabaseSetupError> {
} }
#[tracing::instrument(level = "info")] #[tracing::instrument(level = "info")]
/// Creates a connection pool for the `SQLite` database.
///
/// # Panics
/// Panics if the database path is not valid UTF-8.
pub async fn create_pool(url: Option<&str>) -> Result<DatabasePool, DatabaseSetupError> { pub async fn create_pool(url: Option<&str>) -> Result<DatabasePool, DatabaseSetupError> {
let database_url = url.map(String::from).unwrap_or( let database_url = url.map(String::from).unwrap_or(
#[allow(clippy::expect_used)]
database_path()? database_path()?
.to_str() .to_str()
.expect("database path is not valid UTF-8") .expect("database path is not valid UTF-8")
.to_owned(), .to_string(),
); );
initialize_database(&database_url)?; initialize_database(&database_url)?;
@@ -137,19 +134,19 @@ pub async fn create_pool(url: Option<&str>) -> Result<DatabasePool, DatabaseSetu
} }
#[mutants::skip] #[mutants::skip]
#[expect(clippy::missing_panics_doc, reason = "Tests oriented function")]
/// Creates a test database pool with a temporary `SQLite` database file.
pub async fn create_test_pool() -> DatabasePool { pub async fn create_test_pool() -> DatabasePool {
use rand::distr::{Alphanumeric, SampleString as _}; use rand::distr::{Alphanumeric, SampleString as _};
let tempfile_name = Alphanumeric.sample_string(&mut rand::rng(), 16); let tempfile_name = Alphanumeric.sample_string(&mut rand::rng(), 16);
let file = std::env::temp_dir().join(tempfile_name); let file = std::env::temp_dir().join(tempfile_name);
#[allow(clippy::expect_used)]
let url = file let url = file
.to_str() .to_str()
.expect("temp file path is not valid UTF-8") .expect("temp file path is not valid UTF-8")
.to_owned(); .to_string();
#[allow(clippy::expect_used)]
create_pool(Some(&url)) create_pool(Some(&url))
.await .await
.expect("Failed to create test database pool") .expect("Failed to create test database pool")

View File

@@ -1,14 +1,13 @@
#![allow( #![allow(unused)]
clippy::duplicated_attributes, #![allow(clippy::all)]
reason = "restructed's #[view] causes false positives"
)]
use crate::db::schema::{ use crate::db::schema::{
self, aead_encrypted, arbiter_settings, evm_basic_grant, evm_ether_transfer_grant, 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_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, evm_token_transfer_log, evm_token_transfer_volume_limit, evm_transaction_log, evm_wallet,
integrity_envelope, root_key_history, tls_history, integrity_envelope, root_key_history, tls_history,
}; };
use chrono::{DateTime, Utc};
use diesel::{prelude::*, sqlite::Sqlite}; use diesel::{prelude::*, sqlite::Sqlite};
use restructed::Models; use restructed::Models;
@@ -28,16 +27,16 @@ pub mod types {
pub struct SqliteTimestamp(pub DateTime<Utc>); pub struct SqliteTimestamp(pub DateTime<Utc>);
impl SqliteTimestamp { impl SqliteTimestamp {
pub fn now() -> Self { pub fn now() -> Self {
Self(Utc::now()) SqliteTimestamp(Utc::now())
} }
} }
impl From<DateTime<Utc>> for SqliteTimestamp { impl From<chrono::DateTime<Utc>> for SqliteTimestamp {
fn from(dt: DateTime<Utc>) -> Self { fn from(dt: chrono::DateTime<Utc>) -> Self {
Self(dt) SqliteTimestamp(dt)
} }
} }
impl From<SqliteTimestamp> for DateTime<Utc> { impl From<SqliteTimestamp> for chrono::DateTime<Utc> {
fn from(ts: SqliteTimestamp) -> Self { fn from(ts: SqliteTimestamp) -> Self {
ts.0 ts.0
} }
@@ -48,11 +47,6 @@ pub mod types {
&'b self, &'b self,
out: &mut diesel::serialize::Output<'b, '_, Sqlite>, out: &mut diesel::serialize::Output<'b, '_, Sqlite>,
) -> diesel::serialize::Result { ) -> diesel::serialize::Result {
#[expect(
clippy::cast_possible_truncation,
clippy::as_conversions,
reason = "fixme! #84; this will break up in 2038 :3"
)]
let unix_timestamp = self.0.timestamp() as i32; let unix_timestamp = self.0.timestamp() as i32;
out.set_value(unix_timestamp); out.set_value(unix_timestamp);
Ok(IsNull::No) Ok(IsNull::No)
@@ -75,47 +69,7 @@ pub mod types {
let datetime = let datetime =
DateTime::from_timestamp(unix_timestamp, 0).ok_or("Timestamp is out of bounds")?; DateTime::from_timestamp(unix_timestamp, 0).ok_or("Timestamp is out of bounds")?;
Ok(Self(datetime)) Ok(SqliteTimestamp(datetime))
}
}
#[derive(Debug, FromSqlRow, AsExpression, Clone)]
#[diesel(sql_type = Integer)]
#[repr(transparent)] // hint compiler to optimize the wrapper struct away
pub struct ChainId(pub i32);
#[expect(
clippy::cast_sign_loss,
clippy::cast_possible_truncation,
clippy::as_conversions,
reason = "safe because chain_id is stored as i32 but is guaranteed to be a valid ChainId by the API when creating grants"
)]
const _: () = {
impl From<ChainId> for alloy::primitives::ChainId {
fn from(chain_id: ChainId) -> Self {
chain_id.0 as Self
}
}
impl From<alloy::primitives::ChainId> for ChainId {
fn from(chain_id: alloy::primitives::ChainId) -> Self {
Self(chain_id as _)
}
}
};
impl FromSql<Integer, Sqlite> for ChainId {
fn from_sql(
bytes: <Sqlite as diesel::backend::Backend>::RawValue<'_>,
) -> diesel::deserialize::Result<Self> {
FromSql::<Integer, Sqlite>::from_sql(bytes).map(Self)
}
}
impl ToSql<Integer, Sqlite> for ChainId {
fn to_sql<'b>(
&'b self,
out: &mut diesel::serialize::Output<'b, '_, Sqlite>,
) -> diesel::serialize::Result {
ToSql::<Integer, Sqlite>::to_sql(&self.0, out)
} }
} }
} }
@@ -241,6 +195,7 @@ pub struct ProgramClientMetadataHistory {
#[diesel(table_name = schema::program_client, check_for_backend(Sqlite))] #[diesel(table_name = schema::program_client, check_for_backend(Sqlite))]
pub struct ProgramClient { pub struct ProgramClient {
pub id: i32, pub id: i32,
pub nonce: i32,
pub public_key: Vec<u8>, pub public_key: Vec<u8>,
pub metadata_id: i32, pub metadata_id: i32,
pub created_at: SqliteTimestamp, pub created_at: SqliteTimestamp,
@@ -251,6 +206,7 @@ pub struct ProgramClient {
#[diesel(table_name = schema::useragent_client, check_for_backend(Sqlite))] #[diesel(table_name = schema::useragent_client, check_for_backend(Sqlite))]
pub struct UseragentClient { pub struct UseragentClient {
pub id: i32, pub id: i32,
pub nonce: i32,
pub public_key: Vec<u8>, pub public_key: Vec<u8>,
pub created_at: SqliteTimestamp, pub created_at: SqliteTimestamp,
pub updated_at: SqliteTimestamp, pub updated_at: SqliteTimestamp,
@@ -281,7 +237,7 @@ pub struct EvmEtherTransferLimit {
pub struct EvmBasicGrant { pub struct EvmBasicGrant {
pub id: i32, pub id: i32,
pub wallet_access_id: i32, // references evm_wallet_access.id pub wallet_access_id: i32, // references evm_wallet_access.id
pub chain_id: ChainId, pub chain_id: i32,
pub valid_from: Option<SqliteTimestamp>, pub valid_from: Option<SqliteTimestamp>,
pub valid_until: Option<SqliteTimestamp>, pub valid_until: Option<SqliteTimestamp>,
pub max_gas_fee_per_gas: Option<Vec<u8>>, pub max_gas_fee_per_gas: Option<Vec<u8>>,
@@ -304,7 +260,7 @@ pub struct EvmTransactionLog {
pub id: i32, pub id: i32,
pub grant_id: i32, pub grant_id: i32,
pub wallet_access_id: i32, pub wallet_access_id: i32,
pub chain_id: ChainId, pub chain_id: i32,
pub eth_value: Vec<u8>, pub eth_value: Vec<u8>,
pub signed_at: SqliteTimestamp, pub signed_at: SqliteTimestamp,
} }
@@ -379,7 +335,7 @@ pub struct EvmTokenTransferLog {
pub id: i32, pub id: i32,
pub grant_id: i32, pub grant_id: i32,
pub log_id: i32, pub log_id: i32,
pub chain_id: ChainId, pub chain_id: i32,
pub token_contract: Vec<u8>, pub token_contract: Vec<u8>,
pub recipient_address: Vec<u8>, pub recipient_address: Vec<u8>,
pub value: Vec<u8>, pub value: Vec<u8>,

View File

@@ -155,6 +155,7 @@ diesel::table! {
diesel::table! { diesel::table! {
program_client (id) { program_client (id) {
id -> Integer, id -> Integer,
nonce -> Integer,
public_key -> Binary, public_key -> Binary,
metadata_id -> Integer, metadata_id -> Integer,
created_at -> Integer, created_at -> Integer,
@@ -188,7 +189,9 @@ diesel::table! {
diesel::table! { diesel::table! {
useragent_client (id) { useragent_client (id) {
id -> Integer, id -> Integer,
nonce -> Integer,
public_key -> Binary, public_key -> Binary,
key_type -> Integer,
created_at -> Integer, created_at -> Integer,
updated_at -> Integer, updated_at -> Integer,
} }

View File

@@ -45,7 +45,7 @@ sol! {
sol! { sol! {
/// Permit2 — Uniswap's canonical token approval manager. /// Permit2 — Uniswap's canonical token approval manager.
/// Replaces per-contract ERC-20 `approve()` with a single approval hub. /// Replaces per-contract ERC-20 approve() with a single approval hub.
#[derive(Debug)] #[derive(Debug)]
interface IPermit2 { interface IPermit2 {
struct TokenPermissions { struct TokenPermissions {

View File

@@ -1,3 +1,15 @@
pub mod abi;
pub mod safe_signer;
use alloy::{
consensus::TxEip1559,
primitives::{TxKind, U256},
};
use chrono::Utc;
use diesel::{ExpressionMethods as _, QueryDsl as _, QueryResult, insert_into, sqlite::Sqlite};
use diesel_async::{AsyncConnection, RunQueryDsl};
use kameo::actor::ActorRef;
use crate::{ use crate::{
actors::vault::Vault, actors::vault::Vault,
crypto::integrity, crypto::integrity,
@@ -15,18 +27,6 @@ use crate::{
}, },
}; };
use alloy::{
consensus::TxEip1559,
primitives::{TxKind, U256},
};
use chrono::Utc;
use diesel::{ExpressionMethods as _, QueryDsl as _, QueryResult, insert_into, sqlite::Sqlite};
use diesel_async::{AsyncConnection, RunQueryDsl};
use kameo::actor::ActorRef;
pub mod abi;
pub mod safe_signer;
pub mod policies; pub mod policies;
mod utils; mod utils;
@@ -34,7 +34,7 @@ mod utils;
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum PolicyError { pub enum PolicyError {
#[error("Database error")] #[error("Database error")]
Database(#[from] DatabaseError), Database(#[from] crate::db::DatabaseError),
#[error("Transaction violates policy: {0:?}")] #[error("Transaction violates policy: {0:?}")]
Violations(Vec<EvalViolation>), Violations(Vec<EvalViolation>),
#[error("No matching grant found")] #[error("No matching grant found")]
@@ -66,7 +66,7 @@ pub enum AnalyzeError {
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum ListError { pub enum ListError {
#[error("Database error")] #[error("Database error")]
Database(#[from] DatabaseError), Database(#[from] crate::db::DatabaseError),
#[error("Integrity verification failed for grant")] #[error("Integrity verification failed for grant")]
Integrity(#[from] integrity::Error), Integrity(#[from] integrity::Error),
@@ -127,7 +127,7 @@ async fn check_shared_constraints(
.get_result(conn) .get_result(conn)
.await?; .await?;
if count >= rate_limit.count.into() { if count >= rate_limit.count as i64 {
violations.push(EvalViolation::RateLimitExceeded); violations.push(EvalViolation::RateLimitExceeded);
} }
} }
@@ -185,7 +185,7 @@ impl Engine {
.values(&NewEvmTransactionLog { .values(&NewEvmTransactionLog {
grant_id: grant.common_settings_id, grant_id: grant.common_settings_id,
wallet_access_id: context.target.id, wallet_access_id: context.target.id,
chain_id: context.chain.into(), chain_id: context.chain as i32,
eth_value: utils::u256_to_bytes(context.value).to_vec(), eth_value: utils::u256_to_bytes(context.value).to_vec(),
signed_at: Utc::now().into(), signed_at: Utc::now().into(),
}) })
@@ -207,7 +207,7 @@ impl Engine {
} }
impl Engine { impl Engine {
pub const fn new(db: db::DatabasePool, vault: ActorRef<Vault>) -> Self { pub fn new(db: db::DatabasePool, vault: ActorRef<Vault>) -> Self {
Self { db, vault } Self { db, vault }
} }
@@ -226,15 +226,9 @@ impl Engine {
Box::pin(async move { Box::pin(async move {
use schema::evm_basic_grant; use schema::evm_basic_grant;
#[expect(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::as_conversions,
reason = "fixme! #86"
)]
let basic_grant: EvmBasicGrant = insert_into(evm_basic_grant::table) let basic_grant: EvmBasicGrant = insert_into(evm_basic_grant::table)
.values(&NewEvmBasicGrant { .values(&NewEvmBasicGrant {
chain_id: full_grant.shared.chain.into(), chain_id: full_grant.shared.chain as i32,
wallet_access_id: full_grant.shared.wallet_access_id, wallet_access_id: full_grant.shared.wallet_access_id,
valid_from: full_grant.shared.valid_from.map(SqliteTimestamp), valid_from: full_grant.shared.valid_from.map(SqliteTimestamp),
valid_until: full_grant.shared.valid_until.map(SqliteTimestamp), valid_until: full_grant.shared.valid_until.map(SqliteTimestamp),
@@ -319,7 +313,7 @@ impl Engine {
let TxKind::Call(to) = transaction.to else { let TxKind::Call(to) = transaction.to else {
return Err(VetError::ContractCreationNotSupported); return Err(VetError::ContractCreationNotSupported);
}; };
let context = EvalContext { let context = policies::EvalContext {
target, target,
chain: transaction.chain_id, chain: transaction.chain_id,
to, to,
@@ -410,16 +404,10 @@ mod tests {
conn: &mut DatabaseConnection, conn: &mut DatabaseConnection,
shared: &SharedGrantSettings, shared: &SharedGrantSettings,
) -> EvmBasicGrant { ) -> EvmBasicGrant {
#[expect(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::as_conversions,
reason = "fixme! #86"
)]
insert_into(evm_basic_grant::table) insert_into(evm_basic_grant::table)
.values(NewEvmBasicGrant { .values(NewEvmBasicGrant {
wallet_access_id: shared.wallet_access_id, wallet_access_id: shared.wallet_access_id,
chain_id: shared.chain.into(), chain_id: shared.chain as i32,
valid_from: shared.valid_from.map(SqliteTimestamp), valid_from: shared.valid_from.map(SqliteTimestamp),
valid_until: shared.valid_until.map(SqliteTimestamp), valid_until: shared.valid_until.map(SqliteTimestamp),
max_gas_fee_per_gas: shared max_gas_fee_per_gas: shared
@@ -583,7 +571,7 @@ mod tests {
.values(NewEvmTransactionLog { .values(NewEvmTransactionLog {
grant_id: basic_grant.id, grant_id: basic_grant.id,
wallet_access_id: WALLET_ACCESS_ID, wallet_access_id: WALLET_ACCESS_ID,
chain_id: CHAIN_ID.into(), chain_id: CHAIN_ID as i32,
eth_value: super::utils::u256_to_bytes(U256::ZERO).to_vec(), eth_value: super::utils::u256_to_bytes(U256::ZERO).to_vec(),
signed_at: SqliteTimestamp(Utc::now()), signed_at: SqliteTimestamp(Utc::now()),
}) })

View File

@@ -1,8 +1,4 @@
use crate::{ use std::fmt::Display;
crypto::integrity::v1::Integrable,
db::models::{EvmBasicGrant, EvmWalletAccess},
evm::utils,
};
use alloy::primitives::{Address, Bytes, ChainId, U256}; use alloy::primitives::{Address, Bytes, ChainId, U256};
use chrono::{DateTime, Duration, Utc}; use chrono::{DateTime, Duration, Utc};
@@ -10,9 +6,15 @@ use diesel::{
ExpressionMethods as _, QueryDsl, SelectableHelper, result::QueryResult, sqlite::Sqlite, ExpressionMethods as _, QueryDsl, SelectableHelper, result::QueryResult, sqlite::Sqlite,
}; };
use diesel_async::{AsyncConnection, RunQueryDsl}; use diesel_async::{AsyncConnection, RunQueryDsl};
use std::fmt::Display;
use thiserror::Error; use thiserror::Error;
use crate::{
crypto::integrity::v1::Integrable,
db::models::{self, EvmBasicGrant, EvmWalletAccess},
evm::utils,
};
pub mod ether_transfer; pub mod ether_transfer;
pub mod token_transfers; pub mod token_transfers;
@@ -85,10 +87,10 @@ pub trait Policy: Sized {
// Create a new grant in the database based on the provided grant details, and return its ID // Create a new grant in the database based on the provided grant details, and return its ID
fn create_grant( fn create_grant(
basic: &EvmBasicGrant, basic: &models::EvmBasicGrant,
grant: &Self::Settings, grant: &Self::Settings,
conn: &mut impl AsyncConnection<Backend = Sqlite>, conn: &mut impl AsyncConnection<Backend = Sqlite>,
) -> impl Future<Output = QueryResult<DatabaseID>> + Send; ) -> impl std::future::Future<Output = QueryResult<DatabaseID>> + Send;
// Try to find an existing grant that matches the transaction context, and return its details if found // Try to find an existing grant that matches the transaction context, and return its details if found
// Additionally, return ID of basic grant for shared-logic checks like rate limits and validity periods // Additionally, return ID of basic grant for shared-logic checks like rate limits and validity periods
@@ -125,19 +127,19 @@ pub enum SpecificMeaning {
TokenTransfer(token_transfers::Meaning), TokenTransfer(token_transfers::Meaning),
} }
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, arbiter_macros::Hashable)] #[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct TransactionRateLimit { pub struct TransactionRateLimit {
pub count: u32, pub count: u32,
pub window: Duration, pub window: Duration,
} }
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, arbiter_macros::Hashable)] #[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct VolumeRateLimit { pub struct VolumeRateLimit {
pub max_volume: U256, pub max_volume: U256,
pub window: Duration, pub window: Duration,
} }
#[derive(Clone, Debug, PartialEq, Eq, Hash, arbiter_macros::Hashable)] #[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct SharedGrantSettings { pub struct SharedGrantSettings {
pub wallet_access_id: i32, pub wallet_access_id: i32,
pub chain: ChainId, pub chain: ChainId,
@@ -155,7 +157,7 @@ impl SharedGrantSettings {
pub(crate) fn try_from_model(model: EvmBasicGrant) -> QueryResult<Self> { pub(crate) fn try_from_model(model: EvmBasicGrant) -> QueryResult<Self> {
Ok(Self { Ok(Self {
wallet_access_id: model.wallet_access_id, wallet_access_id: model.wallet_access_id,
chain: model.chain_id.into(), 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_from: model.valid_from.map(Into::into),
valid_until: model.valid_until.map(Into::into), valid_until: model.valid_until.map(Into::into),
max_gas_fee_per_gas: model max_gas_fee_per_gas: model
@@ -166,11 +168,10 @@ impl SharedGrantSettings {
.max_priority_fee_per_gas .max_priority_fee_per_gas
.map(|b| utils::try_bytes_to_u256(&b)) .map(|b| utils::try_bytes_to_u256(&b))
.transpose()?, .transpose()?,
#[expect(clippy::cast_sign_loss, clippy::as_conversions, reason = "fixme! #86")]
rate_limit: match (model.rate_limit_count, model.rate_limit_window_secs) { rate_limit: match (model.rate_limit_count, model.rate_limit_window_secs) {
(Some(count), Some(window_secs)) => Some(TransactionRateLimit { (Some(count), Some(window_secs)) => Some(TransactionRateLimit {
count: count as u32, count: count as u32,
window: Duration::seconds(window_secs.into()), window: Duration::seconds(window_secs as i64),
}), }),
_ => None, _ => None,
}, },
@@ -180,7 +181,7 @@ impl SharedGrantSettings {
pub async fn query_by_id( pub async fn query_by_id(
conn: &mut impl AsyncConnection<Backend = Sqlite>, conn: &mut impl AsyncConnection<Backend = Sqlite>,
id: i32, id: i32,
) -> QueryResult<Self> { ) -> diesel::result::QueryResult<Self> {
use crate::db::schema::evm_basic_grant; use crate::db::schema::evm_basic_grant;
let basic_grant: EvmBasicGrant = evm_basic_grant::table let basic_grant: EvmBasicGrant = evm_basic_grant::table
@@ -199,7 +200,7 @@ pub enum SpecificGrant {
TokenTransfer(token_transfers::Settings), TokenTransfer(token_transfers::Settings),
} }
#[derive(Debug, arbiter_macros::Hashable)] #[derive(Debug)]
pub struct CombinedSettings<PolicyGrant> { pub struct CombinedSettings<PolicyGrant> {
pub shared: SharedGrantSettings, pub shared: SharedGrantSettings,
pub specific: PolicyGrant, pub specific: PolicyGrant,
@@ -218,3 +219,38 @@ impl<P: Integrable> Integrable for CombinedSettings<P> {
const KIND: &'static str = P::KIND; const KIND: &'static str = P::KIND;
const VERSION: i32 = P::VERSION; const VERSION: i32 = P::VERSION;
} }
use crate::crypto::integrity::hashing::Hashable;
impl Hashable for TransactionRateLimit {
fn hash<H: sha2::Digest>(&self, hasher: &mut H) {
self.count.hash(hasher);
self.window.hash(hasher);
}
}
impl Hashable for VolumeRateLimit {
fn hash<H: sha2::Digest>(&self, hasher: &mut H) {
self.max_volume.hash(hasher);
self.window.hash(hasher);
}
}
impl Hashable for SharedGrantSettings {
fn hash<H: sha2::Digest>(&self, hasher: &mut H) {
self.wallet_access_id.hash(hasher);
self.chain.hash(hasher);
self.valid_from.hash(hasher);
self.valid_until.hash(hasher);
self.max_gas_fee_per_gas.hash(hasher);
self.max_priority_fee_per_gas.hash(hasher);
self.rate_limit.hash(hasher);
}
}
impl<P: Hashable> Hashable for CombinedSettings<P> {
fn hash<H: sha2::Digest>(&self, hasher: &mut H) {
self.shared.hash(hasher);
self.specific.hash(hasher);
}
}

View File

@@ -1,31 +1,29 @@
use super::{DatabaseID, EvalContext, EvalViolation}; use std::collections::HashMap;
use crate::{ use std::fmt::Display;
crypto::integrity::v1::Integrable,
db::models::{
EvmBasicGrant, EvmEtherTransferGrant, EvmEtherTransferGrantTarget, EvmEtherTransferLimit,
NewEvmEtherTransferLimit, SqliteTimestamp,
},
db::schema::{evm_basic_grant, evm_ether_transfer_limit, evm_transaction_log},
db::{
models::{NewEvmEtherTransferGrant, NewEvmEtherTransferGrantTarget},
schema::{evm_ether_transfer_grant, evm_ether_transfer_grant_target},
},
evm::policies::{
CombinedSettings, Grant, SharedGrantSettings, SpecificGrant, SpecificMeaning,
VolumeRateLimit,
},
evm::{policies::Policy, utils},
};
use alloy::primitives::{Address, U256}; use alloy::primitives::{Address, U256};
use chrono::{DateTime, Duration, Utc}; use chrono::{DateTime, Duration, Utc};
use diesel::{ use diesel::dsl::{auto_type, insert_into};
dsl::{auto_type, insert_into}, use diesel::sqlite::Sqlite;
prelude::*, use diesel::{ExpressionMethods, JoinOnDsl, prelude::*};
sqlite::Sqlite,
};
use diesel_async::{AsyncConnection, RunQueryDsl}; use diesel_async::{AsyncConnection, RunQueryDsl};
use std::{collections::HashMap, fmt::Display};
use crate::crypto::integrity::v1::Integrable;
use crate::db::models::{
EvmBasicGrant, EvmEtherTransferGrant, EvmEtherTransferGrantTarget, EvmEtherTransferLimit,
NewEvmEtherTransferLimit, SqliteTimestamp,
};
use crate::db::schema::{evm_basic_grant, evm_ether_transfer_limit, evm_transaction_log};
use crate::evm::policies::{
CombinedSettings, Grant, SharedGrantSettings, SpecificGrant, SpecificMeaning, VolumeRateLimit,
};
use crate::{
db::{
models::{self, NewEvmEtherTransferGrant, NewEvmEtherTransferGrantTarget},
schema::{evm_ether_transfer_grant, evm_ether_transfer_grant_target},
},
evm::{policies::Policy, utils},
};
#[auto_type] #[auto_type]
fn grant_join() -> _ { fn grant_join() -> _ {
@@ -34,6 +32,8 @@ fn grant_join() -> _ {
) )
} }
use super::{DatabaseID, EvalContext, EvalViolation};
// Plain ether transfer // Plain ether transfer
#[derive(Clone, Debug, PartialEq, Eq, Hash)] #[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Meaning { pub struct Meaning {
@@ -46,13 +46,13 @@ impl Display for Meaning {
} }
} }
impl From<Meaning> for SpecificMeaning { impl From<Meaning> for SpecificMeaning {
fn from(val: Meaning) -> Self { fn from(val: Meaning) -> SpecificMeaning {
Self::EtherTransfer(val) SpecificMeaning::EtherTransfer(val)
} }
} }
// A grant for ether transfers, which can be scoped to specific target addresses and volume limits // A grant for ether transfers, which can be scoped to specific target addresses and volume limits
#[derive(Debug, Clone, arbiter_macros::Hashable)] #[derive(Debug, Clone)]
pub struct Settings { pub struct Settings {
pub target: Vec<Address>, pub target: Vec<Address>,
pub limit: VolumeRateLimit, pub limit: VolumeRateLimit,
@@ -61,9 +61,18 @@ impl Integrable for Settings {
const KIND: &'static str = "EtherTransfer"; const KIND: &'static str = "EtherTransfer";
} }
use crate::crypto::integrity::hashing::Hashable;
impl Hashable for Settings {
fn hash<H: sha2::Digest>(&self, hasher: &mut H) {
self.target.hash(hasher);
self.limit.hash(hasher);
}
}
impl From<Settings> for SpecificGrant { impl From<Settings> for SpecificGrant {
fn from(val: Settings) -> Self { fn from(val: Settings) -> SpecificGrant {
Self::EtherTransfer(val) SpecificGrant::EtherTransfer(val)
} }
} }
@@ -74,7 +83,9 @@ async fn query_relevant_past_transaction(
) -> QueryResult<Vec<(U256, DateTime<Utc>)>> { ) -> QueryResult<Vec<(U256, DateTime<Utc>)>> {
let past_transactions: Vec<(Vec<u8>, SqliteTimestamp)> = evm_transaction_log::table let past_transactions: Vec<(Vec<u8>, SqliteTimestamp)> = evm_transaction_log::table
.filter(evm_transaction_log::grant_id.eq(grant_id)) .filter(evm_transaction_log::grant_id.eq(grant_id))
.filter(evm_transaction_log::signed_at.ge(SqliteTimestamp(Utc::now() - longest_window))) .filter(
evm_transaction_log::signed_at.ge(SqliteTimestamp(chrono::Utc::now() - longest_window)),
)
.select(( .select((
evm_transaction_log::eth_value, evm_transaction_log::eth_value,
evm_transaction_log::signed_at, evm_transaction_log::signed_at,
@@ -101,7 +112,7 @@ async fn check_rate_limits(
let past_transaction = query_relevant_past_transaction(grant.id, window, db).await?; let past_transaction = query_relevant_past_transaction(grant.id, window, db).await?;
let window_start = Utc::now() - grant.settings.specific.limit.window; let window_start = chrono::Utc::now() - grant.settings.specific.limit.window;
let prospective_cumulative_volume: U256 = past_transaction let prospective_cumulative_volume: U256 = past_transaction
.iter() .iter()
.filter(|(_, timestamp)| timestamp >= &window_start) .filter(|(_, timestamp)| timestamp >= &window_start)
@@ -151,15 +162,10 @@ impl Policy for EtherTransfer {
} }
async fn create_grant( async fn create_grant(
basic: &EvmBasicGrant, basic: &models::EvmBasicGrant,
grant: &Self::Settings, grant: &Self::Settings,
conn: &mut impl AsyncConnection<Backend = Sqlite>, conn: &mut impl AsyncConnection<Backend = Sqlite>,
) -> QueryResult<DatabaseID> { ) -> diesel::result::QueryResult<DatabaseID> {
#[expect(
clippy::cast_possible_truncation,
clippy::as_conversions,
reason = "fixme! #86"
)]
let limit_id: i32 = insert_into(evm_ether_transfer_limit::table) let limit_id: i32 = insert_into(evm_ether_transfer_limit::table)
.values(NewEvmEtherTransferLimit { .values(NewEvmEtherTransferLimit {
window_secs: grant.limit.window.num_seconds() as i32, window_secs: grant.limit.window.num_seconds() as i32,
@@ -194,7 +200,7 @@ impl Policy for EtherTransfer {
async fn try_find_grant( async fn try_find_grant(
context: &EvalContext, context: &EvalContext,
conn: &mut impl AsyncConnection<Backend = Sqlite>, conn: &mut impl AsyncConnection<Backend = Sqlite>,
) -> QueryResult<Option<Grant<Self::Settings>>> { ) -> diesel::result::QueryResult<Option<Grant<Self::Settings>>> {
let target_bytes = context.to.to_vec(); let target_bytes = context.to.to_vec();
// Find a grant where: // Find a grant where:
@@ -248,7 +254,7 @@ impl Policy for EtherTransfer {
limit: VolumeRateLimit { limit: VolumeRateLimit {
max_volume: utils::try_bytes_to_u256(&limit.max_volume) max_volume: utils::try_bytes_to_u256(&limit.max_volume)
.map_err(|err| diesel::result::Error::DeserializationError(Box::new(err)))?, .map_err(|err| diesel::result::Error::DeserializationError(Box::new(err)))?,
window: Duration::seconds(limit.window_secs.into()), window: chrono::Duration::seconds(limit.window_secs as i64),
}, },
}; };
@@ -268,7 +274,7 @@ impl Policy for EtherTransfer {
_log_id: i32, _log_id: i32,
_grant: &Grant<Self::Settings>, _grant: &Grant<Self::Settings>,
_conn: &mut impl AsyncConnection<Backend = Sqlite>, _conn: &mut impl AsyncConnection<Backend = Sqlite>,
) -> QueryResult<()> { ) -> diesel::result::QueryResult<()> {
// Basic log is sufficient // Basic log is sufficient
Ok(()) Ok(())
@@ -321,7 +327,7 @@ impl Policy for EtherTransfer {
.map(|(basic, specific)| { .map(|(basic, specific)| {
let targets: Vec<Address> = targets_by_grant let targets: Vec<Address> = targets_by_grant
.get(&specific.id) .get(&specific.id)
.map(Vec::as_slice) .map(|v| v.as_slice())
.unwrap_or_default() .unwrap_or_default()
.iter() .iter()
.filter_map(|t| { .filter_map(|t| {
@@ -345,7 +351,7 @@ impl Policy for EtherTransfer {
max_volume: utils::try_bytes_to_u256(&limit.max_volume).map_err( max_volume: utils::try_bytes_to_u256(&limit.max_volume).map_err(
|e| diesel::result::Error::DeserializationError(Box::new(e)), |e| diesel::result::Error::DeserializationError(Box::new(e)),
)?, )?,
window: Duration::seconds(limit.window_secs.into()), window: Duration::seconds(limit.window_secs as i64),
}, },
}, },
}, },

View File

@@ -1,28 +1,27 @@
use super::{EtherTransfer, Settings};
use crate::{
db::{
self, DatabaseConnection,
models::{
EvmBasicGrant, EvmWalletAccess, NewEvmBasicGrant, NewEvmTransactionLog, SqliteTimestamp,
},
schema::{evm_basic_grant, evm_transaction_log},
},
evm::{
policies::{
CombinedSettings, EvalContext, EvalViolation, Grant, Policy, SharedGrantSettings,
VolumeRateLimit,
},
utils,
},
};
use alloy::primitives::{Address, Bytes, U256, address}; use alloy::primitives::{Address, Bytes, U256, address};
use chrono::{Duration, Utc}; use chrono::{Duration, Utc};
use diesel::{SelectableHelper, insert_into}; use diesel::{SelectableHelper, insert_into};
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
use crate::db::{
self, DatabaseConnection,
models::{
EvmBasicGrant, EvmWalletAccess, NewEvmBasicGrant, NewEvmTransactionLog, SqliteTimestamp,
},
schema::{evm_basic_grant, evm_transaction_log},
};
use crate::evm::{
policies::{
CombinedSettings, EvalContext, EvalViolation, Grant, Policy, SharedGrantSettings,
VolumeRateLimit,
},
utils,
};
use super::{EtherTransfer, Settings};
const WALLET_ACCESS_ID: i32 = 1; const WALLET_ACCESS_ID: i32 = 1;
const CHAIN_ID: alloy::primitives::ChainId = 1; const CHAIN_ID: u64 = 1;
const ALLOWED: Address = address!("1111111111111111111111111111111111111111"); const ALLOWED: Address = address!("1111111111111111111111111111111111111111");
const OTHER: Address = address!("2222222222222222222222222222222222222222"); const OTHER: Address = address!("2222222222222222222222222222222222222222");
@@ -48,7 +47,7 @@ async fn insert_basic(conn: &mut DatabaseConnection, revoked: bool) -> EvmBasicG
insert_into(evm_basic_grant::table) insert_into(evm_basic_grant::table)
.values(NewEvmBasicGrant { .values(NewEvmBasicGrant {
wallet_access_id: WALLET_ACCESS_ID, wallet_access_id: WALLET_ACCESS_ID,
chain_id: CHAIN_ID.into(), chain_id: CHAIN_ID as i32,
valid_from: None, valid_from: None,
valid_until: None, valid_until: None,
max_gas_fee_per_gas: None, max_gas_fee_per_gas: None,
@@ -161,7 +160,7 @@ async fn evaluate_passes_when_volume_within_limit() {
.values(NewEvmTransactionLog { .values(NewEvmTransactionLog {
grant_id, grant_id,
wallet_access_id: WALLET_ACCESS_ID, wallet_access_id: WALLET_ACCESS_ID,
chain_id: CHAIN_ID.into(), chain_id: CHAIN_ID as i32,
eth_value: utils::u256_to_bytes(U256::from(500u64)).to_vec(), eth_value: utils::u256_to_bytes(U256::from(500u64)).to_vec(),
signed_at: SqliteTimestamp(Utc::now()), signed_at: SqliteTimestamp(Utc::now()),
}) })
@@ -203,7 +202,7 @@ async fn evaluate_rejects_volume_over_limit() {
.values(NewEvmTransactionLog { .values(NewEvmTransactionLog {
grant_id, grant_id,
wallet_access_id: WALLET_ACCESS_ID, wallet_access_id: WALLET_ACCESS_ID,
chain_id: CHAIN_ID.into(), chain_id: CHAIN_ID as i32,
eth_value: utils::u256_to_bytes(U256::from(1_000u64)).to_vec(), eth_value: utils::u256_to_bytes(U256::from(1_000u64)).to_vec(),
signed_at: SqliteTimestamp(Utc::now()), signed_at: SqliteTimestamp(Utc::now()),
}) })
@@ -246,7 +245,7 @@ async fn evaluate_passes_at_exactly_volume_limit() {
.values(NewEvmTransactionLog { .values(NewEvmTransactionLog {
grant_id, grant_id,
wallet_access_id: WALLET_ACCESS_ID, wallet_access_id: WALLET_ACCESS_ID,
chain_id: CHAIN_ID.into(), chain_id: CHAIN_ID as i32,
eth_value: utils::u256_to_bytes(U256::from(900u64)).to_vec(), eth_value: utils::u256_to_bytes(U256::from(900u64)).to_vec(),
signed_at: SqliteTimestamp(Utc::now()), signed_at: SqliteTimestamp(Utc::now()),
}) })
@@ -341,7 +340,7 @@ proptest::proptest! {
) { ) {
use rand::{SeedableRng, seq::SliceRandom}; use rand::{SeedableRng, seq::SliceRandom};
use sha2::Digest; use sha2::Digest;
use arbiter_crypto::hashing::Hashable; use crate::crypto::integrity::hashing::Hashable;
let addrs: Vec<Address> = raw_addrs.iter().map(|b| Address::from(*b)).collect(); let addrs: Vec<Address> = raw_addrs.iter().map(|b| Address::from(*b)).collect();
let mut shuffled = addrs.clone(); let mut shuffled = addrs.clone();

View File

@@ -1,4 +1,16 @@
use super::{DatabaseID, EvalContext, EvalViolation}; use std::collections::HashMap;
use crate::db::schema::{
evm_basic_grant, evm_token_transfer_grant, evm_token_transfer_log,
evm_token_transfer_volume_limit,
};
use crate::evm::{
abi::IERC20::transferCall,
policies::{
Grant, Policy, SharedGrantSettings, SpecificGrant, SpecificMeaning, VolumeRateLimit,
},
utils,
};
use crate::{ use crate::{
crypto::integrity::Integrable, crypto::integrity::Integrable,
db::models::{ db::models::{
@@ -6,33 +18,20 @@ use crate::{
NewEvmTokenTransferGrant, NewEvmTokenTransferLog, NewEvmTokenTransferVolumeLimit, NewEvmTokenTransferGrant, NewEvmTokenTransferLog, NewEvmTokenTransferVolumeLimit,
SqliteTimestamp, SqliteTimestamp,
}, },
db::schema::{
evm_basic_grant, evm_token_transfer_grant, evm_token_transfer_log,
evm_token_transfer_volume_limit,
},
evm::policies::CombinedSettings, evm::policies::CombinedSettings,
evm::{
abi::IERC20::transferCall,
policies::{
Grant, Policy, SharedGrantSettings, SpecificGrant, SpecificMeaning, VolumeRateLimit,
},
utils,
},
}; };
use arbiter_tokens_registry::evm::nonfungible::{self, TokenInfo};
use alloy::{ use alloy::{
primitives::{Address, U256}, primitives::{Address, U256},
sol_types::SolCall, sol_types::SolCall,
}; };
use arbiter_tokens_registry::evm::nonfungible::{self, TokenInfo};
use chrono::{DateTime, Duration, Utc}; use chrono::{DateTime, Duration, Utc};
use diesel::{ use diesel::dsl::{auto_type, insert_into};
dsl::{auto_type, insert_into}, use diesel::sqlite::Sqlite;
prelude::*, use diesel::{ExpressionMethods, prelude::*};
sqlite::Sqlite,
};
use diesel_async::{AsyncConnection, RunQueryDsl}; use diesel_async::{AsyncConnection, RunQueryDsl};
use std::collections::HashMap;
use super::{DatabaseID, EvalContext, EvalViolation};
#[auto_type] #[auto_type]
fn grant_join() -> _ { fn grant_join() -> _ {
@@ -57,13 +56,13 @@ impl std::fmt::Display for Meaning {
} }
} }
impl From<Meaning> for SpecificMeaning { impl From<Meaning> for SpecificMeaning {
fn from(val: Meaning) -> Self { fn from(val: Meaning) -> SpecificMeaning {
Self::TokenTransfer(val) SpecificMeaning::TokenTransfer(val)
} }
} }
// A grant for token transfers, which can be scoped to specific target addresses and volume limits // A grant for token transfers, which can be scoped to specific target addresses and volume limits
#[derive(Debug, Clone, arbiter_macros::Hashable)] #[derive(Debug, Clone)]
pub struct Settings { pub struct Settings {
pub token_contract: Address, pub token_contract: Address,
pub target: Option<Address>, pub target: Option<Address>,
@@ -73,9 +72,19 @@ impl Integrable for Settings {
const KIND: &'static str = "TokenTransfer"; const KIND: &'static str = "TokenTransfer";
} }
use crate::crypto::integrity::hashing::Hashable;
impl Hashable for Settings {
fn hash<H: sha2::Digest>(&self, hasher: &mut H) {
self.token_contract.hash(hasher);
self.target.hash(hasher);
self.volume_limits.hash(hasher);
}
}
impl From<Settings> for SpecificGrant { impl From<Settings> for SpecificGrant {
fn from(val: Settings) -> Self { fn from(val: Settings) -> SpecificGrant {
Self::TokenTransfer(val) SpecificGrant::TokenTransfer(val)
} }
} }
@@ -86,7 +95,10 @@ async fn query_relevant_past_transfers(
) -> QueryResult<Vec<(U256, DateTime<Utc>)>> { ) -> QueryResult<Vec<(U256, DateTime<Utc>)>> {
let past_logs: Vec<(Vec<u8>, SqliteTimestamp)> = evm_token_transfer_log::table let past_logs: Vec<(Vec<u8>, SqliteTimestamp)> = evm_token_transfer_log::table
.filter(evm_token_transfer_log::grant_id.eq(grant_id)) .filter(evm_token_transfer_log::grant_id.eq(grant_id))
.filter(evm_token_transfer_log::created_at.ge(SqliteTimestamp(Utc::now() - longest_window))) .filter(
evm_token_transfer_log::created_at
.ge(SqliteTimestamp(chrono::Utc::now() - longest_window)),
)
.select(( .select((
evm_token_transfer_log::value, evm_token_transfer_log::value,
evm_token_transfer_log::created_at, evm_token_transfer_log::created_at,
@@ -126,7 +138,7 @@ async fn check_volume_rate_limits(
let past_transfers = query_relevant_past_transfers(grant.id, longest_window, db).await?; let past_transfers = query_relevant_past_transfers(grant.id, longest_window, db).await?;
for limit in &grant.settings.specific.volume_limits { for limit in &grant.settings.specific.volume_limits {
let window_start = Utc::now() - limit.window; let window_start = chrono::Utc::now() - limit.window;
let prospective_cumulative_volume: U256 = past_transfers let prospective_cumulative_volume: U256 = past_transfers
.iter() .iter()
.filter(|(_, timestamp)| timestamp >= &window_start) .filter(|(_, timestamp)| timestamp >= &window_start)
@@ -202,11 +214,6 @@ impl Policy for TokenTransfer {
.await?; .await?;
for limit in &grant.volume_limits { for limit in &grant.volume_limits {
#[expect(
clippy::cast_possible_truncation,
clippy::as_conversions,
reason = "fixme! #86"
)]
insert_into(evm_token_transfer_volume_limit::table) insert_into(evm_token_transfer_volume_limit::table)
.values(NewEvmTokenTransferVolumeLimit { .values(NewEvmTokenTransferVolumeLimit {
grant_id, grant_id,
@@ -256,7 +263,7 @@ impl Policy for TokenTransfer {
max_volume: utils::try_bytes_to_u256(&row.max_volume).map_err(|err| { max_volume: utils::try_bytes_to_u256(&row.max_volume).map_err(|err| {
diesel::result::Error::DeserializationError(Box::new(err)) diesel::result::Error::DeserializationError(Box::new(err))
})?, })?,
window: Duration::seconds(row.window_secs.into()), window: Duration::seconds(row.window_secs as i64),
}) })
}) })
.collect::<QueryResult<Vec<_>>>()?; .collect::<QueryResult<Vec<_>>>()?;
@@ -306,7 +313,7 @@ impl Policy for TokenTransfer {
.values(NewEvmTokenTransferLog { .values(NewEvmTokenTransferLog {
grant_id: grant.id, grant_id: grant.id,
log_id, log_id,
chain_id: context.chain.into(), chain_id: context.chain as i32,
token_contract: context.to.to_vec(), token_contract: context.to.to_vec(),
recipient_address: meaning.to.to_vec(), recipient_address: meaning.to.to_vec(),
value: utils::u256_to_bytes(meaning.value).to_vec(), value: utils::u256_to_bytes(meaning.value).to_vec(),
@@ -355,7 +362,7 @@ impl Policy for TokenTransfer {
.map(|(basic, specific)| { .map(|(basic, specific)| {
let volume_limits: Vec<VolumeRateLimit> = limits_by_grant let volume_limits: Vec<VolumeRateLimit> = limits_by_grant
.get(&specific.id) .get(&specific.id)
.map(Vec::as_slice) .map(|v| v.as_slice())
.unwrap_or_default() .unwrap_or_default()
.iter() .iter()
.map(|row| { .map(|row| {
@@ -363,7 +370,7 @@ impl Policy for TokenTransfer {
max_volume: utils::try_bytes_to_u256(&row.max_volume).map_err(|e| { max_volume: utils::try_bytes_to_u256(&row.max_volume).map_err(|e| {
diesel::result::Error::DeserializationError(Box::new(e)) diesel::result::Error::DeserializationError(Box::new(e))
})?, })?,
window: Duration::seconds(row.window_secs.into()), window: Duration::seconds(row.window_secs as i64),
}) })
}) })
.collect::<QueryResult<Vec<_>>>()?; .collect::<QueryResult<Vec<_>>>()?;

View File

@@ -1,27 +1,24 @@
use super::{Settings, TokenTransfer}; use alloy::primitives::{Address, Bytes, U256, address};
use crate::{ use alloy::sol_types::SolCall;
db::{ use chrono::{Duration, Utc};
use diesel::{SelectableHelper, insert_into};
use diesel_async::RunQueryDsl;
use crate::db::{
self, DatabaseConnection, self, DatabaseConnection,
models::{EvmBasicGrant, EvmWalletAccess, NewEvmBasicGrant, SqliteTimestamp}, models::{EvmBasicGrant, EvmWalletAccess, NewEvmBasicGrant, SqliteTimestamp},
schema::evm_basic_grant, schema::evm_basic_grant,
}, };
evm::{ use crate::evm::{
abi::IERC20::transferCall, abi::IERC20::transferCall,
policies::{ policies::{
CombinedSettings, EvalContext, EvalViolation, Grant, Policy, SharedGrantSettings, CombinedSettings, EvalContext, EvalViolation, Grant, Policy, SharedGrantSettings,
VolumeRateLimit, VolumeRateLimit,
}, },
utils, utils,
},
}; };
use alloy::{ use super::{Settings, TokenTransfer};
primitives::{Address, Bytes, U256, address},
sol_types::SolCall,
};
use chrono::{Duration, Utc};
use diesel::{SelectableHelper, insert_into};
use diesel_async::RunQueryDsl;
// DAI on Ethereum mainnet — present in the static token registry // DAI on Ethereum mainnet — present in the static token registry
const CHAIN_ID: u64 = 1; const CHAIN_ID: u64 = 1;
@@ -62,7 +59,7 @@ async fn insert_basic(conn: &mut DatabaseConnection, revoked: bool) -> EvmBasicG
insert_into(evm_basic_grant::table) insert_into(evm_basic_grant::table)
.values(NewEvmBasicGrant { .values(NewEvmBasicGrant {
wallet_access_id: WALLET_ACCESS_ID, wallet_access_id: WALLET_ACCESS_ID,
chain_id: CHAIN_ID.into(), chain_id: CHAIN_ID as i32,
valid_from: None, valid_from: None,
valid_until: None, valid_until: None,
max_gas_fee_per_gas: None, max_gas_fee_per_gas: None,
@@ -241,11 +238,12 @@ async fn evaluate_passes_volume_at_exact_limit() {
.unwrap(); .unwrap();
// Record a past transfer of 900, with current transfer 100 => exactly 1000 limit // Record a past transfer of 900, with current transfer 100 => exactly 1000 limit
insert_into(db::schema::evm_token_transfer_log::table) use crate::db::{models::NewEvmTokenTransferLog, schema::evm_token_transfer_log};
.values(db::models::NewEvmTokenTransferLog { insert_into(evm_token_transfer_log::table)
.values(NewEvmTokenTransferLog {
grant_id, grant_id,
log_id: 0, log_id: 0,
chain_id: CHAIN_ID.into(), chain_id: CHAIN_ID as i32,
token_contract: DAI.to_vec(), token_contract: DAI.to_vec(),
recipient_address: RECIPIENT.to_vec(), recipient_address: RECIPIENT.to_vec(),
value: utils::u256_to_bytes(U256::from(900u64)).to_vec(), value: utils::u256_to_bytes(U256::from(900u64)).to_vec(),
@@ -285,11 +283,12 @@ async fn evaluate_rejects_volume_over_limit() {
.await .await
.unwrap(); .unwrap();
insert_into(db::schema::evm_token_transfer_log::table) use crate::db::{models::NewEvmTokenTransferLog, schema::evm_token_transfer_log};
.values(db::models::NewEvmTokenTransferLog { insert_into(evm_token_transfer_log::table)
.values(NewEvmTokenTransferLog {
grant_id, grant_id,
log_id: 0, log_id: 0,
chain_id: CHAIN_ID.into(), chain_id: CHAIN_ID as i32,
token_contract: DAI.to_vec(), token_contract: DAI.to_vec(),
recipient_address: RECIPIENT.to_vec(), recipient_address: RECIPIENT.to_vec(),
value: utils::u256_to_bytes(U256::from(1_000u64)).to_vec(), value: utils::u256_to_bytes(U256::from(1_000u64)).to_vec(),
@@ -420,7 +419,7 @@ proptest::proptest! {
) { ) {
use rand::{SeedableRng, seq::SliceRandom}; use rand::{SeedableRng, seq::SliceRandom};
use sha2::Digest; use sha2::Digest;
use arbiter_crypto::hashing::Hashable; use crate::crypto::integrity::hashing::Hashable;
let limits: Vec<VolumeRateLimit> = raw_limits let limits: Vec<VolumeRateLimit> = raw_limits
.iter() .iter()

View File

@@ -1,4 +1,4 @@
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _}; use std::sync::Mutex;
use alloy::{ use alloy::{
consensus::SignableTransaction, consensus::SignableTransaction,
@@ -6,9 +6,9 @@ use alloy::{
primitives::{Address, B256, ChainId, Signature}, primitives::{Address, B256, ChainId, Signature},
signers::{Error, Result, Signer, SignerSync, utils::secret_key_to_address}, signers::{Error, Result, Signer, SignerSync, utils::secret_key_to_address},
}; };
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
use async_trait::async_trait; use async_trait::async_trait;
use k256::ecdsa::{self, RecoveryId, SigningKey, signature::hazmat::PrehashSigner}; use k256::ecdsa::{self, RecoveryId, SigningKey, signature::hazmat::PrehashSigner};
use std::sync::Mutex;
/// An Ethereum signer that stores its secp256k1 secret key inside a /// An Ethereum signer that stores its secp256k1 secret key inside a
/// hardware-protected [`MemSafe`] cell. /// hardware-protected [`MemSafe`] cell.
@@ -82,8 +82,8 @@ impl SafeSigner {
}) })
} }
#[expect(clippy::significant_drop_tightening, reason = "false positive")]
fn sign_hash_inner(&self, hash: &B256) -> Result<Signature> { fn sign_hash_inner(&self, hash: &B256) -> Result<Signature> {
#[allow(clippy::expect_used)]
let mut cell = self.key.lock().expect("SafeSigner mutex poisoned"); let mut cell = self.key.lock().expect("SafeSigner mutex poisoned");
let reader = cell.read(); let reader = cell.read();
let sig: (ecdsa::Signature, RecoveryId) = reader.sign_prehash(hash.as_ref())?; let sig: (ecdsa::Signature, RecoveryId) = reader.sign_prehash(hash.as_ref())?;
@@ -96,6 +96,7 @@ impl SafeSigner {
{ {
return Err(Error::TransactionChainIdMismatch { return Err(Error::TransactionChainIdMismatch {
signer: chain_id, signer: chain_id,
#[allow(clippy::expect_used)]
tx: tx.chain_id().expect("Chain ID is guaranteed to be set"), tx: tx.chain_id().expect("Chain ID is guaranteed to be set"),
}); });
} }

View File

@@ -2,20 +2,20 @@ use alloy::primitives::U256;
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
#[error("Expected {expected} bytes but got {actual} bytes")] #[error("Expected {expected} bytes but got {actual} bytes")]
pub(super) struct LengthError { pub struct LengthError {
pub(super) expected: usize, pub expected: usize,
pub(super) actual: usize, pub actual: usize,
} }
pub const fn u256_to_bytes(value: U256) -> [u8; 32] { pub fn u256_to_bytes(value: U256) -> [u8; 32] {
value.to_le_bytes() value.to_le_bytes()
} }
pub(super) fn bytes_to_u256(bytes: &[u8]) -> Option<U256> { pub fn bytes_to_u256(bytes: &[u8]) -> Option<U256> {
let bytes: [u8; 32] = bytes.try_into().ok()?; let bytes: [u8; 32] = bytes.try_into().ok()?;
Some(U256::from_le_bytes(bytes)) Some(U256::from_le_bytes(bytes))
} }
pub(super) fn try_bytes_to_u256(bytes: &[u8]) -> diesel::result::QueryResult<U256> { pub fn try_bytes_to_u256(bytes: &[u8]) -> diesel::result::QueryResult<U256> {
let bytes: [u8; 32] = bytes.try_into().map_err(|_| { let bytes: [u8; 32] = bytes.try_into().map_err(|_| {
diesel::result::Error::DeserializationError(Box::new(LengthError { diesel::result::Error::DeserializationError(Box::new(LengthError {
expected: 32, expected: 32,

View File

@@ -1,7 +1,3 @@
use crate::{
grpc::request_tracker::RequestTracker,
peers::client::{ClientConnection, session::ClientSession},
};
use arbiter_proto::{ use arbiter_proto::{
proto::client::{ proto::client::{
ClientRequest, ClientResponse, client_request::Payload as ClientRequestPayload, ClientRequest, ClientResponse, client_request::Payload as ClientRequestPayload,
@@ -9,11 +5,15 @@ use arbiter_proto::{
}, },
transport::{Receiver, Sender, grpc::GrpcBi}, transport::{Receiver, Sender, grpc::GrpcBi},
}; };
use kameo::actor::{ActorRef, Spawn as _}; use kameo::actor::{ActorRef, Spawn as _};
use tonic::Status; use tonic::Status;
use tracing::{info, warn}; use tracing::{info, warn};
use crate::{
grpc::request_tracker::RequestTracker,
peers::client::{ClientConnection, session::ClientSession},
};
mod auth; mod auth;
mod evm; mod evm;
mod inbound; mod inbound;
@@ -98,7 +98,8 @@ pub async fn start(mut conn: ClientConnection, mut bi: GrpcBi<ClientRequest, Cli
Err(err) => { Err(err) => {
let _ = bi let _ = bi
.send(Err(Status::unauthenticated(format!( .send(Err(Status::unauthenticated(format!(
"Authentication failed: {err}", "Authentication failed: {}",
err
)))) ))))
.await; .await;
warn!(error = ?err, "Client authentication failed"); warn!(error = ?err, "Client authentication failed");

View File

@@ -1,7 +1,3 @@
use crate::{
grpc::{Convert, request_tracker::RequestTracker},
peers::client::{ClientConnection, auth},
};
use arbiter_crypto::authn; use arbiter_crypto::authn;
use arbiter_proto::{ use arbiter_proto::{
ClientMetadata, ClientMetadata,
@@ -21,18 +17,22 @@ use arbiter_proto::{
}, },
transport::{Bi, Error as TransportError, Receiver, Sender, grpc::GrpcBi}, transport::{Bi, Error as TransportError, Receiver, Sender, grpc::GrpcBi},
}; };
use async_trait::async_trait; use async_trait::async_trait;
use tonic::Status; use tonic::Status;
use tracing::warn; use tracing::warn;
pub(super) struct AuthTransportAdapter<'a> { use crate::{
grpc::request_tracker::RequestTracker,
peers::client::{self, ClientConnection, auth},
};
pub struct AuthTransportAdapter<'a> {
bi: &'a mut GrpcBi<ClientRequest, ClientResponse>, bi: &'a mut GrpcBi<ClientRequest, ClientResponse>,
request_tracker: &'a mut RequestTracker, request_tracker: &'a mut RequestTracker,
} }
impl<'a> AuthTransportAdapter<'a> { impl<'a> AuthTransportAdapter<'a> {
pub(super) const fn new( pub fn new(
bi: &'a mut GrpcBi<ClientRequest, ClientResponse>, bi: &'a mut GrpcBi<ClientRequest, ClientResponse>,
request_tracker: &'a mut RequestTracker, request_tracker: &'a mut RequestTracker,
) -> Self { ) -> Self {
@@ -42,6 +42,40 @@ impl<'a> AuthTransportAdapter<'a> {
} }
} }
fn response_to_proto(response: auth::Outbound) -> AuthResponsePayload {
match response {
auth::Outbound::AuthChallenge { pubkey, nonce } => {
AuthResponsePayload::Challenge(ProtoAuthChallenge {
pubkey: pubkey.to_bytes(),
nonce,
})
}
auth::Outbound::AuthSuccess => {
AuthResponsePayload::Result(ProtoAuthResult::Success.into())
}
}
}
fn error_to_proto(error: auth::Error) -> AuthResponsePayload {
AuthResponsePayload::Result(
match error {
auth::Error::InvalidChallengeSolution => ProtoAuthResult::InvalidSignature,
auth::Error::ApproveError(auth::ApproveError::Denied) => {
ProtoAuthResult::ApprovalDenied
}
auth::Error::ApproveError(auth::ApproveError::Upstream(
crate::actors::flow_coordinator::ApprovalError::NoUserAgentsConnected,
)) => ProtoAuthResult::NoUserAgentsOnline,
auth::Error::ApproveError(auth::ApproveError::Internal)
| auth::Error::DatabasePoolUnavailable
| auth::Error::DatabaseOperationFailed
| auth::Error::IntegrityCheckFailed
| auth::Error::Transport => ProtoAuthResult::Internal,
}
.into(),
)
}
async fn send_client_response( async fn send_client_response(
&mut self, &mut self,
payload: AuthResponsePayload, payload: AuthResponsePayload,
@@ -69,8 +103,8 @@ impl Sender<Result<auth::Outbound, auth::Error>> for AuthTransportAdapter<'_> {
item: Result<auth::Outbound, auth::Error>, item: Result<auth::Outbound, auth::Error>,
) -> Result<(), TransportError> { ) -> Result<(), TransportError> {
let payload = match item { let payload = match item {
Ok(message) => message.convert(), Ok(message) => AuthTransportAdapter::response_to_proto(message),
Err(err) => err.convert(), Err(err) => AuthTransportAdapter::error_to_proto(err),
}; };
self.send_client_response(payload).await self.send_client_response(payload).await
@@ -133,7 +167,7 @@ impl Receiver<auth::Inbound> for AuthTransportAdapter<'_> {
}; };
Some(auth::Inbound::AuthChallengeRequest { Some(auth::Inbound::AuthChallengeRequest {
pubkey, pubkey,
metadata: client_info.convert(), metadata: client_metadata_from_proto(client_info),
}) })
} }
AuthRequestPayload::ChallengeSolution(ProtoAuthChallengeSolution { signature }) => { AuthRequestPayload::ChallengeSolution(ProtoAuthChallengeSolution { signature }) => {
@@ -151,69 +185,19 @@ impl Receiver<auth::Inbound> for AuthTransportAdapter<'_> {
impl Bi<auth::Inbound, Result<auth::Outbound, auth::Error>> for AuthTransportAdapter<'_> {} impl Bi<auth::Inbound, Result<auth::Outbound, auth::Error>> for AuthTransportAdapter<'_> {}
impl Convert for ProtoClientInfo { fn client_metadata_from_proto(metadata: ProtoClientInfo) -> ClientMetadata {
type Output = ClientMetadata;
fn convert(self) -> Self::Output {
ClientMetadata { ClientMetadata {
name: self.name, name: metadata.name,
description: self.description, description: metadata.description,
version: self.version, version: metadata.version,
}
}
}
impl Convert for auth::Error {
type Output = AuthResponsePayload;
fn convert(self) -> Self::Output {
use auth::Error::{
ApproveError, DatabaseOperationFailed, DatabasePoolUnavailable, IntegrityCheckFailed,
InvalidChallengeSolution, Transport,
};
AuthResponsePayload::Result(
match self {
InvalidChallengeSolution => ProtoAuthResult::InvalidSignature,
ApproveError(auth::ApproveError::Denied) => ProtoAuthResult::ApprovalDenied,
ApproveError(auth::ApproveError::Upstream(
crate::actors::flow_coordinator::ApprovalError::NoUserAgentsConnected,
)) => ProtoAuthResult::NoUserAgentsOnline,
ApproveError(auth::ApproveError::Internal)
| DatabasePoolUnavailable
| DatabaseOperationFailed
| IntegrityCheckFailed
| Transport => ProtoAuthResult::Internal,
}
.into(),
)
} }
} }
impl Convert for auth::Outbound { pub async fn start(
type Output = AuthResponsePayload;
fn convert(self) -> Self::Output {
match self {
Self::AuthChallenge { challenge } => {
AuthResponsePayload::Challenge(ProtoAuthChallenge {
timestamp_nanos: challenge
.timestamp
.timestamp_nanos_opt()
.expect("timestamp within range")
as u64,
random: challenge.nonce.to_vec(),
})
}
Self::AuthSuccess => AuthResponsePayload::Result(ProtoAuthResult::Success.into()),
}
}
}
pub(super) async fn start(
conn: &mut ClientConnection, conn: &mut ClientConnection,
bi: &mut GrpcBi<ClientRequest, ClientResponse>, bi: &mut GrpcBi<ClientRequest, ClientResponse>,
request_tracker: &mut RequestTracker, request_tracker: &mut RequestTracker,
) -> Result<i32, auth::Error> { ) -> Result<i32, auth::Error> {
let mut transport = AuthTransportAdapter::new(bi, request_tracker); let mut transport = AuthTransportAdapter::new(bi, request_tracker);
auth::authenticate(conn, &mut transport).await client::auth::authenticate(conn, &mut transport).await
} }

View File

@@ -1,10 +1,3 @@
use crate::{
grpc::{
Convert, TryConvert,
common::inbound::{RawEvmAddress, RawEvmTransaction},
},
peers::client::session::{ClientSession, HandleSignTransaction, SignTransactionRpcError},
};
use arbiter_proto::proto::{ use arbiter_proto::proto::{
client::{ client::{
client_response::Payload as ClientResponsePayload, client_response::Payload as ClientResponsePayload,
@@ -18,12 +11,19 @@ use arbiter_proto::proto::{
evm_sign_transaction_response::Result as EvmSignTransactionResult, evm_sign_transaction_response::Result as EvmSignTransactionResult,
}, },
}; };
use kameo::actor::ActorRef; use kameo::actor::ActorRef;
use tonic::Status; use tonic::Status;
use tracing::warn; use tracing::warn;
const fn wrap_response(payload: EvmResponsePayload) -> ClientResponsePayload { use crate::{
grpc::{
Convert, TryConvert,
common::inbound::{RawEvmAddress, RawEvmTransaction},
},
peers::client::session::{ClientSession, HandleSignTransaction, SignTransactionRpcError},
};
fn wrap_response(payload: EvmResponsePayload) -> ClientResponsePayload {
ClientResponsePayload::Evm(proto_evm::Response { ClientResponsePayload::Evm(proto_evm::Response {
payload: Some(payload), payload: Some(payload),
}) })

View File

@@ -1,7 +1,3 @@
use crate::{
actors::vault::VaultState,
peers::client::session::{ClientSession, Error, HandleQueryVaultState},
};
use arbiter_proto::proto::{ use arbiter_proto::proto::{
client::{ client::{
client_response::Payload as ClientResponsePayload, client_response::Payload as ClientResponsePayload,
@@ -12,11 +8,15 @@ use arbiter_proto::proto::{
}, },
shared::VaultState as ProtoVaultState, shared::VaultState as ProtoVaultState,
}; };
use kameo::{actor::ActorRef, error::SendError}; use kameo::{actor::ActorRef, error::SendError};
use tonic::Status; use tonic::Status;
use tracing::warn; use tracing::warn;
use crate::{
actors::vault::VaultState,
peers::client::session::{ClientSession, Error, HandleQueryVaultState},
};
pub(super) async fn dispatch( pub(super) async fn dispatch(
actor: &ActorRef<ClientSession>, actor: &ActorRef<ClientSession>,
req: proto_vault::Request, req: proto_vault::Request,
@@ -28,7 +28,7 @@ pub(super) async fn dispatch(
}; };
match payload { match payload {
VaultRequestPayload::QueryState(()) => { VaultRequestPayload::QueryState(_) => {
let state = match actor.ask(HandleQueryVaultState {}).await { let state = match actor.ask(HandleQueryVaultState {}).await {
Ok(VaultState::Unbootstrapped) => ProtoVaultState::Unbootstrapped, Ok(VaultState::Unbootstrapped) => ProtoVaultState::Unbootstrapped,
Ok(VaultState::Sealed) => ProtoVaultState::Sealed, Ok(VaultState::Sealed) => ProtoVaultState::Sealed,

View File

@@ -1,2 +1,2 @@
pub(super) mod inbound; pub mod inbound;
pub(super) mod outbound; pub mod outbound;

View File

@@ -1,8 +1,8 @@
use crate::grpc::TryConvert;
use alloy::{consensus::TxEip1559, primitives::Address, rlp::Decodable as _}; use alloy::{consensus::TxEip1559, primitives::Address, rlp::Decodable as _};
pub(in crate::grpc) struct RawEvmAddress(pub(in crate::grpc) Vec<u8>); use crate::grpc::TryConvert;
pub struct RawEvmAddress(pub Vec<u8>);
impl TryConvert for RawEvmAddress { impl TryConvert for RawEvmAddress {
type Output = Address; type Output = Address;
@@ -21,7 +21,7 @@ impl TryConvert for RawEvmAddress {
} }
} }
pub(in crate::grpc) struct RawEvmTransaction(pub(in crate::grpc) Vec<u8>); pub struct RawEvmTransaction(pub Vec<u8>);
impl TryConvert for RawEvmTransaction { impl TryConvert for RawEvmTransaction {
type Output = TxEip1559; type Output = TxEip1559;

View File

@@ -1,10 +1,4 @@
use crate::{ use alloy::primitives::U256;
evm::{
PolicyError, VetError,
policies::{EvalViolation, SpecificMeaning},
},
grpc::Convert,
};
use arbiter_proto::proto::{ use arbiter_proto::proto::{
evm::{ evm::{
EvmError as ProtoEvmError, EvmError as ProtoEvmError,
@@ -20,7 +14,13 @@ use arbiter_proto::proto::{
}, },
}; };
use alloy::primitives::U256; use crate::{
evm::{
PolicyError, VetError,
policies::{EvalViolation, SpecificMeaning},
},
grpc::Convert,
};
fn u256_to_proto_bytes(value: U256) -> Vec<u8> { fn u256_to_proto_bytes(value: U256) -> Vec<u8> {
value.to_be_bytes::<32>().to_vec() value.to_be_bytes::<32>().to_vec()
@@ -31,16 +31,16 @@ impl Convert for SpecificMeaning {
fn convert(self) -> Self::Output { fn convert(self) -> Self::Output {
let kind = match self { let kind = match self {
Self::EtherTransfer(meaning) => ProtoSpecificMeaningKind::EtherTransfer( SpecificMeaning::EtherTransfer(meaning) => ProtoSpecificMeaningKind::EtherTransfer(
arbiter_proto::proto::shared::evm::EtherTransferMeaning { arbiter_proto::proto::shared::evm::EtherTransferMeaning {
to: meaning.to.to_vec(), to: meaning.to.to_vec(),
value: u256_to_proto_bytes(meaning.value), value: u256_to_proto_bytes(meaning.value),
}, },
), ),
Self::TokenTransfer(meaning) => ProtoSpecificMeaningKind::TokenTransfer( SpecificMeaning::TokenTransfer(meaning) => ProtoSpecificMeaningKind::TokenTransfer(
arbiter_proto::proto::shared::evm::TokenTransferMeaning { arbiter_proto::proto::shared::evm::TokenTransferMeaning {
token: Some(ProtoTokenInfo { token: Some(ProtoTokenInfo {
symbol: meaning.token.symbol.to_owned(), symbol: meaning.token.symbol.to_string(),
address: meaning.token.contract.to_vec(), address: meaning.token.contract.to_vec(),
chain_id: meaning.token.chain, chain_id: meaning.token.chain,
}), }),
@@ -61,21 +61,25 @@ impl Convert for EvalViolation {
fn convert(self) -> Self::Output { fn convert(self) -> Self::Output {
let kind = match self { let kind = match self {
Self::InvalidTarget { target } => { EvalViolation::InvalidTarget { target } => {
ProtoEvalViolationKind::InvalidTarget(target.to_vec()) ProtoEvalViolationKind::InvalidTarget(target.to_vec())
} }
Self::GasLimitExceeded { EvalViolation::GasLimitExceeded {
max_gas_fee_per_gas, max_gas_fee_per_gas,
max_priority_fee_per_gas, max_priority_fee_per_gas,
} => ProtoEvalViolationKind::GasLimitExceeded(GasLimitExceededViolation { } => ProtoEvalViolationKind::GasLimitExceeded(GasLimitExceededViolation {
max_gas_fee_per_gas: max_gas_fee_per_gas.map(u256_to_proto_bytes), max_gas_fee_per_gas: max_gas_fee_per_gas.map(u256_to_proto_bytes),
max_priority_fee_per_gas: max_priority_fee_per_gas.map(u256_to_proto_bytes), max_priority_fee_per_gas: max_priority_fee_per_gas.map(u256_to_proto_bytes),
}), }),
Self::RateLimitExceeded => ProtoEvalViolationKind::RateLimitExceeded(()), EvalViolation::RateLimitExceeded => ProtoEvalViolationKind::RateLimitExceeded(()),
Self::VolumetricLimitExceeded => ProtoEvalViolationKind::VolumetricLimitExceeded(()), EvalViolation::VolumetricLimitExceeded => {
Self::InvalidTime => ProtoEvalViolationKind::InvalidTime(()), ProtoEvalViolationKind::VolumetricLimitExceeded(())
Self::InvalidTransactionType => ProtoEvalViolationKind::InvalidTransactionType(()), }
Self::MismatchingChainId { expected, actual } => { EvalViolation::InvalidTime => ProtoEvalViolationKind::InvalidTime(()),
EvalViolation::InvalidTransactionType => {
ProtoEvalViolationKind::InvalidTransactionType(())
}
EvalViolation::MismatchingChainId { expected, actual } => {
ProtoEvalViolationKind::ChainIdMismatch(proto_eval_violation::ChainIdMismatch { ProtoEvalViolationKind::ChainIdMismatch(proto_eval_violation::ChainIdMismatch {
expected, expected,
actual, actual,
@@ -92,13 +96,13 @@ impl Convert for VetError {
fn convert(self) -> Self::Output { fn convert(self) -> Self::Output {
let kind = match self { let kind = match self {
Self::ContractCreationNotSupported => { VetError::ContractCreationNotSupported => {
ProtoTransactionEvalErrorKind::ContractCreationNotSupported(()) ProtoTransactionEvalErrorKind::ContractCreationNotSupported(())
} }
Self::UnsupportedTransactionType => { VetError::UnsupportedTransactionType => {
ProtoTransactionEvalErrorKind::UnsupportedTransactionType(()) ProtoTransactionEvalErrorKind::UnsupportedTransactionType(())
} }
Self::Evaluated(meaning, policy_error) => match policy_error { VetError::Evaluated(meaning, policy_error) => match policy_error {
PolicyError::NoMatchingGrant => { PolicyError::NoMatchingGrant => {
ProtoTransactionEvalErrorKind::NoMatchingGrant(NoMatchingGrantError { ProtoTransactionEvalErrorKind::NoMatchingGrant(NoMatchingGrantError {
meaning: Some(meaning.convert()), meaning: Some(meaning.convert()),

View File

@@ -1,4 +1,3 @@
use crate::peers::{client::ClientConnection, user_agent::UserAgentConnection};
use arbiter_proto::{ use arbiter_proto::{
proto::{ proto::{
client::{ClientRequest, ClientResponse}, client::{ClientRequest, ClientResponse},
@@ -6,11 +5,14 @@ use arbiter_proto::{
}, },
transport::grpc::GrpcBi, transport::grpc::GrpcBi,
}; };
use tokio_stream::wrappers::ReceiverStream; use tokio_stream::wrappers::ReceiverStream;
use tonic::{Request, Response, Status, async_trait}; use tonic::{Request, Response, Status, async_trait};
use tracing::info; use tracing::info;
use crate::{
peers::{client::ClientConnection, user_agent::UserAgentConnection},
};
mod request_tracker; mod request_tracker;
pub mod client; pub mod client;

View File

@@ -1,12 +1,12 @@
use tonic::Status; use tonic::Status;
#[derive(Default)] #[derive(Default)]
pub(super) struct RequestTracker { pub struct RequestTracker {
next_request_id: i32, next_request_id: i32,
} }
impl RequestTracker { impl RequestTracker {
pub(super) fn request(&mut self, id: i32) -> Result<i32, Status> { pub fn request(&mut self, id: i32) -> Result<i32, Status> {
if id < self.next_request_id { if id < self.next_request_id {
return Err(Status::invalid_argument("Duplicate request id")); return Err(Status::invalid_argument("Duplicate request id"));
} }
@@ -20,7 +20,7 @@ impl RequestTracker {
// This is used to set the response id for auth responses, which need to match the request id of the auth challenge request. // This is used to set the response id for auth responses, which need to match the request id of the auth challenge request.
// -1 offset is needed because request() increments the next_request_id after returning the current request id. // -1 offset is needed because request() increments the next_request_id after returning the current request id.
pub(super) const fn current_request_id(&self) -> i32 { pub fn current_request_id(&self) -> i32 {
self.next_request_id - 1 self.next_request_id - 1
} }
} }

View File

@@ -1,7 +1,5 @@
use crate::{ use tokio::sync::{mpsc, oneshot};
grpc::request_tracker::RequestTracker,
peers::user_agent::{OutOfBand, UserAgentConnection, UserAgentSession},
};
use arbiter_proto::{ use arbiter_proto::{
proto::user_agent::{ proto::user_agent::{
UserAgentRequest, UserAgentResponse, UserAgentRequest, UserAgentResponse,
@@ -10,13 +8,20 @@ use arbiter_proto::{
}, },
transport::{Error as TransportError, Receiver, Sender, grpc::GrpcBi}, transport::{Error as TransportError, Receiver, Sender, grpc::GrpcBi},
}; };
use async_trait::async_trait; use async_trait::async_trait;
use kameo::actor::ActorRef; use kameo::actor::{ActorRef, Spawn as _};
use tokio::sync::mpsc;
use tonic::Status; use tonic::Status;
use tracing::{error, info, warn}; use tracing::{error, info, warn};
use crate::{
crypto::integrity,
grpc::request_tracker::RequestTracker,
peers::user_agent::{
Credentials, OutOfBand, UserAgentConnection, UserAgentSession,
vault_gate::VaultGate,
},
};
mod auth; mod auth;
mod evm; mod evm;
mod inbound; mod inbound;
@@ -124,22 +129,115 @@ pub async fn start(
) { ) {
let mut request_tracker = RequestTracker::default(); let mut request_tracker = RequestTracker::default();
let (oob_sender, oob_receiver) = mpsc::channel(16); let auth_creds = match auth::start(&mut conn, &mut bi, &mut request_tracker).await {
let oob_adapter = OutOfBandAdapter(oob_sender); Ok(creds) => creds,
let actor = {
let transport = auth::AuthTransportAdapter::new(&mut bi, &mut request_tracker);
match crate::peers::user_agent::start(&mut conn, transport, Box::new(oob_adapter)).await {
Ok(actor) => actor,
Err(e) => { Err(e) => {
warn!(error = ?e, "User agent connection failed"); warn!(error = ?e, "Authentication failed");
return; return;
} }
}
}; };
info!("User agent session established"); info!(pubkey = ?auth_creds.creds.pubkey, "User authenticated successfully");
dispatch_loop(bi, actor.clone(), oob_receiver, request_tracker).await; let creds = if integrity::is_signing_available(&conn.actors.vault)
actor.kill(); .await
.unwrap_or(false)
{
// Vault is unsealed; integrity was verified during auth — promote directly.
auth_creds.creds
} else {
// Vault is sealed/unbootstrapped; run the VaultGate phase.
let (promotion_tx, promotion_rx) = oneshot::channel();
let gate = VaultGate::spawn(VaultGate::new(
auth_creds,
conn.actors.clone(),
conn.db.clone(),
promotion_tx,
));
let result = vault_gate_loop(&mut bi, &gate, &mut request_tracker, promotion_rx).await;
gate.kill();
match result {
Some(creds) => creds,
None => return,
}
};
let (oob_sender, oob_receiver) = mpsc::channel(16);
let oob_adapter = OutOfBandAdapter(oob_sender);
let actor = UserAgentSession::spawn(UserAgentSession::new(conn, creds, Box::new(oob_adapter)));
let actor_for_cleanup = actor.clone();
dispatch_loop(bi, actor, oob_receiver, request_tracker).await;
actor_for_cleanup.kill();
}
async fn vault_gate_loop(
bi: &mut GrpcBi<UserAgentRequest, UserAgentResponse>,
gate: &ActorRef<VaultGate>,
request_tracker: &mut RequestTracker,
mut promotion_rx: oneshot::Receiver<Result<Credentials, crate::peers::user_agent::vault_gate::Error>>,
) -> Option<Credentials> {
loop {
tokio::select! {
result = &mut promotion_rx => {
return match result {
Ok(Ok(creds)) => Some(creds),
Ok(Err(e)) => {
warn!(error = ?e, "VaultGate promotion failed");
None
}
Err(_) => {
warn!("VaultGate promotion channel closed unexpectedly");
None
}
};
}
message = bi.recv() => {
let Some(message) = message else { return None; };
let conn = match message {
Ok(conn) => conn,
Err(err) => {
warn!(error = ?err, "Failed to receive request during vault gate phase");
return None;
}
};
let request_id = match request_tracker.request(conn.id) {
Ok(id) => id,
Err(err) => {
let _ = bi.send(Err(err)).await;
return None;
}
};
let Some(payload) = conn.payload else {
let _ = bi.send(Err(Status::invalid_argument("Missing request payload"))).await;
return None;
};
let response = match payload {
UserAgentRequestPayload::Vault(req) => vault_gate::dispatch(gate, req).await,
_ => Err(Status::permission_denied("Only vault operations are permitted before unsealing")),
};
match response {
Ok(Some(payload)) => {
if bi.send(Ok(UserAgentResponse { id: Some(request_id), payload: Some(payload) })).await.is_err() {
return None;
}
}
Ok(None) => {}
Err(status) => {
let _ = bi.send(Err(status)).await;
return None;
}
}
}
}
}
} }

View File

@@ -1,4 +1,3 @@
use crate::{grpc::request_tracker::RequestTracker, peers::user_agent::auth};
use arbiter_crypto::authn; use arbiter_crypto::authn;
use arbiter_proto::{ use arbiter_proto::{
proto::user_agent::{ proto::user_agent::{
@@ -14,18 +13,22 @@ use arbiter_proto::{
}, },
transport::{Bi, Error as TransportError, Receiver, Sender, grpc::GrpcBi}, transport::{Bi, Error as TransportError, Receiver, Sender, grpc::GrpcBi},
}; };
use async_trait::async_trait; use async_trait::async_trait;
use tonic::Status; use tonic::Status;
use tracing::warn; use tracing::warn;
pub(super) struct AuthTransportAdapter<'a> { use crate::{
grpc::request_tracker::RequestTracker,
peers::user_agent::{AuthCredentials, UserAgentConnection, auth},
};
pub struct AuthTransportAdapter<'a> {
pub(super) bi: &'a mut GrpcBi<UserAgentRequest, UserAgentResponse>, pub(super) bi: &'a mut GrpcBi<UserAgentRequest, UserAgentResponse>,
pub(super) request_tracker: &'a mut RequestTracker, pub(super) request_tracker: &'a mut RequestTracker,
} }
impl<'a> AuthTransportAdapter<'a> { impl<'a> AuthTransportAdapter<'a> {
pub(super) const fn new( pub fn new(
bi: &'a mut GrpcBi<UserAgentRequest, UserAgentResponse>, bi: &'a mut GrpcBi<UserAgentRequest, UserAgentResponse>,
request_tracker: &'a mut RequestTracker, request_tracker: &'a mut RequestTracker,
) -> Self { ) -> Self {
@@ -35,11 +38,11 @@ impl<'a> AuthTransportAdapter<'a> {
} }
} }
pub(super) const fn bi_mut(&mut self) -> &mut GrpcBi<UserAgentRequest, UserAgentResponse> { pub(super) fn bi_mut(&mut self) -> &mut GrpcBi<UserAgentRequest, UserAgentResponse> {
self.bi self.bi
} }
pub(super) const fn tracker_mut(&mut self) -> &mut RequestTracker { pub(super) fn tracker_mut(&mut self) -> &mut RequestTracker {
self.request_tracker self.request_tracker
} }
@@ -74,15 +77,8 @@ impl Sender<Result<auth::Outbound, auth::Error>> for AuthTransportAdapter<'_> {
) -> Result<(), TransportError> { ) -> Result<(), TransportError> {
use auth::{Error, Outbound}; use auth::{Error, Outbound};
let payload = match item { let payload = match item {
Ok(Outbound::AuthChallenge { challenge }) => { Ok(Outbound::AuthChallenge { nonce }) => {
AuthResponsePayload::Challenge(ProtoAuthChallenge { AuthResponsePayload::Challenge(ProtoAuthChallenge { nonce })
timestamp_nanos: challenge
.timestamp
.timestamp_nanos_opt()
.expect("timestamp within range")
as u64,
random: challenge.nonce.to_vec(),
})
} }
Ok(Outbound::AuthSuccess) => { Ok(Outbound::AuthSuccess) => {
AuthResponsePayload::Result(ProtoAuthResult::Success.into()) AuthResponsePayload::Result(ProtoAuthResult::Success.into())
@@ -182,3 +178,12 @@ impl Receiver<auth::Inbound> for AuthTransportAdapter<'_> {
} }
impl Bi<auth::Inbound, Result<auth::Outbound, auth::Error>> for AuthTransportAdapter<'_> {} impl Bi<auth::Inbound, Result<auth::Outbound, auth::Error>> for AuthTransportAdapter<'_> {}
pub async fn start(
conn: &mut UserAgentConnection,
bi: &mut GrpcBi<UserAgentRequest, UserAgentResponse>,
request_tracker: &mut RequestTracker,
) -> Result<AuthCredentials, auth::Error> {
let mut transport = AuthTransportAdapter::new(bi, request_tracker);
auth::authenticate(conn, &mut transport).await
}

View File

@@ -1,17 +1,3 @@
use crate::{
grpc::{
Convert, TryConvert,
common::inbound::{RawEvmAddress, RawEvmTransaction},
},
peers::user_agent::{
UserAgentSession,
session::handlers::{
GrantMutationError, HandleEvmWalletCreate, HandleEvmWalletList, HandleGrantCreate,
HandleGrantDelete, HandleGrantList, HandleSignTransaction,
SignTransactionError as SessionSignTransactionError,
},
},
};
use arbiter_proto::proto::{ use arbiter_proto::proto::{
evm::{ evm::{
EvmError as ProtoEvmError, EvmGrantCreateRequest, EvmGrantCreateResponse, EvmError as ProtoEvmError, EvmGrantCreateRequest, EvmGrantCreateResponse,
@@ -32,12 +18,26 @@ use arbiter_proto::proto::{
user_agent_response::Payload as UserAgentResponsePayload, user_agent_response::Payload as UserAgentResponsePayload,
}, },
}; };
use kameo::actor::ActorRef; use kameo::actor::ActorRef;
use tonic::Status; use tonic::Status;
use tracing::warn; use tracing::warn;
const fn wrap_evm_response(payload: EvmResponsePayload) -> UserAgentResponsePayload { use crate::{
grpc::{
Convert, TryConvert,
common::inbound::{RawEvmAddress, RawEvmTransaction},
},
peers::user_agent::{
UserAgentSession,
session::handlers::{
GrantMutationError, HandleEvmWalletCreate, HandleEvmWalletList, HandleGrantCreate,
HandleGrantDelete, HandleGrantList, HandleSignTransaction,
SignTransactionError as SessionSignTransactionError,
},
},
};
fn wrap_evm_response(payload: EvmResponsePayload) -> UserAgentResponsePayload {
UserAgentResponsePayload::Evm(proto_evm::Response { UserAgentResponsePayload::Evm(proto_evm::Response {
payload: Some(payload), payload: Some(payload),
}) })
@@ -52,8 +52,8 @@ pub(super) async fn dispatch(
}; };
match payload { match payload {
EvmRequestPayload::WalletCreate(()) => handle_wallet_create(actor).await, EvmRequestPayload::WalletCreate(_) => handle_wallet_create(actor).await,
EvmRequestPayload::WalletList(()) => handle_wallet_list(actor).await, EvmRequestPayload::WalletList(_) => handle_wallet_list(actor).await,
EvmRequestPayload::GrantCreate(req) => handle_grant_create(actor, req).await, EvmRequestPayload::GrantCreate(req) => handle_grant_create(actor, req).await,
EvmRequestPayload::GrantDelete(req) => handle_grant_delete(actor, req).await, EvmRequestPayload::GrantDelete(req) => handle_grant_delete(actor, req).await,
EvmRequestPayload::GrantList(_) => handle_grant_list(actor).await, EvmRequestPayload::GrantList(_) => handle_grant_list(actor).await,

View File

@@ -1,32 +1,32 @@
use crate::{ use alloy::primitives::{Address, U256};
db::models::{CoreEvmWalletAccess, NewEvmWalletAccess}, use arbiter_proto::proto::evm::{
evm::policies::{
SharedGrantSettings, SpecificGrant, TransactionRateLimit, VolumeRateLimit, ether_transfer,
token_transfers,
},
grpc::Convert,
grpc::TryConvert,
};
use arbiter_proto::{
proto::evm::{
EtherTransferSettings as ProtoEtherTransferSettings, SharedSettings as ProtoSharedSettings, EtherTransferSettings as ProtoEtherTransferSettings, SharedSettings as ProtoSharedSettings,
SpecificGrant as ProtoSpecificGrant, TokenTransferSettings as ProtoTokenTransferSettings, SpecificGrant as ProtoSpecificGrant, TokenTransferSettings as ProtoTokenTransferSettings,
TransactionRateLimit as ProtoTransactionRateLimit, VolumeRateLimit as ProtoVolumeRateLimit, TransactionRateLimit as ProtoTransactionRateLimit, VolumeRateLimit as ProtoVolumeRateLimit,
specific_grant::Grant as ProtoSpecificGrantType, specific_grant::Grant as ProtoSpecificGrantType,
},
proto::user_agent::sdk_client::{WalletAccess, WalletAccessEntry as SdkClientWalletAccess},
}; };
use arbiter_proto::proto::user_agent::sdk_client::{
use alloy::primitives::{Address, U256}; WalletAccess, WalletAccessEntry as SdkClientWalletAccess,
};
use chrono::{DateTime, TimeZone, Utc}; use chrono::{DateTime, TimeZone, Utc};
use prost_types::Timestamp as ProtoTimestamp; use prost_types::Timestamp as ProtoTimestamp;
use tonic::Status; use tonic::Status;
fn address_from_bytes(bytes: &[u8]) -> Result<Address, Status> { use crate::db::models::{CoreEvmWalletAccess, NewEvmWalletAccess};
use crate::grpc::Convert;
use crate::{
evm::policies::{
SharedGrantSettings, SpecificGrant, TransactionRateLimit, VolumeRateLimit, ether_transfer,
token_transfers,
},
grpc::TryConvert,
};
fn address_from_bytes(bytes: Vec<u8>) -> Result<Address, Status> {
if bytes.len() != 20 { if bytes.len() != 20 {
return Err(Status::invalid_argument("Invalid EVM address")); return Err(Status::invalid_argument("Invalid EVM address"));
} }
Ok(Address::from_slice(bytes)) Ok(Address::from_slice(&bytes))
} }
fn u256_from_proto_bytes(bytes: &[u8]) -> Result<U256, Status> { fn u256_from_proto_bytes(bytes: &[u8]) -> Result<U256, Status> {
@@ -41,7 +41,7 @@ impl TryConvert for ProtoTimestamp {
type Error = Status; type Error = Status;
fn try_convert(self) -> Result<DateTime<Utc>, Status> { fn try_convert(self) -> Result<DateTime<Utc>, Status> {
Utc.timestamp_opt(self.seconds, self.nanos.try_into().unwrap_or_default()) Utc.timestamp_opt(self.seconds, self.nanos as u32)
.single() .single()
.ok_or_else(|| Status::invalid_argument("Invalid timestamp")) .ok_or_else(|| Status::invalid_argument("Invalid timestamp"))
} }
@@ -116,8 +116,7 @@ impl TryConvert for ProtoSpecificGrant {
limit, limit,
})) => Ok(SpecificGrant::EtherTransfer(ether_transfer::Settings { })) => Ok(SpecificGrant::EtherTransfer(ether_transfer::Settings {
target: targets target: targets
.iter() .into_iter()
.map(Vec::as_slice)
.map(address_from_bytes) .map(address_from_bytes)
.collect::<Result<_, _>>()?, .collect::<Result<_, _>>()?,
limit: limit limit: limit
@@ -131,10 +130,8 @@ impl TryConvert for ProtoSpecificGrant {
target, target,
volume_limits, volume_limits,
})) => Ok(SpecificGrant::TokenTransfer(token_transfers::Settings { })) => Ok(SpecificGrant::TokenTransfer(token_transfers::Settings {
token_contract: address_from_bytes(&token_contract)?, token_contract: address_from_bytes(token_contract)?,
target: target target: target.map(address_from_bytes).transpose()?,
.map(|target| address_from_bytes(&target))
.transpose()?,
volume_limits: volume_limits volume_limits: volume_limits
.into_iter() .into_iter()
.map(ProtoVolumeRateLimit::try_convert) .map(ProtoVolumeRateLimit::try_convert)

View File

@@ -1,8 +1,3 @@
use crate::{
db::models::EvmWalletAccess,
evm::policies::{SharedGrantSettings, SpecificGrant, TransactionRateLimit, VolumeRateLimit},
grpc::Convert,
};
use arbiter_proto::proto::{ use arbiter_proto::proto::{
evm::{ evm::{
EtherTransferSettings as ProtoEtherTransferSettings, SharedSettings as ProtoSharedSettings, EtherTransferSettings as ProtoEtherTransferSettings, SharedSettings as ProtoSharedSettings,
@@ -12,17 +7,22 @@ use arbiter_proto::proto::{
}, },
user_agent::sdk_client::{WalletAccess, WalletAccessEntry as ProtoSdkClientWalletAccess}, user_agent::sdk_client::{WalletAccess, WalletAccessEntry as ProtoSdkClientWalletAccess},
}; };
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use prost_types::Timestamp as ProtoTimestamp; use prost_types::Timestamp as ProtoTimestamp;
use crate::{
db::models::EvmWalletAccess,
evm::policies::{SharedGrantSettings, SpecificGrant, TransactionRateLimit, VolumeRateLimit},
grpc::Convert,
};
impl Convert for DateTime<Utc> { impl Convert for DateTime<Utc> {
type Output = ProtoTimestamp; type Output = ProtoTimestamp;
fn convert(self) -> ProtoTimestamp { fn convert(self) -> ProtoTimestamp {
ProtoTimestamp { ProtoTimestamp {
seconds: self.timestamp(), seconds: self.timestamp(),
nanos: self.timestamp_subsec_nanos().try_into().unwrap_or(i32::MAX), nanos: self.timestamp_subsec_nanos() as i32,
} }
} }
} }
@@ -74,13 +74,13 @@ impl Convert for SpecificGrant {
fn convert(self) -> ProtoSpecificGrant { fn convert(self) -> ProtoSpecificGrant {
let grant = match self { let grant = match self {
Self::EtherTransfer(s) => { SpecificGrant::EtherTransfer(s) => {
ProtoSpecificGrantType::EtherTransfer(ProtoEtherTransferSettings { ProtoSpecificGrantType::EtherTransfer(ProtoEtherTransferSettings {
targets: s.target.into_iter().map(|a| a.to_vec()).collect(), targets: s.target.into_iter().map(|a| a.to_vec()).collect(),
limit: Some(s.limit.convert()), limit: Some(s.limit.convert()),
}) })
} }
Self::TokenTransfer(s) => { SpecificGrant::TokenTransfer(s) => {
ProtoSpecificGrantType::TokenTransfer(ProtoTokenTransferSettings { ProtoSpecificGrantType::TokenTransfer(ProtoTokenTransferSettings {
token_contract: s.token_contract.to_vec(), token_contract: s.token_contract.to_vec(),
target: s.target.map(|a| a.to_vec()), target: s.target.map(|a| a.to_vec()),

View File

@@ -1,14 +1,3 @@
use crate::{
db::models::NewEvmWalletAccess,
grpc::Convert,
peers::user_agent::{
OutOfBand, UserAgentSession,
session::handlers::{
HandleGrantEvmWalletAccess, HandleListWalletAccess, HandleNewClientApprove,
HandleRevokeEvmWalletAccess, HandleSdkClientList,
},
},
};
use arbiter_crypto::authn; use arbiter_crypto::authn;
use arbiter_proto::proto::{ use arbiter_proto::proto::{
shared::ClientInfo as ProtoClientMetadata, shared::ClientInfo as ProtoClientMetadata,
@@ -27,12 +16,23 @@ use arbiter_proto::proto::{
user_agent_response::Payload as UserAgentResponsePayload, user_agent_response::Payload as UserAgentResponsePayload,
}, },
}; };
use kameo::actor::ActorRef; use kameo::actor::ActorRef;
use tonic::Status; use tonic::Status;
use tracing::{info, warn}; use tracing::{info, warn};
const fn wrap_sdk_client_response(payload: SdkClientResponsePayload) -> UserAgentResponsePayload { use crate::{
db::models::NewEvmWalletAccess,
grpc::Convert,
peers::user_agent::{
OutOfBand, UserAgentSession,
session::handlers::{
HandleGrantEvmWalletAccess, HandleListWalletAccess, HandleNewClientApprove,
HandleRevokeEvmWalletAccess, HandleSdkClientList,
},
},
};
fn wrap_sdk_client_response(payload: SdkClientResponsePayload) -> UserAgentResponsePayload {
UserAgentResponsePayload::SdkClient(proto_sdk_client::Response { UserAgentResponsePayload::SdkClient(proto_sdk_client::Response {
payload: Some(payload), payload: Some(payload),
}) })
@@ -75,14 +75,14 @@ pub(super) async fn dispatch(
SdkClientRequestPayload::Revoke(_) => Err(Status::unimplemented( SdkClientRequestPayload::Revoke(_) => Err(Status::unimplemented(
"SdkClientRevoke is not yet implemented", "SdkClientRevoke is not yet implemented",
)), )),
SdkClientRequestPayload::List(()) => handle_list(actor).await, SdkClientRequestPayload::List(_) => handle_list(actor).await,
SdkClientRequestPayload::GrantWalletAccess(req) => { SdkClientRequestPayload::GrantWalletAccess(req) => {
handle_grant_wallet_access(actor, req).await handle_grant_wallet_access(actor, req).await
} }
SdkClientRequestPayload::RevokeWalletAccess(req) => { SdkClientRequestPayload::RevokeWalletAccess(req) => {
handle_revoke_wallet_access(actor, req).await handle_revoke_wallet_access(actor, req).await
} }
SdkClientRequestPayload::ListWalletAccess(()) => handle_list_wallet_access(actor).await, SdkClientRequestPayload::ListWalletAccess(_) => handle_list_wallet_access(actor).await,
} }
} }
@@ -91,7 +91,7 @@ async fn handle_connection_response(
resp: ProtoSdkClientConnectionResponse, resp: ProtoSdkClientConnectionResponse,
) -> Result<Option<UserAgentResponsePayload>, Status> { ) -> Result<Option<UserAgentResponsePayload>, Status> {
let pubkey = authn::PublicKey::try_from(resp.pubkey.as_slice()) let pubkey = authn::PublicKey::try_from(resp.pubkey.as_slice())
.map_err(|()| Status::invalid_argument("Invalid ML-DSA public key"))?; .map_err(|_| Status::invalid_argument("Invalid ML-DSA public key"))?;
actor actor
.ask(HandleNewClientApprove { .ask(HandleNewClientApprove {
@@ -116,17 +116,12 @@ async fn handle_list(
.into_iter() .into_iter()
.map(|(client, metadata)| ProtoSdkClientEntry { .map(|(client, metadata)| ProtoSdkClientEntry {
id: client.id, id: client.id,
pubkey: client.public_key.clone(), pubkey: client.public_key.to_vec(),
info: Some(ProtoClientMetadata { info: Some(ProtoClientMetadata {
name: metadata.name, name: metadata.name,
description: metadata.description, description: metadata.description,
version: metadata.version, version: metadata.version,
}), }),
#[expect(
clippy::cast_possible_truncation,
clippy::as_conversions,
reason = "fixme! #84"
)]
created_at: client.created_at.0.timestamp() as i32, created_at: client.created_at.0.timestamp() as i32,
}) })
.collect(), .collect(),
@@ -147,7 +142,7 @@ async fn handle_grant_wallet_access(
actor: &ActorRef<UserAgentSession>, actor: &ActorRef<UserAgentSession>,
req: ProtoSdkClientGrantWalletAccess, req: ProtoSdkClientGrantWalletAccess,
) -> Result<Option<UserAgentResponsePayload>, Status> { ) -> Result<Option<UserAgentResponsePayload>, Status> {
let entries: Vec<NewEvmWalletAccess> = req.accesses.into_iter().map(Convert::convert).collect(); let entries: Vec<NewEvmWalletAccess> = req.accesses.into_iter().map(|a| a.convert()).collect();
match actor.ask(HandleGrantEvmWalletAccess { entries }).await { match actor.ask(HandleGrantEvmWalletAccess { entries }).await {
Ok(()) => { Ok(()) => {
info!("Successfully granted wallet access"); info!("Successfully granted wallet access");
@@ -187,7 +182,7 @@ async fn handle_list_wallet_access(
match actor.ask(HandleListWalletAccess {}).await { match actor.ask(HandleListWalletAccess {}).await {
Ok(accesses) => Ok(Some(wrap_sdk_client_response( Ok(accesses) => Ok(Some(wrap_sdk_client_response(
SdkClientResponsePayload::ListWalletAccess(ListWalletAccessResponse { SdkClientResponsePayload::ListWalletAccess(ListWalletAccessResponse {
accesses: accesses.into_iter().map(Convert::convert).collect(), accesses: accesses.into_iter().map(|a| a.convert()).collect(),
}), }),
))), ))),
Err(err) => { Err(err) => {

View File

@@ -1,23 +1,18 @@
use crate::{ use arbiter_proto::proto::shared::VaultState as ProtoVaultState;
actors::vault::VaultState, use arbiter_proto::proto::user_agent::{
peers::user_agent::{UserAgentSession, session::handlers::HandleQueryVaultState},
};
use arbiter_proto::{
proto::shared::VaultState as ProtoVaultState,
proto::user_agent::{
user_agent_response::Payload as UserAgentResponsePayload, user_agent_response::Payload as UserAgentResponsePayload,
vault::{ vault::{self as proto_vault, request::Payload as VaultRequestPayload, response::Payload as VaultResponsePayload},
self as proto_vault, request::Payload as VaultRequestPayload,
response::Payload as VaultResponsePayload,
},
},
}; };
use kameo::actor::ActorRef; use kameo::actor::ActorRef;
use tonic::Status; use tonic::Status;
use tracing::warn; use tracing::warn;
const fn wrap_vault_response(payload: VaultResponsePayload) -> UserAgentResponsePayload { use crate::{
actors::vault::VaultState,
peers::user_agent::{UserAgentSession, session::handlers::HandleQueryVaultState},
};
fn wrap_vault_response(payload: VaultResponsePayload) -> UserAgentResponsePayload {
UserAgentResponsePayload::Vault(proto_vault::Response { UserAgentResponsePayload::Vault(proto_vault::Response {
payload: Some(payload), payload: Some(payload),
}) })
@@ -32,7 +27,7 @@ pub(super) async fn dispatch(
}; };
match payload { match payload {
VaultRequestPayload::QueryState(()) => handle_query_vault_state(actor).await, VaultRequestPayload::QueryState(_) => handle_query_vault_state(actor).await,
VaultRequestPayload::Unseal(_) | VaultRequestPayload::Bootstrap(_) => { VaultRequestPayload::Unseal(_) | VaultRequestPayload::Bootstrap(_) => {
Err(Status::permission_denied( Err(Status::permission_denied(
"Vault is already unsealed; unseal/bootstrap not permitted in session", "Vault is already unsealed; unseal/bootstrap not permitted in session",

View File

@@ -1,27 +1,66 @@
use super::auth::AuthTransportAdapter; use arbiter_proto::{
use crate::{ proto::user_agent::{
grpc::TryConvert, user_agent_request::Payload as UserAgentRequestPayload,
peers::user_agent::vault_gate::{self as vault_gate}, user_agent_response::Payload as UserAgentResponsePayload,
vault::{
self as proto_vault,
bootstrap::{self as proto_bootstrap, BootstrapResult as ProtoBootstrapResult},
request::Payload as VaultRequestPayload,
response::Payload as VaultResponsePayload,
unseal::{
self as proto_unseal, UnsealResult as ProtoUnsealResult,
request::Payload as UnsealRequestPayload,
response::Payload as UnsealResponsePayload,
},
},
},
transport::{Bi, Error as TransportError, Receiver, Sender},
}; };
use arbiter_proto::transport::{Bi, Error as TransportError, Receiver, Sender};
use async_trait::async_trait; use async_trait::async_trait;
use tonic::Status; use tonic::Status;
use tracing::warn; use tracing::warn;
mod inbound; use super::auth::AuthTransportAdapter;
mod outbound; use crate::peers::user_agent::vault_gate::{
self as vault_gate, HandleBootstrapEncryptedKey, HandleHandshake, HandleUnsealEncryptedKey,
};
fn wrap_vault_response(payload: VaultResponsePayload) -> UserAgentResponsePayload {
UserAgentResponsePayload::Vault(proto_vault::Response {
payload: Some(payload),
})
}
fn wrap_unseal_response(payload: UnsealResponsePayload) -> UserAgentResponsePayload {
wrap_vault_response(VaultResponsePayload::Unseal(proto_unseal::Response {
payload: Some(payload),
}))
}
fn wrap_bootstrap_response(result: ProtoBootstrapResult) -> UserAgentResponsePayload {
wrap_vault_response(VaultResponsePayload::Bootstrap(proto_bootstrap::Response {
result: result.into(),
}))
}
impl AuthTransportAdapter<'_> {
async fn send_query_state(&mut self) -> Result<(), TransportError> {
use arbiter_proto::proto::shared::VaultState as ProtoVaultState;
self.send_response_payload(wrap_vault_response(VaultResponsePayload::State(
ProtoVaultState::Sealed.into(),
)))
.await
}
}
#[async_trait] #[async_trait]
impl Receiver<vault_gate::Inbound> for AuthTransportAdapter<'_> { impl Receiver<vault_gate::Inbound> for AuthTransportAdapter<'_> {
async fn recv(&mut self) -> Option<vault_gate::Inbound> { async fn recv(&mut self) -> Option<vault_gate::Inbound> {
loop {
let request = match self.bi_mut().recv().await? { let request = match self.bi_mut().recv().await? {
Ok(request) => request, Ok(request) => request,
Err(error) => { Err(error) => {
warn!( warn!(?error, "Failed to receive user agent request during vault gate");
?error,
"Failed to receive user agent request during vault gate"
);
return None; return None;
} }
}; };
@@ -39,11 +78,86 @@ impl Receiver<vault_gate::Inbound> for AuthTransportAdapter<'_> {
return None; return None;
}; };
match payload.try_convert() { let vault_req = match payload {
Ok(inbound) => Some(inbound), UserAgentRequestPayload::Vault(req) => req,
Err(status) => { _ => {
let _ = self.bi_mut().send(Err(status)).await; let _ = self
None .bi_mut()
.send(Err(Status::permission_denied(
"Only vault operations are permitted before unsealing",
)))
.await;
return None;
}
};
let Some(vault_payload) = vault_req.payload else {
let _ = self
.bi_mut()
.send(Err(Status::invalid_argument("Missing vault request payload")))
.await;
return None;
};
match vault_payload {
VaultRequestPayload::QueryState(_) => {
if self.send_query_state().await.is_err() {
return None;
}
continue;
}
VaultRequestPayload::Unseal(req) => {
let Some(unseal_payload) = req.payload else {
let _ = self
.bi_mut()
.send(Err(Status::invalid_argument("Missing unseal request payload")))
.await;
return None;
};
match unseal_payload {
UnsealRequestPayload::Start(start) => {
let Ok(bytes) = <[u8; 32]>::try_from(start.client_pubkey) else {
let _ = self
.bi_mut()
.send(Err(Status::invalid_argument(
"Invalid X25519 public key",
)))
.await;
return None;
};
return Some(vault_gate::Inbound::HandleHandshake(HandleHandshake {
client_pubkey: x25519_dalek::PublicKey::from(bytes),
}));
}
UnsealRequestPayload::EncryptedKey(key) => {
return Some(vault_gate::Inbound::HandleUnsealEncryptedKey(
HandleUnsealEncryptedKey {
nonce: key.nonce,
ciphertext: key.ciphertext,
associated_data: key.associated_data,
},
));
}
}
}
VaultRequestPayload::Bootstrap(req) => {
let Some(encrypted_key) = req.encrypted_key else {
let _ = self
.bi_mut()
.send(Err(Status::invalid_argument(
"Missing bootstrap encrypted key",
)))
.await;
return None;
};
return Some(vault_gate::Inbound::HandleBootstrapEncryptedKey(
HandleBootstrapEncryptedKey {
nonce: encrypted_key.nonce,
ciphertext: encrypted_key.ciphertext,
associated_data: encrypted_key.associated_data,
},
));
}
} }
} }
} }
@@ -66,10 +180,55 @@ impl Sender<Result<vault_gate::Outbound, vault_gate::Error>> for AuthTransportAd
} }
}; };
match outbound.try_convert() { let payload = match outbound {
Ok(payload) => self.send_response_payload(payload).await, vault_gate::Outbound::HandleHandshake(Ok(response)) => {
Err(status) => self.bi_mut().send(Err(status)).await, wrap_unseal_response(UnsealResponsePayload::Start(
proto_unseal::UnsealStartResponse {
server_pubkey: response.server_pubkey.as_bytes().to_vec(),
},
))
} }
vault_gate::Outbound::HandleHandshake(Err(err)) => {
warn!(?err, "handshake failed");
return self
.bi_mut()
.send(Err(Status::internal("Failed to start unseal flow")))
.await;
}
vault_gate::Outbound::HandleUnsealEncryptedKey(result) => {
let proto_result = match result {
Ok(()) => ProtoUnsealResult::Success,
Err(vault_gate::Error::InvalidKey) => ProtoUnsealResult::InvalidKey,
Err(err) => {
warn!(?err, "unseal failed");
return self
.bi_mut()
.send(Err(Status::internal("Failed to unseal vault")))
.await;
}
};
wrap_unseal_response(UnsealResponsePayload::Result(proto_result.into()))
}
vault_gate::Outbound::HandleBootstrapEncryptedKey(result) => {
let proto_result = match result {
Ok(()) => ProtoBootstrapResult::Success,
Err(vault_gate::Error::InvalidKey) => ProtoBootstrapResult::InvalidKey,
Err(vault_gate::Error::AlreadyBootstrapped) => {
ProtoBootstrapResult::AlreadyBootstrapped
}
Err(err) => {
warn!(?err, "bootstrap failed");
return self
.bi_mut()
.send(Err(Status::internal("Failed to bootstrap vault")))
.await;
}
};
wrap_bootstrap_response(proto_result)
}
};
self.send_response_payload(payload).await
} }
} }

View File

@@ -1,129 +0,0 @@
use crate::{
grpc::{Convert, TryConvert},
peers::user_agent::vault_gate::{
self as vault_gate, HandleBootstrapEncryptedKey, HandleHandshake, HandleUnsealEncryptedKey,
},
};
use arbiter_proto::proto::user_agent::{
user_agent_request::Payload as UserAgentRequestPayload,
vault::{
self as proto_vault,
bootstrap::{self as proto_bootstrap},
request::Payload as VaultRequestPayload,
unseal::{self as proto_unseal, request::Payload as UnsealRequestPayload},
},
};
use tonic::Status;
impl TryConvert for UserAgentRequestPayload {
type Output = vault_gate::Inbound;
type Error = Status;
fn try_convert(self) -> Result<vault_gate::Inbound, Status> {
match self {
Self::Vault(req) => req.try_convert(),
_ => Err(Status::permission_denied(
"Only vault operations are permitted before unsealing",
)),
}
}
}
impl TryConvert for proto_vault::Request {
type Output = vault_gate::Inbound;
type Error = Status;
fn try_convert(self) -> Result<vault_gate::Inbound, Status> {
self.payload
.ok_or_else(|| Status::invalid_argument("Missing vault request payload"))?
.try_convert()
}
}
impl TryConvert for VaultRequestPayload {
type Output = vault_gate::Inbound;
type Error = Status;
fn try_convert(self) -> Result<vault_gate::Inbound, Status> {
match self {
Self::QueryState(()) => Ok(vault_gate::Inbound::HandleVaultState),
Self::Unseal(req) => req.try_convert(),
Self::Bootstrap(req) => req.try_convert(),
}
}
}
impl TryConvert for proto_unseal::Request {
type Output = vault_gate::Inbound;
type Error = Status;
fn try_convert(self) -> Result<vault_gate::Inbound, Status> {
self.payload
.ok_or_else(|| Status::invalid_argument("Missing unseal request payload"))?
.try_convert()
}
}
impl TryConvert for UnsealRequestPayload {
type Output = vault_gate::Inbound;
type Error = Status;
fn try_convert(self) -> Result<vault_gate::Inbound, Status> {
match self {
Self::Start(start) => start.try_convert(),
Self::EncryptedKey(key) => Ok(key.convert()),
}
}
}
impl TryConvert for proto_unseal::UnsealStart {
type Output = vault_gate::Inbound;
type Error = Status;
fn try_convert(self) -> Result<vault_gate::Inbound, Status> {
let bytes = <[u8; 32]>::try_from(self.client_pubkey)
.map_err(|_| Status::invalid_argument("Invalid X25519 public key"))?;
Ok(vault_gate::Inbound::HandleHandshake(HandleHandshake {
client_pubkey: x25519_dalek::PublicKey::from(bytes),
}))
}
}
impl Convert for proto_unseal::UnsealEncryptedKey {
type Output = vault_gate::Inbound;
fn convert(self) -> vault_gate::Inbound {
vault_gate::Inbound::HandleUnsealEncryptedKey(HandleUnsealEncryptedKey {
nonce: self.nonce,
ciphertext: self.ciphertext,
associated_data: self.associated_data,
})
}
}
impl TryConvert for proto_bootstrap::Request {
type Output = vault_gate::Inbound;
type Error = Status;
fn try_convert(self) -> Result<vault_gate::Inbound, Status> {
self.encrypted_key
.ok_or_else(|| Status::invalid_argument("Missing bootstrap encrypted key"))?
.try_convert()
}
}
impl TryConvert for proto_bootstrap::BootstrapEncryptedKey {
type Output = vault_gate::Inbound;
type Error = Status;
fn try_convert(self) -> Result<vault_gate::Inbound, Status> {
Ok(vault_gate::Inbound::HandleBootstrapEncryptedKey(
HandleBootstrapEncryptedKey {
nonce: self.nonce,
ciphertext: self.ciphertext,
associated_data: self.associated_data,
},
))
}
}

View File

@@ -1,115 +0,0 @@
use crate::{
actors::vault::VaultState,
grpc::{Convert, TryConvert},
peers::user_agent::vault_gate::{self as vault_gate},
};
use arbiter_proto::proto::{
shared::VaultState as ProtoVaultState,
user_agent::{
user_agent_response::Payload as UserAgentResponsePayload,
vault::{
self as proto_vault,
bootstrap::{self as proto_bootstrap, BootstrapResult as ProtoBootstrapResult},
response::Payload as VaultResponsePayload,
unseal::{
self as proto_unseal, UnsealResult as ProtoUnsealResult,
response::Payload as UnsealResponsePayload,
},
},
},
};
use tonic::Status;
use tracing::warn;
const fn wrap_vault_response(payload: VaultResponsePayload) -> UserAgentResponsePayload {
UserAgentResponsePayload::Vault(proto_vault::Response {
payload: Some(payload),
})
}
const fn wrap_unseal_response(payload: UnsealResponsePayload) -> UserAgentResponsePayload {
wrap_vault_response(VaultResponsePayload::Unseal(proto_unseal::Response {
payload: Some(payload),
}))
}
fn wrap_bootstrap_response(result: ProtoBootstrapResult) -> UserAgentResponsePayload {
wrap_vault_response(VaultResponsePayload::Bootstrap(proto_bootstrap::Response {
result: result.into(),
}))
}
impl Convert for VaultState {
type Output = UserAgentResponsePayload;
fn convert(self) -> UserAgentResponsePayload {
let proto_state = match self {
Self::Unbootstrapped => ProtoVaultState::Unbootstrapped,
Self::Sealed => ProtoVaultState::Sealed,
Self::Unsealed => ProtoVaultState::Unsealed,
};
wrap_vault_response(VaultResponsePayload::State(proto_state.into()))
}
}
impl Convert for vault_gate::HandshakeResponse {
type Output = UserAgentResponsePayload;
fn convert(self) -> UserAgentResponsePayload {
wrap_unseal_response(UnsealResponsePayload::Start(
proto_unseal::UnsealStartResponse {
server_pubkey: self.server_pubkey.as_bytes().to_vec(),
},
))
}
}
impl TryConvert for vault_gate::Outbound {
type Output = UserAgentResponsePayload;
type Error = Status;
fn try_convert(self) -> Result<UserAgentResponsePayload, Status> {
match self {
Self::HandleVaultState(result) => result
.map_err(|err| {
warn!(?err, "vault state query failed");
Status::internal("Failed to query vault state")
})
.map(VaultState::convert),
Self::HandleHandshake(result) => result
.map_err(|err| {
warn!(?err, "handshake failed");
Status::internal("Failed to start unseal flow")
})
.map(vault_gate::HandshakeResponse::convert),
Self::HandleUnsealEncryptedKey(result) => {
let proto_result = match result {
Ok(()) => ProtoUnsealResult::Success,
Err(vault_gate::Error::InvalidKey) => ProtoUnsealResult::InvalidKey,
Err(err) => {
warn!(?err, "unseal failed");
return Err(Status::internal("Failed to unseal vault"));
}
};
Ok(wrap_unseal_response(UnsealResponsePayload::Result(
proto_result.into(),
)))
}
Self::HandleBootstrapEncryptedKey(result) => {
let proto_result = match result {
Ok(()) => ProtoBootstrapResult::Success,
Err(vault_gate::Error::InvalidKey) => ProtoBootstrapResult::InvalidKey,
Err(vault_gate::Error::AlreadyBootstrapped) => {
ProtoBootstrapResult::AlreadyBootstrapped
}
Err(err) => {
warn!(?err, "bootstrap failed");
return Err(Status::internal("Failed to bootstrap vault"));
}
};
Ok(wrap_bootstrap_response(proto_result))
}
}
}
}

View File

@@ -1,3 +1,4 @@
#![forbid(unsafe_code)]
use crate::context::ServerContext; use crate::context::ServerContext;
pub mod actors; pub mod actors;
@@ -14,7 +15,7 @@ pub struct Server {
} }
impl Server { impl Server {
pub const fn new(context: ServerContext) -> Self { pub fn new(context: ServerContext) -> Self {
Self { context } Self { context }
} }
} }

View File

@@ -1,9 +1,9 @@
use arbiter_proto::{proto::arbiter_service_server::ArbiterServiceServer, url::ArbiterUrl}; use std::net::SocketAddr;
use arbiter_server::{Server, actors::bootstrap::GetToken, context::ServerContext, db};
use anyhow::anyhow; use anyhow::anyhow;
use arbiter_proto::{proto::arbiter_service_server::ArbiterServiceServer, url::ArbiterUrl};
use arbiter_server::{Server, actors::bootstrap::GetToken, context::ServerContext, db};
use rustls::crypto::aws_lc_rs; use rustls::crypto::aws_lc_rs;
use std::net::SocketAddr;
use tonic::transport::{Identity, ServerTlsConfig}; use tonic::transport::{Identity, ServerTlsConfig};
use tracing::info; use tracing::info;

View File

@@ -1,4 +1,17 @@
use super::{ClientConnection, ClientCredentials, ClientProfile}; use arbiter_crypto::authn::{self, CLIENT_CONTEXT};
use arbiter_proto::{
ClientMetadata,
transport::{Bi, expect_message},
};
use chrono::Utc;
use diesel::{
ExpressionMethods as _, OptionalExtension as _, QueryDsl as _, SelectableHelper as _,
dsl::insert_into, update,
};
use diesel_async::RunQueryDsl as _;
use kameo::{actor::ActorRef, error::SendError};
use tracing::error;
use crate::{ use crate::{
actors::{ actors::{
GlobalActors, GlobalActors,
@@ -12,20 +25,8 @@ use crate::{
schema::program_client, schema::program_client,
}, },
}; };
use arbiter_crypto::authn::{self, AuthChallenge, CLIENT_CONTEXT};
use arbiter_proto::{
ClientMetadata,
transport::{Bi, expect_message},
};
use chrono::Utc; use super::{ClientConnection, ClientCredentials, ClientProfile};
use diesel::{
ExpressionMethods as _, OptionalExtension as _, QueryDsl as _, SelectableHelper as _,
dsl::insert_into, update,
};
use diesel_async::RunQueryDsl as _;
use kameo::{actor::ActorRef, error::SendError};
use tracing::error;
#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)] #[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
pub enum Error { pub enum Error {
@@ -73,14 +74,19 @@ pub enum Inbound {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Outbound { pub enum Outbound {
AuthChallenge { challenge: AuthChallenge }, AuthChallenge {
pubkey: authn::PublicKey,
nonce: i32,
},
AuthSuccess, AuthSuccess,
} }
async fn get_client_id( /// Returns the current nonce and client ID for a registered client.
/// Returns `None` if the pubkey is not registered.
async fn get_current_nonce_and_id(
db: &db::DatabasePool, db: &db::DatabasePool,
pubkey: &authn::PublicKey, pubkey: &authn::PublicKey,
) -> Result<Option<i32>, Error> { ) -> Result<Option<(i32, i32)>, Error> {
let pubkey_bytes = pubkey.to_bytes(); let pubkey_bytes = pubkey.to_bytes();
let mut conn = db.get().await.map_err(|e| { let mut conn = db.get().await.map_err(|e| {
error!(error = ?e, "Database pool error"); error!(error = ?e, "Database pool error");
@@ -88,8 +94,8 @@ async fn get_client_id(
})?; })?;
program_client::table program_client::table
.filter(program_client::public_key.eq(&pubkey_bytes)) .filter(program_client::public_key.eq(&pubkey_bytes))
.select(program_client::id) .select((program_client::id, program_client::nonce))
.first::<i32>(&mut conn) .first::<(i32, i32)>(&mut conn)
.await .await
.optional() .optional()
.map_err(|e| { .map_err(|e| {
@@ -108,7 +114,7 @@ async fn verify_integrity(
Error::DatabasePoolUnavailable Error::DatabasePoolUnavailable
})?; })?;
let id = get_client_id(db, pubkey).await?.ok_or_else(|| { let (id, nonce) = get_current_nonce_and_id(db, pubkey).await?.ok_or_else(|| {
error!("Client not found during integrity verification"); error!("Client not found during integrity verification");
Error::DatabaseOperationFailed Error::DatabaseOperationFailed
})?; })?;
@@ -118,6 +124,7 @@ async fn verify_integrity(
vault, vault,
&ClientCredentials { &ClientCredentials {
pubkey: pubkey.clone(), pubkey: pubkey.clone(),
nonce,
}, },
id, id,
) )
@@ -135,6 +142,53 @@ async fn verify_integrity(
Ok(()) Ok(())
} }
/// Atomically increments the nonce and re-signs the integrity envelope.
/// Returns the new nonce, which is used as the challenge nonce.
async fn create_nonce(
db: &db::DatabasePool,
vault: &ActorRef<Vault>,
pubkey: &authn::PublicKey,
) -> Result<i32, Error> {
let pubkey_bytes = pubkey.to_bytes();
let pubkey = pubkey.clone();
let mut conn = db.get().await.map_err(|e| {
error!(error = ?e, "Database pool error");
Error::DatabasePoolUnavailable
})?;
conn.exclusive_transaction(|conn| {
let vault = vault.clone();
let pubkey = pubkey.clone();
Box::pin(async move {
let (id, new_nonce): (i32, i32) = update(program_client::table)
.filter(program_client::public_key.eq(&pubkey_bytes))
.set(program_client::nonce.eq(program_client::nonce + 1))
.returning((program_client::id, program_client::nonce))
.get_result(conn)
.await?;
integrity::sign_entity(
conn,
&vault,
&ClientCredentials {
pubkey: pubkey.clone(),
nonce: new_nonce,
},
id,
)
.await
.map_err(|e| {
error!(?e, "Integrity sign failed after nonce update");
Error::DatabaseOperationFailed
})?;
Ok(new_nonce)
})
})
.await
}
async fn approve_new_client(actors: &GlobalActors, profile: ClientProfile) -> Result<(), Error> { async fn approve_new_client(actors: &GlobalActors, profile: ClientProfile) -> Result<(), Error> {
let result = actors let result = actors
.flow_coordinator .flow_coordinator
@@ -161,8 +215,7 @@ async fn insert_client(
pubkey: &authn::PublicKey, pubkey: &authn::PublicKey,
metadata: &ClientMetadata, metadata: &ClientMetadata,
) -> Result<i32, Error> { ) -> Result<i32, Error> {
use crate::db::schema::client_metadata; use crate::db::schema::{client_metadata, program_client};
let pubkey = pubkey.clone(); let pubkey = pubkey.clone();
let metadata = metadata.clone(); let metadata = metadata.clone();
@@ -175,6 +228,8 @@ async fn insert_client(
let vault = vault.clone(); let vault = vault.clone();
let pubkey = pubkey.clone(); let pubkey = pubkey.clone();
Box::pin(async move { Box::pin(async move {
const NONCE_START: i32 = 1;
let metadata_id = insert_into(client_metadata::table) let metadata_id = insert_into(client_metadata::table)
.values(( .values((
client_metadata::name.eq(&metadata.name), client_metadata::name.eq(&metadata.name),
@@ -189,6 +244,7 @@ async fn insert_client(
.values(( .values((
program_client::public_key.eq(pubkey.to_bytes()), program_client::public_key.eq(pubkey.to_bytes()),
program_client::metadata_id.eq(metadata_id), program_client::metadata_id.eq(metadata_id),
program_client::nonce.eq(NONCE_START),
)) ))
.on_conflict_do_nothing() .on_conflict_do_nothing()
.returning(program_client::id) .returning(program_client::id)
@@ -200,6 +256,7 @@ async fn insert_client(
&vault, &vault,
&ClientCredentials { &ClientCredentials {
pubkey: pubkey.clone(), pubkey: pubkey.clone(),
nonce: NONCE_START,
}, },
client_id, client_id,
) )
@@ -289,14 +346,15 @@ async fn sync_client_metadata(
async fn challenge_client<T>( async fn challenge_client<T>(
transport: &mut T, transport: &mut T,
pubkey: authn::PublicKey, pubkey: authn::PublicKey,
challenge: AuthChallenge, nonce: i32,
) -> Result<(), Error> ) -> Result<(), Error>
where where
T: Bi<Inbound, Result<Outbound, Error>> + ?Sized, T: Bi<Inbound, Result<Outbound, Error>> + ?Sized,
{ {
transport transport
.send(Ok(Outbound::AuthChallenge { .send(Ok(Outbound::AuthChallenge {
challenge: challenge.clone(), pubkey: pubkey.clone(),
nonce,
})) }))
.await .await
.map_err(|e| { .map_err(|e| {
@@ -314,7 +372,7 @@ where
Error::Transport Error::Transport
})?; })?;
if !pubkey.verify(&challenge, CLIENT_CONTEXT, &signature) { if !pubkey.verify(nonce, CLIENT_CONTEXT, &signature) {
error!("Challenge solution verification failed"); error!("Challenge solution verification failed");
return Err(Error::InvalidChallengeSolution); return Err(Error::InvalidChallengeSolution);
} }
@@ -330,10 +388,12 @@ where
return Err(Error::Transport); return Err(Error::Transport);
}; };
let client_id = if let Some(id) = get_client_id(&props.db, &pubkey).await? { let client_id = match get_current_nonce_and_id(&props.db, &pubkey).await? {
Some((id, _)) => {
verify_integrity(&props.db, &props.actors.vault, &pubkey).await?; verify_integrity(&props.db, &props.actors.vault, &pubkey).await?;
id id
} else { }
None => {
approve_new_client( approve_new_client(
&props.actors, &props.actors,
ClientProfile { ClientProfile {
@@ -343,12 +403,12 @@ where
) )
.await?; .await?;
insert_client(&props.db, &props.actors.vault, &pubkey, &metadata).await? insert_client(&props.db, &props.actors.vault, &pubkey, &metadata).await?
}
}; };
sync_client_metadata(&props.db, client_id, &metadata).await?; sync_client_metadata(&props.db, client_id, &metadata).await?;
let challenge_nonce = create_nonce(&props.db, &props.actors.vault, &pubkey).await?;
let challenge = AuthChallenge::generate(&mut rand::rng()); challenge_client(transport, pubkey, challenge_nonce).await?;
challenge_client(transport, pubkey, challenge).await?;
transport transport
.send(Ok(Outbound::AuthSuccess)) .send(Ok(Outbound::AuthSuccess))

View File

@@ -1,35 +1,44 @@
use crate::{
actors::GlobalActors, crypto::integrity::Integrable, db, peers::client::session::ClientSession,
};
use arbiter_crypto::authn; use arbiter_crypto::authn;
use arbiter_macros::Hashable;
use arbiter_proto::{ClientMetadata, transport::Bi}; use arbiter_proto::{ClientMetadata, transport::Bi};
use kameo::actor::Spawn; use kameo::actor::Spawn;
use tracing::{error, info}; use tracing::{error, info};
use crate::{
actors::GlobalActors,
crypto::integrity::{Integrable, hashing::Hashable},
db,
peers::client::session::ClientSession,
};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ClientProfile { pub struct ClientProfile {
pub pubkey: authn::PublicKey, pub pubkey: authn::PublicKey,
pub metadata: ClientMetadata, pub metadata: ClientMetadata,
} }
#[derive(Hashable)]
pub struct ClientCredentials { pub struct ClientCredentials {
pub pubkey: authn::PublicKey, pub pubkey: authn::PublicKey,
pub nonce: i32,
} }
impl Integrable for ClientCredentials { impl Integrable for ClientCredentials {
const KIND: &'static str = "client_credentials"; const KIND: &'static str = "client_credentials";
} }
impl Hashable for ClientCredentials {
fn hash<H: sha2::Digest>(&self, hasher: &mut H) {
hasher.update(self.pubkey.to_bytes());
self.nonce.hash(hasher);
}
}
pub struct ClientConnection { pub struct ClientConnection {
pub(crate) db: db::DatabasePool, pub(crate) db: db::DatabasePool,
pub(crate) actors: GlobalActors, pub(crate) actors: GlobalActors,
} }
impl ClientConnection { impl ClientConnection {
pub const fn new(db: db::DatabasePool, actors: GlobalActors) -> Self { pub fn new(db: db::DatabasePool, actors: GlobalActors) -> Self {
Self { db, actors } Self { db, actors }
} }
} }
@@ -42,7 +51,7 @@ where
T: Bi<auth::Inbound, Result<auth::Outbound, auth::Error>> + Send + ?Sized, T: Bi<auth::Inbound, Result<auth::Outbound, auth::Error>> + Send + ?Sized,
{ {
let fut = auth::authenticate(&mut props, transport); let fut = auth::authenticate(&mut props, transport);
println!("authenticate future size: {}", size_of_val(&fut)); println!("authenticate future size: {}", std::mem::size_of_val(&fut));
match fut.await { match fut.await {
Ok(client_id) => { Ok(client_id) => {
ClientSession::spawn(ClientSession::new(props, client_id)); ClientSession::spawn(ClientSession::new(props, client_id));

View File

@@ -1,4 +1,8 @@
use super::ClientConnection; use kameo::{Actor, messages};
use tracing::error;
use alloy::{consensus::TxEip1559, primitives::Address, signers::Signature};
use crate::{ use crate::{
actors::{ actors::{
GlobalActors, GlobalActors,
@@ -10,9 +14,7 @@ use crate::{
evm::VetError, evm::VetError,
}; };
use alloy::{consensus::TxEip1559, primitives::Address, signers::Signature}; use super::ClientConnection;
use kameo::{Actor, messages};
use tracing::error;
pub struct ClientSession { pub struct ClientSession {
props: ClientConnection, props: ClientConnection,
@@ -20,7 +22,7 @@ pub struct ClientSession {
} }
impl ClientSession { impl ClientSession {
pub(crate) const fn new(props: ClientConnection, client_id: i32) -> Self { pub(crate) fn new(props: ClientConnection, client_id: i32) -> Self {
Self { props, client_id } Self { props, client_id }
} }
} }
@@ -91,7 +93,7 @@ impl Actor for ClientSession {
} }
impl ClientSession { impl ClientSession {
pub const fn new_test(db: db::DatabasePool, actors: GlobalActors) -> Self { pub fn new_test(db: db::DatabasePool, actors: GlobalActors) -> Self {
let props = ClientConnection::new(db, actors); let props = ClientConnection::new(db, actors);
Self { Self {
props, props,

View File

@@ -1,14 +1,11 @@
use super::{Credentials, UserAgentConnection}; use arbiter_crypto::authn;
use arbiter_crypto::authn::{self, AuthChallenge};
use arbiter_proto::transport::Bi; use arbiter_proto::transport::Bi;
use state::{
AuthContext, AuthError, AuthEvents, AuthStateMachine, AuthStates, ChallengeRequest,
ChallengeSolution,
};
use tracing::error; use tracing::error;
mod state; mod state;
use state::*;
use super::{AuthCredentials, UserAgentConnection};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Inbound { pub enum Inbound {
@@ -47,7 +44,7 @@ impl From<diesel::result::Error> for Error {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Outbound { pub enum Outbound {
AuthChallenge { challenge: AuthChallenge }, AuthChallenge { nonce: i32 },
AuthSuccess, AuthSuccess,
} }
@@ -55,11 +52,12 @@ fn parse_auth_event(payload: Inbound) -> AuthEvents {
match payload { match payload {
Inbound::AuthChallengeRequest { Inbound::AuthChallengeRequest {
pubkey, pubkey,
bootstrap_token, bootstrap_token: None,
} => AuthEvents::AuthRequest(ChallengeRequest { } => AuthEvents::AuthRequest(ChallengeRequest { pubkey }),
Inbound::AuthChallengeRequest {
pubkey, pubkey,
bootstrap_token, bootstrap_token: Some(token),
}), } => AuthEvents::BootstrapAuthRequest(BootstrapAuthRequest { pubkey, token }),
Inbound::AuthChallengeSolution { signature } => { Inbound::AuthChallengeSolution { signature } => {
AuthEvents::ReceivedSolution(ChallengeSolution { AuthEvents::ReceivedSolution(ChallengeSolution {
solution: signature, solution: signature,
@@ -71,13 +69,14 @@ fn parse_auth_event(payload: Inbound) -> AuthEvents {
pub async fn authenticate<T>( pub async fn authenticate<T>(
props: &mut UserAgentConnection, props: &mut UserAgentConnection,
transport: &mut T, transport: &mut T,
) -> Result<Credentials, Error> ) -> Result<AuthCredentials, Error>
where where
T: Bi<Inbound, Result<Outbound, Error>> + Send + ?Sized, T: Bi<Inbound, Result<Outbound, Error>> + Send + ?Sized,
{ {
let mut state = AuthStateMachine::new(AuthContext::new(props, transport)); let mut state = AuthStateMachine::new(AuthContext::new(props, transport));
loop { loop {
// `state` holds a mutable reference to `props` so we can't access it directly here
let Some(payload) = state.context_mut().transport.recv().await else { let Some(payload) = state.context_mut().transport.recv().await else {
return Err(Error::Transport); return Err(Error::Transport);
}; };

View File

@@ -1,32 +1,36 @@
use super::{ use super::super::{AuthCredentials, Credentials, UserAgentConnection};
super::{Credentials, UserAgentConnection}, use arbiter_crypto::authn::{self, USERAGENT_CONTEXT};
Error,
};
use crate::{
actors::bootstrap::ConsumeToken,
db::{DatabasePool, schema::useragent_client},
peers::user_agent::auth::Outbound,
};
use arbiter_crypto::authn::{self, AuthChallenge, USERAGENT_CONTEXT};
use arbiter_proto::transport::Bi; use arbiter_proto::transport::Bi;
use diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl, sqlite::Sqlite, update};
use diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl}; use diesel_async::{AsyncConnection, RunQueryDsl};
use diesel_async::RunQueryDsl; use kameo::actor::ActorRef;
use tracing::error; use tracing::error;
pub(super) struct ChallengeRequest { use super::Error;
pub(super) pubkey: authn::PublicKey, use crate::peers::user_agent::auth::Outbound;
pub(super) bootstrap_token: Option<String>, use crate::{
actors::{bootstrap::ConsumeToken, vault::Vault},
crypto::integrity,
db::{DatabasePool, schema::useragent_client},
};
pub struct ChallengeRequest {
pub pubkey: authn::PublicKey,
} }
pub(super) struct ChallengeContext { pub struct BootstrapAuthRequest {
pub(super) challenge: AuthChallenge, pub pubkey: authn::PublicKey,
pub(super) pubkey: authn::PublicKey, pub token: String,
pub(super) bootstrap_token: Option<String>,
} }
pub(super) struct ChallengeSolution { pub struct ChallengeContext {
pub(super) solution: Vec<u8>, pub id: i32,
pub challenge_nonce: i32,
pub key: authn::PublicKey,
}
pub struct ChallengeSolution {
pub solution: Vec<u8>,
} }
smlang::statemachine!( smlang::statemachine!(
@@ -34,25 +38,116 @@ smlang::statemachine!(
custom_error: true, custom_error: true,
transitions: { transitions: {
*Init + AuthRequest(ChallengeRequest) / async prepare_challenge = SentChallenge(ChallengeContext), *Init + AuthRequest(ChallengeRequest) / async prepare_challenge = SentChallenge(ChallengeContext),
SentChallenge(ChallengeContext) + ReceivedSolution(ChallengeSolution) / async verify_solution = AuthOk(Credentials), Init + BootstrapAuthRequest(BootstrapAuthRequest) / async verify_bootstrap_token = AuthOk(AuthCredentials),
SentChallenge(ChallengeContext) + ReceivedSolution(ChallengeSolution) / async verify_solution = AuthOk(AuthCredentials),
} }
); );
async fn get_client_id(db: &DatabasePool, pubkey: &authn::PublicKey) -> Result<Option<i32>, Error> { const NONCE_START: i32 = 1;
let mut conn = db.get().await.map_err(|e| {
/// Returns the current nonce, ready to use for the challenge nonce.
async fn get_current_nonce_and_id(
db: &DatabasePool,
key: &authn::PublicKey,
) -> Result<(i32, i32), Error> {
let mut db_conn = db.get().await.map_err(|e| {
error!(error = ?e, "Database pool error"); error!(error = ?e, "Database pool error");
Error::internal("Database unavailable") Error::internal("Database unavailable")
})?; })?;
db_conn
.exclusive_transaction(|conn| {
Box::pin(async move {
useragent_client::table useragent_client::table
.filter(useragent_client::public_key.eq(pubkey.to_bytes())) .filter(useragent_client::public_key.eq(key.to_bytes()))
.select(useragent_client::id) .select((useragent_client::id, useragent_client::nonce))
.first::<i32>(&mut conn) .first::<(i32, i32)>(conn)
.await
})
})
.await .await
.optional() .optional()
.map_err(|e| { .map_err(|e| {
error!(error = ?e, "Database error"); error!(error = ?e, "Database error");
Error::internal("Database operation failed") Error::internal("Database operation failed")
})?
.ok_or_else(|| {
error!(?key, "Public key not found in database");
Error::UnregisteredPublicKey
})
}
async fn verify_integrity(
db: &DatabasePool,
vault: &ActorRef<Vault>,
pubkey: &authn::PublicKey,
) -> Result<(), Error> {
let mut db_conn = db.get().await.map_err(|e| {
error!(error = ?e, "Database pool error");
Error::internal("Database unavailable")
})?;
let (id, nonce) = get_current_nonce_and_id(db, pubkey).await?;
let _result = integrity::verify_entity(
&mut db_conn,
vault,
&AuthCredentials {
creds: Credentials {
id,
pubkey: pubkey.clone(),
},
new_nonce: nonce,
},
id,
)
.await
.map_err(|e| {
error!(?e, "Integrity verification failed");
Error::internal("Integrity verification failed")
})?;
Ok(())
}
async fn compute_current_nonce(
conn: &mut impl AsyncConnection<Backend = Sqlite>,
pubkey: &authn::PublicKey,
) -> Result<(i32, i32), Error> {
update(useragent_client::table)
.filter(useragent_client::public_key.eq(pubkey.to_bytes()))
.set(useragent_client::nonce.eq(useragent_client::nonce + 1))
.returning((useragent_client::id, useragent_client::nonce))
.get_result(conn)
.await
.map_err(|e| {
error!(error = ?e, "Database error incrementing nonce");
Error::internal("Database operation failed")
})
}
async fn resign_credentials(
conn: &mut impl AsyncConnection<Backend = Sqlite>,
vault: &ActorRef<Vault>,
id: i32,
pubkey: &authn::PublicKey,
new_nonce: i32,
) -> Result<(), Error> {
integrity::sign_entity(
conn,
vault,
&AuthCredentials {
creds: Credentials {
id,
pubkey: pubkey.clone(),
},
new_nonce,
},
id,
)
.await
.map_err(|e| {
error!(?e, "Integrity signature update failed");
Error::internal("Database error")
}) })
} }
@@ -64,7 +159,10 @@ async fn register_key(db: &DatabasePool, pubkey: &authn::PublicKey) -> Result<i3
})?; })?;
let id: i32 = diesel::insert_into(useragent_client::table) let id: i32 = diesel::insert_into(useragent_client::table)
.values((useragent_client::public_key.eq(pubkey_bytes),)) .values((
useragent_client::public_key.eq(pubkey_bytes),
useragent_client::nonce.eq(NONCE_START),
))
.returning(useragent_client::id) .returning(useragent_client::id)
.get_result(&mut conn) .get_result(&mut conn)
.await .await
@@ -76,44 +174,57 @@ async fn register_key(db: &DatabasePool, pubkey: &authn::PublicKey) -> Result<i3
Ok(id) Ok(id)
} }
pub(super) struct AuthContext<'a, T: ?Sized> { pub struct AuthContext<'a, T: ?Sized> {
pub(super) conn: &'a mut UserAgentConnection, pub(super) conn: &'a mut UserAgentConnection,
pub(super) transport: &'a mut T, pub(super) transport: &'a mut T,
} }
impl<'a, T: ?Sized> AuthContext<'a, T> { impl<'a, T: ?Sized> AuthContext<'a, T> {
pub(super) const fn new(conn: &'a mut UserAgentConnection, transport: &'a mut T) -> Self { pub fn new(conn: &'a mut UserAgentConnection, transport: &'a mut T) -> Self {
Self { conn, transport } Self { conn, transport }
} }
} }
impl<T> AuthStateMachineContext for AuthContext<'_, T> impl<T> AuthStateMachineContext for AuthContext<'_, T>
where where
T: Bi<super::Inbound, Result<Outbound, Error>> + Send + ?Sized, T: Bi<super::Inbound, Result<super::Outbound, Error>> + Send + ?Sized,
{ {
type Error = Error; type Error = Error;
async fn prepare_challenge( async fn prepare_challenge(
&mut self, &mut self,
ChallengeRequest { ChallengeRequest { pubkey }: ChallengeRequest,
pubkey,
bootstrap_token,
}: ChallengeRequest,
) -> Result<ChallengeContext, Self::Error> { ) -> Result<ChallengeContext, Self::Error> {
// Verify pubkey is registered (unless bootstrapping) let is_signing = integrity::is_signing_available(&self.conn.actors.vault)
if bootstrap_token.is_none() { .await
let id = get_client_id(&self.conn.db, &pubkey).await?; .unwrap_or(false);
if id.is_none() {
return Err(Error::UnregisteredPublicKey); if is_signing {
} verify_integrity(&self.conn.db, &self.conn.actors.vault, &pubkey).await?;
} }
let challenge = AuthChallenge::generate(&mut rand::rng()); let vault = self.conn.actors.vault.clone();
let mut conn = self.conn.db.get().await.map_err(|e| {
error!(error = ?e, "Database pool error");
Error::internal("Database unavailable")
})?;
let (id, nonce) = conn
.exclusive_transaction(|conn| {
let pubkey = pubkey.clone();
let vault = vault.clone();
Box::pin(async move {
let (id, new_nonce) = compute_current_nonce(conn, &pubkey).await?;
if is_signing {
resign_credentials(conn, &vault, id, &pubkey, new_nonce).await?;
}
Result::<_, Error>::Ok((id, new_nonce))
})
})
.await?;
self.transport self.transport
.send(Ok(Outbound::AuthChallenge { .send(Ok(Outbound::AuthChallenge { nonce }))
challenge: challenge.clone(),
}))
.await .await
.map_err(|e| { .map_err(|e| {
error!(?e, "Failed to send auth challenge"); error!(?e, "Failed to send auth challenge");
@@ -121,41 +232,18 @@ where
})?; })?;
Ok(ChallengeContext { Ok(ChallengeContext {
challenge, id,
pubkey, challenge_nonce: nonce,
bootstrap_token, key: pubkey,
}) })
} }
#[allow(missing_docs)] #[allow(missing_docs)]
#[allow(clippy::unused_unit)] #[allow(clippy::result_unit_err)]
async fn verify_solution( async fn verify_bootstrap_token(
&mut self, &mut self,
ChallengeContext { BootstrapAuthRequest { pubkey, token }: BootstrapAuthRequest,
challenge, ) -> Result<AuthCredentials, Self::Error> {
pubkey,
bootstrap_token,
}: &ChallengeContext,
ChallengeSolution { solution }: ChallengeSolution,
) -> Result<Credentials, Self::Error> {
let signature = authn::Signature::try_from(solution.as_slice()).map_err(|()| {
error!("Failed to decode signature in challenge solution");
Error::InvalidChallengeSolution
})?;
let valid = pubkey.verify(challenge, USERAGENT_CONTEXT, &signature);
if !valid {
self.transport
.send(Err(Error::InvalidChallengeSolution))
.await
.map_err(|_| Error::Transport)?;
return Err(Error::InvalidChallengeSolution);
}
// Resolve client id: bootstrap (consume token + register) or lookup
let id = match bootstrap_token {
Some(token) => {
let token_ok: bool = self let token_ok: bool = self
.conn .conn
.actors .actors
@@ -171,28 +259,71 @@ where
if !token_ok { if !token_ok {
error!("Invalid bootstrap token provided"); error!("Invalid bootstrap token provided");
self.transport
.send(Err(Error::InvalidBootstrapToken))
.await
.map_err(|_| Error::Transport)?;
return Err(Error::InvalidBootstrapToken); return Err(Error::InvalidBootstrapToken);
} }
register_key(&self.conn.db, pubkey).await? match token_ok {
} true => {
None => get_client_id(&self.conn.db, pubkey) let id = register_key(&self.conn.db, &pubkey).await?;
.await?
.ok_or(Error::UnregisteredPublicKey)?,
};
self.transport self.transport
.send(Ok(Outbound::AuthSuccess)) .send(Ok(Outbound::AuthSuccess))
.await .await
.map_err(|_| Error::Transport)?; .map_err(|_| Error::Transport)?;
Ok(AuthCredentials {
Ok(Credentials { creds: Credentials { id, pubkey },
id, new_nonce: NONCE_START,
pubkey: pubkey.clone(),
}) })
} }
false => {
error!("Invalid bootstrap token provided");
self.transport
.send(Err(Error::InvalidBootstrapToken))
.await
.map_err(|_| Error::Transport)?;
Err(Error::InvalidBootstrapToken)
}
}
}
#[allow(missing_docs)]
#[allow(clippy::unused_unit)]
async fn verify_solution(
&mut self,
ChallengeContext {
id,
challenge_nonce,
key,
}: &ChallengeContext,
ChallengeSolution { solution }: ChallengeSolution,
) -> Result<AuthCredentials, Self::Error> {
let signature = authn::Signature::try_from(solution.as_slice()).map_err(|_| {
error!("Failed to decode signature in challenge solution");
Error::InvalidChallengeSolution
})?;
let valid = key.verify(*challenge_nonce, USERAGENT_CONTEXT, &signature);
match valid {
true => {
self.transport
.send(Ok(Outbound::AuthSuccess))
.await
.map_err(|_| Error::Transport)?;
Ok(AuthCredentials {
creds: Credentials {
id: *id,
pubkey: key.clone(),
},
new_nonce: *challenge_nonce,
})
}
false => {
self.transport
.send(Err(Error::InvalidChallengeSolution))
.await
.map_err(|_| Error::Transport)?;
Err(Error::InvalidChallengeSolution)
}
}
}
} }

View File

@@ -1,35 +1,58 @@
use crate::{ use crate::{
actors::{ actors::GlobalActors,
GlobalActors, crypto::integrity::{self, Integrable},
vault::{GetState, Vault}, db,
},
crypto::integrity::{self, AttestationStatus, Integrable},
db::{DatabaseError, DatabasePool},
peers::client::ClientProfile, peers::client::ClientProfile,
}; };
use arbiter_crypto::authn; use arbiter_crypto::authn;
use arbiter_macros::Hashable;
use arbiter_proto::transport::{Bi, Sender}; use arbiter_proto::transport::{Bi, Sender};
pub use auth::authenticate;
use kameo::actor::{ActorRef, Spawn as _};
pub use session::UserAgentSession;
use tokio::sync::oneshot;
use tracing::warn;
use vault_gate::VaultGate; use vault_gate::VaultGate;
use kameo::actor::{ActorRef, Spawn as _}; use crate::crypto::integrity::hashing::Hashable;
use tokio::sync::oneshot;
use tracing::{error, warn};
pub use auth::authenticate;
pub use session::UserAgentSession;
pub mod auth; pub mod auth;
pub mod session; pub mod session;
pub mod vault_gate; pub mod vault_gate;
#[derive(Debug, Clone, Hashable)] #[derive(Debug, Clone, Hash)]
pub struct Credentials { pub struct Credentials {
pub id: i32, pub id: i32,
pub pubkey: authn::PublicKey, pub pubkey: authn::PublicKey,
} }
impl Hashable for Credentials {
fn hash<H: sha2::Digest>(&self, hasher: &mut H) {
self.id.hash(hasher);
self.pubkey.hash(hasher);
}
}
impl Integrable for Credentials { #[derive(Debug, Clone)]
pub struct AuthCredentials {
pub creds: Credentials,
// denotes new nonce, not current
pub new_nonce: i32,
}
impl Hashable for authn::PublicKey {
fn hash<H: sha2::Digest>(&self, hasher: &mut H) {
hasher.update(self.to_bytes());
}
}
impl Hashable for AuthCredentials {
fn hash<H: sha2::Digest>(&self, hasher: &mut H) {
self.creds.hash(hasher);
self.new_nonce.hash(hasher);
}
}
impl Integrable for AuthCredentials {
const KIND: &'static str = "useragent_credentials"; const KIND: &'static str = "useragent_credentials";
} }
@@ -42,12 +65,12 @@ pub enum OutOfBand {
#[derive(Clone)] #[derive(Clone)]
pub struct UserAgentConnection { pub struct UserAgentConnection {
pub(crate) db: DatabasePool, pub(crate) db: db::DatabasePool,
pub(crate) actors: GlobalActors, pub(crate) actors: GlobalActors,
} }
impl UserAgentConnection { impl UserAgentConnection {
pub const fn new(db: DatabasePool, actors: GlobalActors) -> Self { pub fn new(db: db::DatabasePool, actors: GlobalActors) -> Self {
Self { db, actors } Self { db, actors }
} }
} }
@@ -60,8 +83,6 @@ pub enum Error {
VaultGate(#[from] vault_gate::Error), VaultGate(#[from] vault_gate::Error),
#[error("transport closed unexpectedly")] #[error("transport closed unexpectedly")]
Transport, Transport,
#[error("database error: {0}")]
Database(DatabaseError),
#[error("internal: {0}")] #[error("internal: {0}")]
Internal(String), Internal(String),
} }
@@ -72,44 +93,38 @@ impl From<auth::Error> for Error {
} }
} }
async fn verify_integrity( pub async fn start<T>(
db: &DatabasePool, props: &mut UserAgentConnection,
vault: &ActorRef<Vault>, mut transport: T,
credentials: &Credentials, oob_sender: Box<dyn Sender<OutOfBand>>,
) -> Result<(), Error> { ) -> Result<ActorRef<UserAgentSession>, Error>
let mut conn = db where
.get() T: Bi<auth::Inbound, Result<auth::Outbound, auth::Error>> + Send,
.await T: Bi<vault_gate::Inbound, Result<vault_gate::Outbound, vault_gate::Error>> + Send,
.map_err(|_| Error::Internal("DB unavailable".into()))?; {
match integrity::verify_entity(&mut conn, vault, credentials, credentials.id).await { let auth_creds = authenticate(props, &mut transport).await?;
Ok(AttestationStatus::Attested) => Ok(()),
Ok(AttestationStatus::Unavailable) => {
Err(Error::Internal("Vault sealed during promotion".into()))
}
Err(e) => {
error!(?e, "Integrity verification failed during unseal promotion");
Err(Error::Internal("Integrity check failed".into()))
}
}
}
async fn should_run_gate(vault: &ActorRef<Vault>) -> Result<bool, Error> { let creds = if integrity::is_signing_available(&props.actors.vault)
let vault_state = vault
.ask(GetState {})
.await .await
.map_err(|_| Error::Internal("Failed to contact the vault".into()))?; .unwrap_or(false)
{
auth_creds.creds
} else {
run_vault_gate(props, &mut transport, auth_creds).await?
};
Ok(!matches!( Ok(UserAgentSession::spawn(UserAgentSession::new(
vault_state, props.clone(),
crate::actors::vault::VaultState::Unsealed creds,
)) oob_sender,
)))
} }
async fn run_vault_gate<T>( async fn run_vault_gate<T>(
props: &UserAgentConnection, props: &UserAgentConnection,
transport: &mut T, transport: &mut T,
auth_creds: Credentials, auth_creds: AuthCredentials,
) -> Result<(), Error> ) -> Result<Credentials, Error>
where where
T: Bi<vault_gate::Inbound, Result<vault_gate::Outbound, vault_gate::Error>> + Send + ?Sized, T: Bi<vault_gate::Inbound, Result<vault_gate::Outbound, vault_gate::Error>> + Send + ?Sized,
{ {
@@ -158,28 +173,3 @@ where
gate.kill(); gate.kill();
result result
} }
pub async fn start<T>(
props: &mut UserAgentConnection,
mut transport: T,
oob_sender: Box<dyn Sender<OutOfBand>>,
) -> Result<ActorRef<UserAgentSession>, Error>
where
T: Bi<auth::Inbound, Result<auth::Outbound, auth::Error>> + Send,
T: Bi<vault_gate::Inbound, Result<vault_gate::Outbound, vault_gate::Error>> + Send,
{
let creds = authenticate(props, &mut transport).await?;
// should run vault gate only if sealed / unbootstrapped
if should_run_gate(&props.actors.vault).await? {
run_vault_gate(props, &mut transport, creds.clone()).await?;
}
// checking the integrity
verify_integrity(&props.db, &props.actors.vault, &creds).await?;
Ok(UserAgentSession::spawn(UserAgentSession::new(
props.clone(),
oob_sender,
)))
}

View File

@@ -1,21 +1,37 @@
use super::{Error, UserAgentSession}; use std::sync::Mutex;
use crate::{
actors::evm::{ use alloy::{consensus::TxEip1559, primitives::Address, signers::Signature};
use arbiter_crypto::{
authn,
safecell::{SafeCell, SafeCellHandle as _},
};
use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit};
use diesel::{ExpressionMethods as _, QueryDsl as _, SelectableHelper};
use diesel_async::{AsyncConnection, RunQueryDsl};
use kameo::error::SendError;
use kameo::messages;
use kameo::prelude::Context;
use tracing::{error, info};
use x25519_dalek::{EphemeralSecret, PublicKey};
use crate::actors::flow_coordinator::client_connect_approval::ClientApprovalAnswer;
use crate::actors::{
evm::{
ClientSignTransaction, Generate, ListWallets, SignTransactionError as EvmSignError, ClientSignTransaction, Generate, ListWallets, SignTransactionError as EvmSignError,
UseragentCreateGrant, UseragentListGrants, UseragentCreateGrant, UseragentListGrants,
}, },
actors::flow_coordinator::client_connect_approval::ClientApprovalAnswer, vault::{self, Bootstrap, TryUnseal},
actors::vault::VaultState, };
db::models::{EvmWalletAccess, NewEvmWalletAccess, ProgramClient, ProgramClientMetadata}, use crate::db::models::{
evm::policies::{Grant, SpecificGrant}, EvmWalletAccess, NewEvmWalletAccess, ProgramClient, ProgramClientMetadata,
};
use crate::evm::policies::{Grant, SpecificGrant};
use crate::{
actors::vault::VaultState,
}; };
use arbiter_crypto::authn;
use alloy::{consensus::TxEip1559, primitives::Address, signers::Signature}; use super::{Error, UserAgentSession};
use diesel::{ExpressionMethods as _, QueryDsl as _, SelectableHelper};
use diesel_async::{AsyncConnection, RunQueryDsl};
use kameo::{error::SendError, messages, prelude::Context};
use tracing::error;
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum SignTransactionError { pub enum SignTransactionError {
@@ -98,7 +114,7 @@ impl UserAgentSession {
pub(crate) async fn handle_grant_create( pub(crate) async fn handle_grant_create(
&mut self, &mut self,
basic: crate::evm::policies::SharedGrantSettings, basic: crate::evm::policies::SharedGrantSettings,
grant: SpecificGrant, grant: crate::evm::policies::SpecificGrant,
) -> Result<i32, GrantMutationError> { ) -> Result<i32, GrantMutationError> {
match self match self
.props .props
@@ -116,7 +132,10 @@ impl UserAgentSession {
} }
#[message] #[message]
pub(crate) fn handle_grant_delete(&mut self, grant_id: i32) -> Result<(), GrantMutationError> { pub(crate) async fn handle_grant_delete(
&mut self,
grant_id: i32,
) -> Result<(), GrantMutationError> {
// match self // match self
// .props // .props
// .actors // .actors
@@ -234,10 +253,12 @@ impl UserAgentSession {
pubkey: authn::PublicKey, pubkey: authn::PublicKey,
ctx: &mut Context<Self, Result<(), Error>>, ctx: &mut Context<Self, Result<(), Error>>,
) -> Result<(), Error> { ) -> Result<(), Error> {
let Some(pending_approval) = self.pending_client_approvals.remove(&pubkey.to_bytes()) let pending_approval = match self.pending_client_approvals.remove(&pubkey.to_bytes()) {
else { Some(approval) => approval,
None => {
error!("Received client connection response for unknown client"); error!("Received client connection response for unknown client");
return Err(Error::internal("Unknown client in connection response")); return Err(Error::internal("Unknown client in connection response"));
}
}; };
pending_approval pending_approval

View File

@@ -1,19 +1,24 @@
use super::{OutOfBand, UserAgentConnection};
use crate::{
actors::{
flow_coordinator::client_connect_approval::ClientApprovalController,
useragent_registry::ConnectUseragent,
},
peers::client::ClientProfile,
};
use arbiter_crypto::authn; use arbiter_crypto::authn;
use arbiter_proto::transport::Sender; use diesel::{ExpressionMethods, QueryDsl};
use diesel_async::{RunQueryDsl};
use kameo_actors::message_bus::Register;
use kameo::{Actor, actor::ActorRef, messages};
use std::{borrow::Cow, collections::HashMap}; use std::{borrow::Cow, collections::HashMap};
use arbiter_proto::transport::Sender;
use kameo::{Actor, actor::ActorRef, messages, prelude::Message};
use thiserror::Error; use thiserror::Error;
use tracing::error; use tracing::error;
use crate::{
actors::{
flow_coordinator::{RegisterUserAgent, client_connect_approval::ClientApprovalController},
vault::events,
}, crypto::integrity, db::schema::useragent_client, peers::{client::ClientProfile, user_agent::{AuthCredentials, Credentials}}
};
use super::{OutOfBand, UserAgentConnection};
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum Error { pub enum Error {
#[error("State transition failed")] #[error("State transition failed")]
@@ -50,6 +55,7 @@ pub struct PendingClientApproval {
} }
pub struct UserAgentSession { pub struct UserAgentSession {
creds: Credentials,
props: UserAgentConnection, props: UserAgentConnection,
sender: Box<dyn Sender<OutOfBand>>, sender: Box<dyn Sender<OutOfBand>>,
@@ -59,8 +65,13 @@ pub struct UserAgentSession {
pub mod handlers; pub mod handlers;
impl UserAgentSession { impl UserAgentSession {
pub(crate) fn new(props: UserAgentConnection, sender: Box<dyn Sender<OutOfBand>>) -> Self { pub(crate) fn new(
props: UserAgentConnection,
creds: Credentials,
sender: Box<dyn Sender<OutOfBand>>,
) -> Self {
Self { Self {
creds,
props, props,
sender, sender,
pending_client_approvals: Default::default(), pending_client_approvals: Default::default(),
@@ -106,20 +117,23 @@ impl Actor for UserAgentSession {
type Error = Error; type Error = Error;
async fn on_start(args: Self::Args, this: ActorRef<Self>) -> Result<Self, Self::Error> { async fn on_start(
args: Self::Args,
this: kameo::prelude::ActorRef<Self>,
) -> Result<Self, Self::Error> {
args.props args.props
.actors .actors
.useragent_registry .flow_coordinator
.ask(ConnectUseragent { .ask(RegisterUserAgent {
actor: this.clone(), actor: this.clone(),
}) })
.await .await
.map_err(|err| { .map_err(|err| {
error!( error!(
?err, ?err,
"Failed to register user agent connection with user agent registry" "Failed to register user agent connection with flow coordinator"
); );
Error::internal("Failed to register user agent connection with user agent registry") Error::internal("Failed to register user agent connection with flow coordinator")
})?; })?;
Ok(args) Ok(args)
} }

View File

@@ -1,15 +1,4 @@
use super::Credentials;
use crate::{
actors::{
GlobalActors,
vault::{self, Bootstrap, GetState, TryUnseal, VaultState, events},
},
crypto::integrity::{self},
db::DatabasePool,
};
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _}; use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
use state::State;
use chacha20poly1305::{AeadInPlace, KeyInit as _, XChaCha20Poly1305, XNonce}; use chacha20poly1305::{AeadInPlace, KeyInit as _, XChaCha20Poly1305, XNonce};
use kameo::{Actor, error::SendError, messages, prelude::Message}; use kameo::{Actor, error::SendError, messages, prelude::Message};
use kameo_actors::message_bus::Register; use kameo_actors::message_bus::Register;
@@ -18,6 +7,17 @@ use tracing::{error, info};
use x25519_dalek::{EphemeralSecret, PublicKey, SharedSecret}; use x25519_dalek::{EphemeralSecret, PublicKey, SharedSecret};
pub mod state; pub mod state;
use state::*;
use super::{AuthCredentials, Credentials};
use crate::{
actors::{
GlobalActors,
vault::{self, Bootstrap, TryUnseal, events},
},
crypto::integrity::{self, AttestationStatus},
db::DatabasePool,
};
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum Error { pub enum Error {
@@ -43,8 +43,8 @@ pub struct HandshakeResponse {
} }
pub struct VaultGate { pub struct VaultGate {
pub auth_creds: Credentials, pub auth_creds: AuthCredentials,
pub promotion_tx: Option<oneshot::Sender<Result<(), Error>>>, pub promotion_tx: Option<oneshot::Sender<Result<Credentials, Error>>>,
pub state: State, pub state: State,
pub actors: GlobalActors, pub actors: GlobalActors,
pub db: DatabasePool, pub db: DatabasePool,
@@ -52,10 +52,10 @@ pub struct VaultGate {
impl VaultGate { impl VaultGate {
pub fn new( pub fn new(
auth_creds: Credentials, auth_creds: AuthCredentials,
actors: GlobalActors, actors: GlobalActors,
db: DatabasePool, db: DatabasePool,
promotion_tx: oneshot::Sender<Result<(), Error>>, promotion_tx: oneshot::Sender<Result<Credentials, Error>>,
) -> Self { ) -> Self {
Self { Self {
auth_creds, auth_creds,
@@ -110,7 +110,7 @@ impl VaultGate {
}); });
match decryption_result { match decryption_result {
Ok(()) => Ok(key_buffer), Ok(_) => Ok(key_buffer),
Err(err) => { Err(err) => {
error!(?err, "Failed to decrypt encrypted key material"); error!(?err, "Failed to decrypt encrypted key material");
Err(()) Err(())
@@ -122,9 +122,9 @@ impl VaultGate {
#[messages(messages = Inbound, replies = Outbound)] #[messages(messages = Inbound, replies = Outbound)]
impl VaultGate { impl VaultGate {
#[message] #[message]
pub fn handle_handshake( pub async fn handle_handshake(
&mut self, &mut self,
client_pubkey: PublicKey, client_pubkey: x25519_dalek::PublicKey,
) -> Result<HandshakeResponse, Error> { ) -> Result<HandshakeResponse, Error> {
let ephemeral_secret = EphemeralSecret::random(); let ephemeral_secret = EphemeralSecret::random();
let public_key = PublicKey::from(&ephemeral_secret); let public_key = PublicKey::from(&ephemeral_secret);
@@ -132,7 +132,7 @@ impl VaultGate {
let secret = ephemeral_secret.diffie_hellman(&client_pubkey); let secret = ephemeral_secret.diffie_hellman(&client_pubkey);
self.state = State::ReadyForExchange { self.state = State::ReadyForExchange {
server_key: public_key, server_key: public_key.clone(),
secret, secret,
}; };
@@ -152,9 +152,12 @@ impl VaultGate {
return Err(Error::State); return Err(Error::State);
}; };
let Ok(seal_key_buffer) = Self::decrypt_key(secret, &nonce, &ciphertext, &associated_data) let seal_key_buffer = match Self::decrypt_key(secret, &nonce, &ciphertext, &associated_data)
else { {
Ok(buffer) => buffer,
Err(()) => {
return Err(Error::InvalidKey); return Err(Error::InvalidKey);
}
}; };
match self match self
@@ -165,7 +168,7 @@ impl VaultGate {
}) })
.await .await
{ {
Ok(()) => { Ok(_) => {
info!("Successfully unsealed key with client-provided key"); info!("Successfully unsealed key with client-provided key");
Ok(()) Ok(())
} }
@@ -176,7 +179,7 @@ impl VaultGate {
} }
Err(err) => { Err(err) => {
error!(?err, "Failed to send unseal request to vault"); error!(?err, "Failed to send unseal request to vault");
Err(Error::internal("Vault actor error")) Err(Error::internal("Vault actor error").into())
} }
} }
} }
@@ -192,9 +195,12 @@ impl VaultGate {
return Err(Error::State); return Err(Error::State);
}; };
let Ok(seal_key_buffer) = Self::decrypt_key(secret, &nonce, &ciphertext, &associated_data) let seal_key_buffer = match Self::decrypt_key(secret, &nonce, &ciphertext, &associated_data)
else { {
Ok(buffer) => buffer,
Err(()) => {
return Err(Error::InvalidKey); return Err(Error::InvalidKey);
}
}; };
match self match self
@@ -205,7 +211,7 @@ impl VaultGate {
}) })
.await .await
{ {
Ok(()) => { Ok(_) => {
info!("Successfully bootstrapped vault with client-provided key"); info!("Successfully bootstrapped vault with client-provided key");
Ok(()) Ok(())
} }
@@ -222,18 +228,6 @@ impl VaultGate {
} }
} }
} }
#[message]
pub async fn handle_vault_state(&mut self) -> Result<VaultState, Error> {
let answer = self
.actors
.vault
.ask(GetState {})
.await
.map_err(|_| Error::internal("failed to query vault"))?;
Ok(answer)
}
} }
impl Message<events::Bootstrapped> for VaultGate { impl Message<events::Bootstrapped> for VaultGate {
@@ -245,23 +239,14 @@ impl Message<events::Bootstrapped> for VaultGate {
ctx: &mut kameo::prelude::Context<Self, Self::Reply>, ctx: &mut kameo::prelude::Context<Self, Self::Reply>,
) -> Self::Reply { ) -> Self::Reply {
let result = async { let result = async {
let mut conn = self let mut conn = self.db.get().await.map_err(|_| Error::internal("DB unavailable"))?;
.db integrity::sign_entity(&mut conn, &self.actors.vault, &self.auth_creds, self.auth_creds.creds.id)
.get()
.await
.map_err(|_| Error::internal("DB unavailable"))?;
integrity::sign_entity(
&mut conn,
&self.actors.vault,
&self.auth_creds,
self.auth_creds.id,
)
.await .await
.map_err(|e| { .map_err(|e| {
error!(?e, "Failed to sign integrity envelope on bootstrap"); error!(?e, "Failed to sign integrity envelope on bootstrap");
Error::internal("Integrity sign failed") Error::internal("Integrity sign failed")
})?; })?;
Ok(()) Ok(self.auth_creds.creds.clone())
} }
.await; .await;
@@ -280,8 +265,30 @@ impl Message<events::Unsealed> for VaultGate {
_: events::Unsealed, _: events::Unsealed,
ctx: &mut kameo::prelude::Context<Self, Self::Reply>, ctx: &mut kameo::prelude::Context<Self, Self::Reply>,
) -> Self::Reply { ) -> Self::Reply {
let result = async {
let mut conn = self.db.get().await.map_err(|_| Error::internal("DB unavailable"))?;
match integrity::verify_entity(
&mut conn,
&self.actors.vault,
&self.auth_creds,
self.auth_creds.creds.id,
)
.await
{
Ok(AttestationStatus::Attested) => Ok(self.auth_creds.creds.clone()),
Ok(AttestationStatus::Unavailable) => {
Err(Error::internal("Vault sealed during promotion"))
}
Err(e) => {
error!(?e, "Integrity verification failed during unseal promotion");
Err(Error::InvalidKey)
}
}
}
.await;
if let Some(tx) = self.promotion_tx.take() { if let Some(tx) = self.promotion_tx.take() {
let _ = tx.send(Ok(())); let _ = tx.send(result);
} }
ctx.stop(); ctx.stop();
} }

View File

@@ -1,4 +1,10 @@
use x25519_dalek::{PublicKey, SharedSecret}; use std::sync::Mutex;
use x25519_dalek::{EphemeralSecret, PublicKey, SharedSecret};
pub struct Handshake {
client_pubkey: PublicKey,
}
#[derive(Default)] #[derive(Default)]
pub enum State { pub enum State {

View File

@@ -1,22 +1,20 @@
use super::common::ChannelTransport;
use arbiter_crypto::{ use arbiter_crypto::{
authn::{self, AuthChallenge, CLIENT_CONTEXT}, authn::{self, CLIENT_CONTEXT, format_challenge},
safecell::{SafeCell, SafeCellHandle as _}, safecell::{SafeCell, SafeCellHandle as _},
}; };
use arbiter_proto::{ use arbiter_proto::ClientMetadata;
ClientMetadata, use arbiter_proto::transport::{Receiver, Sender};
transport::{Receiver, Sender},
};
use arbiter_server::{ use arbiter_server::{
actors::{GlobalActors, vault::Bootstrap}, actors::{GlobalActors, vault::Bootstrap},
crypto::integrity, crypto::integrity,
db::{self, schema}, db::{self, schema},
peers::client::{ClientConnection, ClientCredentials, auth, connect_client}, peers::client::{ClientConnection, ClientCredentials, auth, connect_client},
}; };
use diesel::{ExpressionMethods as _, NullableExpressionMethods as _, QueryDsl as _, insert_into}; use diesel::{ExpressionMethods as _, NullableExpressionMethods as _, QueryDsl as _, insert_into};
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
use ml_dsa::{KeyGen, MlDsa87, SigningKey, VerifyingKey, signature::Keypair}; use ml_dsa::{KeyGen, MlDsa87, SigningKey, VerifyingKey, signature::Keypair as _};
use super::common::ChannelTransport;
fn metadata(name: &str, description: Option<&str>, version: Option<&str>) -> ClientMetadata { fn metadata(name: &str, description: Option<&str>, version: Option<&str>) -> ClientMetadata {
ClientMetadata { ClientMetadata {
@@ -26,10 +24,6 @@ fn metadata(name: &str, description: Option<&str>, version: Option<&str>) -> Cli
} }
} }
fn verifying_key(key: &SigningKey<MlDsa87>) -> VerifyingKey<MlDsa87> {
<SigningKey<MlDsa87> as Keypair>::verifying_key(key)
}
async fn insert_registered_client( async fn insert_registered_client(
db: &db::DatabasePool, db: &db::DatabasePool,
actors: &GlobalActors, actors: &GlobalActors,
@@ -51,7 +45,7 @@ async fn insert_registered_client(
.unwrap(); .unwrap();
let client_id: i32 = insert_into(program_client::table) let client_id: i32 = insert_into(program_client::table)
.values(( .values((
program_client::public_key.eq(pubkey.encode().0.to_vec()), program_client::public_key.eq(pubkey.encode().to_vec()),
program_client::metadata_id.eq(metadata_id), program_client::metadata_id.eq(metadata_id),
)) ))
.returning(program_client::id) .returning(program_client::id)
@@ -64,6 +58,7 @@ async fn insert_registered_client(
&actors.vault, &actors.vault,
&ClientCredentials { &ClientCredentials {
pubkey: pubkey.into(), pubkey: pubkey.into(),
nonce: 1,
}, },
client_id, client_id,
) )
@@ -71,8 +66,12 @@ async fn insert_registered_client(
.unwrap(); .unwrap();
} }
fn sign_client_challenge(key: &SigningKey<MlDsa87>, challenge: &AuthChallenge) -> authn::Signature { fn sign_client_challenge(
let challenge = challenge.format(); key: &SigningKey<MlDsa87>,
nonce: i32,
pubkey: &authn::PublicKey,
) -> authn::Signature {
let challenge = format_challenge(nonce, &pubkey.to_bytes());
key.signing_key() key.signing_key()
.sign_deterministic(&challenge, CLIENT_CONTEXT) .sign_deterministic(&challenge, CLIENT_CONTEXT)
.unwrap() .unwrap()
@@ -81,13 +80,16 @@ fn sign_client_challenge(key: &SigningKey<MlDsa87>, challenge: &AuthChallenge) -
async fn insert_bootstrap_sentinel_useragent(db: &db::DatabasePool) { async fn insert_bootstrap_sentinel_useragent(db: &db::DatabasePool) {
let mut conn = db.get().await.unwrap(); let mut conn = db.get().await.unwrap();
let sentinel_key = verifying_key(&MlDsa87::key_gen(&mut rand::rng())) let sentinel_key = MlDsa87::key_gen(&mut rand::rng())
.verifying_key()
.encode() .encode()
.0
.to_vec(); .to_vec();
insert_into(schema::useragent_client::table) insert_into(schema::useragent_client::table)
.values((schema::useragent_client::public_key.eq(sentinel_key),)) .values((
schema::useragent_client::public_key.eq(sentinel_key),
schema::useragent_client::key_type.eq(1i32),
))
.execute(&mut conn) .execute(&mut conn)
.await .await
.unwrap(); .unwrap();
@@ -109,7 +111,7 @@ async fn spawn_test_actors(db: &db::DatabasePool) -> GlobalActors {
#[tokio::test] #[tokio::test]
#[test_log::test] #[test_log::test]
pub async fn unregistered_pubkey_rejected() { pub async fn test_unregistered_pubkey_rejected() {
let db = db::create_test_pool().await; let db = db::create_test_pool().await;
let (server_transport, mut test_transport) = ChannelTransport::new(); let (server_transport, mut test_transport) = ChannelTransport::new();
@@ -124,7 +126,7 @@ pub async fn unregistered_pubkey_rejected() {
test_transport test_transport
.send(auth::Inbound::AuthChallengeRequest { .send(auth::Inbound::AuthChallengeRequest {
pubkey: verifying_key(&new_key).into(), pubkey: new_key.verifying_key().into(),
metadata: metadata("client", Some("desc"), Some("1.0.0")), metadata: metadata("client", Some("desc"), Some("1.0.0")),
}) })
.await .await
@@ -136,18 +138,18 @@ pub async fn unregistered_pubkey_rejected() {
#[tokio::test] #[tokio::test]
#[test_log::test] #[test_log::test]
pub async fn challenge_auth() { pub async fn test_challenge_auth() {
let db = db::create_test_pool().await; let db = db::create_test_pool().await;
let actors = spawn_test_actors(&db).await; let actors = spawn_test_actors(&db).await;
let new_key = MlDsa87::key_gen(&mut rand::rng()); let new_key = MlDsa87::key_gen(&mut rand::rng());
Box::pin(insert_registered_client( insert_registered_client(
&db, &db,
&actors, &actors,
verifying_key(&new_key), new_key.verifying_key(),
&metadata("client", Some("desc"), Some("1.0.0")), &metadata("client", Some("desc"), Some("1.0.0")),
)) )
.await; .await;
let (server_transport, mut test_transport) = ChannelTransport::new(); let (server_transport, mut test_transport) = ChannelTransport::new();
@@ -160,7 +162,7 @@ pub async fn challenge_auth() {
// Send challenge request // Send challenge request
test_transport test_transport
.send(auth::Inbound::AuthChallengeRequest { .send(auth::Inbound::AuthChallengeRequest {
pubkey: verifying_key(&new_key).into(), pubkey: new_key.verifying_key().into(),
metadata: metadata("client", Some("desc"), Some("1.0.0")), metadata: metadata("client", Some("desc"), Some("1.0.0")),
}) })
.await .await
@@ -173,14 +175,14 @@ pub async fn challenge_auth() {
.expect("should receive challenge"); .expect("should receive challenge");
let challenge = match response { let challenge = match response {
Ok(resp) => match resp { Ok(resp) => match resp {
auth::Outbound::AuthChallenge { challenge } => challenge, auth::Outbound::AuthChallenge { pubkey, nonce } => (pubkey, nonce),
other @ auth::Outbound::AuthSuccess => panic!("Expected AuthChallenge, got {other:?}"), other => panic!("Expected AuthChallenge, got {other:?}"),
}, },
Err(err) => panic!("Expected Ok response, got Err({err:?})"), Err(err) => panic!("Expected Ok response, got Err({err:?})"),
}; };
// Sign the challenge and send solution // Sign the challenge and send solution
let signature = sign_client_challenge(&new_key, &challenge); let signature = sign_client_challenge(&new_key, challenge.1, &challenge.0);
test_transport test_transport
.send(auth::Inbound::AuthChallengeSolution { signature }) .send(auth::Inbound::AuthChallengeSolution { signature })
@@ -203,19 +205,13 @@ pub async fn challenge_auth() {
#[tokio::test] #[tokio::test]
#[test_log::test] #[test_log::test]
pub async fn metadata_unchanged_does_not_append_history() { pub async fn test_metadata_unchanged_does_not_append_history() {
let db = db::create_test_pool().await; let db = db::create_test_pool().await;
let actors = spawn_test_actors(&db).await; let actors = spawn_test_actors(&db).await;
let new_key = MlDsa87::key_gen(&mut rand::rng()); let new_key = MlDsa87::key_gen(&mut rand::rng());
let requested = metadata("client", Some("desc"), Some("1.0.0")); let requested = metadata("client", Some("desc"), Some("1.0.0"));
Box::pin(insert_registered_client( insert_registered_client(&db, &actors, new_key.verifying_key(), &requested).await;
&db,
&actors,
verifying_key(&new_key),
&requested,
))
.await;
let props = ClientConnection::new(db.clone(), actors); let props = ClientConnection::new(db.clone(), actors);
@@ -227,18 +223,18 @@ pub async fn metadata_unchanged_does_not_append_history() {
test_transport test_transport
.send(auth::Inbound::AuthChallengeRequest { .send(auth::Inbound::AuthChallengeRequest {
pubkey: verifying_key(&new_key).into(), pubkey: new_key.verifying_key().into(),
metadata: requested, metadata: requested,
}) })
.await .await
.unwrap(); .unwrap();
let response = test_transport.recv().await.unwrap().unwrap(); let response = test_transport.recv().await.unwrap().unwrap();
let challenge = match response { let (pubkey, nonce) = match response {
auth::Outbound::AuthChallenge { challenge } => challenge, auth::Outbound::AuthChallenge { pubkey, nonce } => (pubkey, nonce),
auth::Outbound::AuthSuccess => panic!("Expected AuthChallenge, got AuthSuccess"), other => panic!("Expected AuthChallenge, got {other:?}"),
}; };
let signature = sign_client_challenge(&new_key, &challenge); let signature = sign_client_challenge(&new_key, nonce, &pubkey);
test_transport test_transport
.send(auth::Inbound::AuthChallengeSolution { signature }) .send(auth::Inbound::AuthChallengeSolution { signature })
.await .await
@@ -266,17 +262,17 @@ pub async fn metadata_unchanged_does_not_append_history() {
#[tokio::test] #[tokio::test]
#[test_log::test] #[test_log::test]
pub async fn metadata_change_appends_history_and_repoints_binding() { pub async fn test_metadata_change_appends_history_and_repoints_binding() {
let db = db::create_test_pool().await; let db = db::create_test_pool().await;
let actors = spawn_test_actors(&db).await; let actors = spawn_test_actors(&db).await;
let new_key = MlDsa87::key_gen(&mut rand::rng()); let new_key = MlDsa87::key_gen(&mut rand::rng());
Box::pin(insert_registered_client( insert_registered_client(
&db, &db,
&actors, &actors,
verifying_key(&new_key), new_key.verifying_key(),
&metadata("client", Some("old"), Some("1.0.0")), &metadata("client", Some("old"), Some("1.0.0")),
)) )
.await; .await;
let props = ClientConnection::new(db.clone(), actors); let props = ClientConnection::new(db.clone(), actors);
@@ -289,23 +285,23 @@ pub async fn metadata_change_appends_history_and_repoints_binding() {
test_transport test_transport
.send(auth::Inbound::AuthChallengeRequest { .send(auth::Inbound::AuthChallengeRequest {
pubkey: verifying_key(&new_key).into(), pubkey: new_key.verifying_key().into(),
metadata: metadata("client", Some("new"), Some("2.0.0")), metadata: metadata("client", Some("new"), Some("2.0.0")),
}) })
.await .await
.unwrap(); .unwrap();
let response = test_transport.recv().await.unwrap().unwrap(); let response = test_transport.recv().await.unwrap().unwrap();
let challenge = match response { let (pubkey, nonce) = match response {
auth::Outbound::AuthChallenge { challenge } => challenge, auth::Outbound::AuthChallenge { pubkey, nonce } => (pubkey, nonce),
auth::Outbound::AuthSuccess => panic!("Expected AuthChallenge, got AuthSuccess"), other => panic!("Expected AuthChallenge, got {other:?}"),
}; };
let signature = sign_client_challenge(&new_key, &challenge); let signature = sign_client_challenge(&new_key, nonce, &pubkey);
test_transport test_transport
.send(auth::Inbound::AuthChallengeSolution { signature }) .send(auth::Inbound::AuthChallengeSolution { signature })
.await .await
.unwrap(); .unwrap();
drop(test_transport.recv().await.unwrap()); let _ = test_transport.recv().await.unwrap();
task.await.unwrap(); task.await.unwrap();
{ {
@@ -353,7 +349,7 @@ pub async fn metadata_change_appends_history_and_repoints_binding() {
#[tokio::test] #[tokio::test]
#[test_log::test] #[test_log::test]
pub async fn challenge_auth_rejects_integrity_tag_mismatch() { pub async fn test_challenge_auth_rejects_integrity_tag_mismatch() {
let db = db::create_test_pool().await; let db = db::create_test_pool().await;
let actors = spawn_test_actors(&db).await; let actors = spawn_test_actors(&db).await;
@@ -375,7 +371,7 @@ pub async fn challenge_auth_rejects_integrity_tag_mismatch() {
.unwrap(); .unwrap();
insert_into(program_client::table) insert_into(program_client::table)
.values(( .values((
program_client::public_key.eq(verifying_key(&new_key).encode().0.to_vec()), program_client::public_key.eq(new_key.verifying_key().encode().to_vec()),
program_client::metadata_id.eq(metadata_id), program_client::metadata_id.eq(metadata_id),
)) ))
.execute(&mut conn) .execute(&mut conn)
@@ -392,7 +388,7 @@ pub async fn challenge_auth_rejects_integrity_tag_mismatch() {
test_transport test_transport
.send(auth::Inbound::AuthChallengeRequest { .send(auth::Inbound::AuthChallengeRequest {
pubkey: verifying_key(&new_key).into(), pubkey: new_key.verifying_key().into(),
metadata: requested, metadata: requested,
}) })
.await .await

View File

@@ -1,7 +1,3 @@
#![allow(
dead_code,
reason = "Common test utilities that may not be used in every test"
)]
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _}; use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
use arbiter_proto::transport::{Bi, Error, Receiver, Sender}; use arbiter_proto::transport::{Bi, Error, Receiver, Sender};
use arbiter_server::{ use arbiter_server::{
@@ -14,7 +10,8 @@ use diesel::QueryDsl;
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
use tokio::sync::mpsc; use tokio::sync::mpsc;
pub(crate) async fn bootstrapped_vault(db: &db::DatabasePool) -> Vault { #[allow(dead_code)]
pub async fn bootstrapped_vault(db: &db::DatabasePool) -> Vault {
let mut actor = Vault::new(db.clone(), GlobalActors::spawn_message_bus()) let mut actor = Vault::new(db.clone(), GlobalActors::spawn_message_bus())
.await .await
.unwrap(); .unwrap();
@@ -25,7 +22,8 @@ pub(crate) async fn bootstrapped_vault(db: &db::DatabasePool) -> Vault {
actor actor
} }
pub(crate) async fn root_key_history_id(db: &db::DatabasePool) -> i32 { #[allow(dead_code)]
pub async fn root_key_history_id(db: &db::DatabasePool) -> i32 {
let mut conn = db.get().await.unwrap(); let mut conn = db.get().await.unwrap();
let id = schema::arbiter_settings::table let id = schema::arbiter_settings::table
.select(schema::arbiter_settings::root_key_id) .select(schema::arbiter_settings::root_key_id)
@@ -35,13 +33,15 @@ pub(crate) async fn root_key_history_id(db: &db::DatabasePool) -> i32 {
id.expect("root_key_id should be set after bootstrap") id.expect("root_key_id should be set after bootstrap")
} }
pub(crate) struct ChannelTransport<T, Y> { #[allow(dead_code)]
pub struct ChannelTransport<T, Y> {
receiver: mpsc::Receiver<T>, receiver: mpsc::Receiver<T>,
sender: mpsc::Sender<Y>, sender: mpsc::Sender<Y>,
} }
impl<T, Y> ChannelTransport<T, Y> { impl<T, Y> ChannelTransport<T, Y> {
pub(crate) fn new() -> (Self, ChannelTransport<Y, T>) { #[allow(dead_code)]
pub fn new() -> (Self, ChannelTransport<Y, T>) {
let (tx1, rx1) = mpsc::channel(10); let (tx1, rx1) = mpsc::channel(10);
let (tx2, rx2) = mpsc::channel(10); let (tx2, rx2) = mpsc::channel(10);
( (

View File

@@ -1,157 +1,36 @@
use super::common::ChannelTransport;
use arbiter_crypto::{ use arbiter_crypto::{
authn::{self, AuthChallenge, USERAGENT_CONTEXT}, authn::{self, USERAGENT_CONTEXT, format_challenge},
safecell::{SafeCell, SafeCellHandle as _}, safecell::{SafeCell, SafeCellHandle as _},
}; };
use arbiter_proto::transport::{Error as TransportError, Receiver, Sender};
use arbiter_proto::transport::{Receiver, Sender};
use arbiter_server::{ use arbiter_server::{
actors::{GlobalActors, bootstrap::GetToken, vault::Bootstrap}, actors::{GlobalActors, bootstrap::GetToken, vault::Bootstrap},
crypto::integrity, crypto::integrity,
db::{self, schema}, db::{self, schema},
peers::user_agent::{self, Credentials, UserAgentConnection, auth, vault_gate}, peers::user_agent::{AuthCredentials, Credentials, UserAgentConnection, auth},
}; };
use async_trait::async_trait;
use diesel::{ExpressionMethods as _, QueryDsl, insert_into}; use diesel::{ExpressionMethods as _, QueryDsl, insert_into};
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
use ml_dsa::{KeyGen, MlDsa87, SigningKey, VerifyingKey, signature::Keypair}; use ml_dsa::{KeyGen, MlDsa87, SigningKey, signature::Keypair as _};
use tokio::sync::mpsc;
fn verifying_key(key: &SigningKey<MlDsa87>) -> VerifyingKey<MlDsa87> { use super::common::ChannelTransport;
<SigningKey<MlDsa87> as Keypair>::verifying_key(key)
}
fn sign_useragent_challenge( fn sign_useragent_challenge(
key: &SigningKey<MlDsa87>, key: &SigningKey<MlDsa87>,
challenge: &AuthChallenge, nonce: i32,
pubkey_bytes: &[u8],
) -> authn::Signature { ) -> authn::Signature {
let challenge = challenge.format(); let challenge = format_challenge(nonce, pubkey_bytes);
key.signing_key() key.signing_key()
.sign_deterministic(&challenge, USERAGENT_CONTEXT) .sign_deterministic(&challenge, USERAGENT_CONTEXT)
.unwrap() .unwrap()
.into() .into()
} }
fn tamper_challenge(challenge: &AuthChallenge) -> AuthChallenge {
let mut challenge = challenge.clone();
challenge.nonce[0] ^= 1;
challenge
}
struct NullOobSender;
#[async_trait]
impl Sender<user_agent::OutOfBand> for NullOobSender {
async fn send(&mut self, _item: user_agent::OutOfBand) -> Result<(), TransportError> {
Ok(())
}
}
struct StartServerTransport {
auth_rx: mpsc::Receiver<auth::Inbound>,
auth_tx: mpsc::Sender<Result<auth::Outbound, auth::Error>>,
vault_rx: mpsc::Receiver<vault_gate::Inbound>,
vault_tx: mpsc::Sender<Result<vault_gate::Outbound, vault_gate::Error>>,
}
struct StartTestTransport {
auth_rx: mpsc::Receiver<Result<auth::Outbound, auth::Error>>,
auth_tx: mpsc::Sender<auth::Inbound>,
}
fn start_transport_pair() -> (StartServerTransport, StartTestTransport) {
let (auth_in_tx, auth_in_rx) = mpsc::channel(10);
let (auth_out_tx, auth_out_rx) = mpsc::channel(10);
let (_vault_in_tx, vault_in_rx) = mpsc::channel(10);
let (vault_out_tx, _vault_out_rx) = mpsc::channel(10);
(
StartServerTransport {
auth_rx: auth_in_rx,
auth_tx: auth_out_tx,
vault_rx: vault_in_rx,
vault_tx: vault_out_tx,
},
StartTestTransport {
auth_rx: auth_out_rx,
auth_tx: auth_in_tx,
},
)
}
#[async_trait]
impl Receiver<auth::Inbound> for StartServerTransport {
async fn recv(&mut self) -> Option<auth::Inbound> {
self.auth_rx.recv().await
}
}
#[async_trait]
impl Sender<Result<auth::Outbound, auth::Error>> for StartServerTransport {
async fn send(
&mut self,
item: Result<auth::Outbound, auth::Error>,
) -> Result<(), TransportError> {
self.auth_tx
.send(item)
.await
.map_err(|_| TransportError::ChannelClosed)
}
}
impl arbiter_proto::transport::Bi<auth::Inbound, Result<auth::Outbound, auth::Error>>
for StartServerTransport
{
}
#[async_trait]
impl Receiver<vault_gate::Inbound> for StartServerTransport {
async fn recv(&mut self) -> Option<vault_gate::Inbound> {
self.vault_rx.recv().await
}
}
#[async_trait]
impl Sender<Result<vault_gate::Outbound, vault_gate::Error>> for StartServerTransport {
async fn send(
&mut self,
item: Result<vault_gate::Outbound, vault_gate::Error>,
) -> Result<(), TransportError> {
self.vault_tx
.send(item)
.await
.map_err(|_| TransportError::ChannelClosed)
}
}
impl
arbiter_proto::transport::Bi<
vault_gate::Inbound,
Result<vault_gate::Outbound, vault_gate::Error>,
> for StartServerTransport
{
}
#[async_trait]
impl Receiver<Result<auth::Outbound, auth::Error>> for StartTestTransport {
async fn recv(&mut self) -> Option<Result<auth::Outbound, auth::Error>> {
self.auth_rx.recv().await
}
}
#[async_trait]
impl Sender<auth::Inbound> for StartTestTransport {
async fn send(&mut self, item: auth::Inbound) -> Result<(), TransportError> {
self.auth_tx
.send(item)
.await
.map_err(|_| TransportError::ChannelClosed)
}
}
#[tokio::test] #[tokio::test]
#[test_log::test] #[test_log::test]
pub async fn bootstrap_token_auth() { pub async fn test_bootstrap_token_auth() {
let db = db::create_test_pool().await; let db = db::create_test_pool().await;
let actors = GlobalActors::spawn(db.clone()).await.unwrap(); let actors = GlobalActors::spawn(db.clone()).await.unwrap();
actors actors
@@ -173,35 +52,20 @@ pub async fn bootstrap_token_auth() {
let new_key = MlDsa87::key_gen(&mut rand::rng()); let new_key = MlDsa87::key_gen(&mut rand::rng());
test_transport test_transport
.send(auth::Inbound::AuthChallengeRequest { .send(auth::Inbound::AuthChallengeRequest {
pubkey: verifying_key(&new_key).into(), pubkey: new_key.verifying_key().into(),
bootstrap_token: Some(token), bootstrap_token: Some(token),
}) })
.await .await
.unwrap(); .unwrap();
let response = test_transport
.recv()
.await
.expect("should receive challenge");
let challenge = match response {
Ok(auth::Outbound::AuthChallenge { challenge }) => challenge,
other => panic!("Expected AuthChallenge, got {other:?}"),
};
let signature = sign_useragent_challenge(&new_key, &challenge);
test_transport
.send(auth::Inbound::AuthChallengeSolution {
signature: signature.to_bytes(),
})
.await
.unwrap();
let response = test_transport let response = test_transport
.recv() .recv()
.await .await
.expect("should receive auth result"); .expect("should receive auth result");
assert!(matches!(response, Ok(auth::Outbound::AuthSuccess))); match response {
Ok(auth::Outbound::AuthSuccess) => {}
other => panic!("Expected AuthSuccess, got {other:?}"),
}
task.await.unwrap().unwrap(); task.await.unwrap().unwrap();
@@ -211,12 +75,12 @@ pub async fn bootstrap_token_auth() {
.first::<Vec<u8>>(&mut conn) .first::<Vec<u8>>(&mut conn)
.await .await
.unwrap(); .unwrap();
assert_eq!(stored_pubkey, verifying_key(&new_key).encode().0.to_vec()); assert_eq!(stored_pubkey, new_key.verifying_key().encode().to_vec());
} }
#[tokio::test] #[tokio::test]
#[test_log::test] #[test_log::test]
pub async fn bootstrap_invalid_token_auth() { pub async fn test_bootstrap_invalid_token_auth() {
let db = db::create_test_pool().await; let db = db::create_test_pool().await;
let actors = GlobalActors::spawn(db.clone()).await.unwrap(); let actors = GlobalActors::spawn(db.clone()).await.unwrap();
@@ -230,25 +94,8 @@ pub async fn bootstrap_invalid_token_auth() {
let new_key = MlDsa87::key_gen(&mut rand::rng()); let new_key = MlDsa87::key_gen(&mut rand::rng());
test_transport test_transport
.send(auth::Inbound::AuthChallengeRequest { .send(auth::Inbound::AuthChallengeRequest {
pubkey: verifying_key(&new_key).into(), pubkey: new_key.verifying_key().into(),
bootstrap_token: Some("invalid_token".to_owned()), bootstrap_token: Some("invalid_token".to_string()),
})
.await
.unwrap();
let response = test_transport
.recv()
.await
.expect("should receive challenge");
let challenge = match response {
Ok(auth::Outbound::AuthChallenge { challenge }) => challenge,
other => panic!("Expected AuthChallenge, got {other:?}"),
};
let signature = sign_useragent_challenge(&new_key, &challenge);
test_transport
.send(auth::Inbound::AuthChallengeSolution {
signature: signature.to_bytes(),
}) })
.await .await
.unwrap(); .unwrap();
@@ -269,7 +116,7 @@ pub async fn bootstrap_invalid_token_auth() {
#[tokio::test] #[tokio::test]
#[test_log::test] #[test_log::test]
pub async fn challenge_auth() { pub async fn test_challenge_auth() {
let db = db::create_test_pool().await; let db = db::create_test_pool().await;
let actors = GlobalActors::spawn(db.clone()).await.unwrap(); let actors = GlobalActors::spawn(db.clone()).await.unwrap();
actors actors
@@ -281,12 +128,15 @@ pub async fn challenge_auth() {
.unwrap(); .unwrap();
let new_key = MlDsa87::key_gen(&mut rand::rng()); let new_key = MlDsa87::key_gen(&mut rand::rng());
let pubkey_bytes = authn::PublicKey::from(verifying_key(&new_key)).to_bytes(); let pubkey_bytes = new_key.verifying_key().encode().to_vec();
{ {
let mut conn = db.get().await.unwrap(); let mut conn = db.get().await.unwrap();
let id: i32 = insert_into(schema::useragent_client::table) let id: i32 = 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),
))
.returning(schema::useragent_client::id) .returning(schema::useragent_client::id)
.get_result(&mut conn) .get_result(&mut conn)
.await .await
@@ -294,9 +144,12 @@ pub async fn challenge_auth() {
integrity::sign_entity( integrity::sign_entity(
&mut conn, &mut conn,
&actors.vault, &actors.vault,
&Credentials { &AuthCredentials {
creds: Credentials {
id, id,
pubkey: verifying_key(&new_key).into(), pubkey: new_key.verifying_key().into(),
},
new_nonce: 1,
}, },
id, id,
) )
@@ -313,7 +166,7 @@ pub async fn challenge_auth() {
test_transport test_transport
.send(auth::Inbound::AuthChallengeRequest { .send(auth::Inbound::AuthChallengeRequest {
pubkey: verifying_key(&new_key).into(), pubkey: new_key.verifying_key().into(),
bootstrap_token: None, bootstrap_token: None,
}) })
.await .await
@@ -325,13 +178,13 @@ pub async fn challenge_auth() {
.expect("should receive challenge"); .expect("should receive challenge");
let challenge = match response { let challenge = match response {
Ok(resp) => match resp { Ok(resp) => match resp {
auth::Outbound::AuthChallenge { challenge } => challenge, auth::Outbound::AuthChallenge { nonce } => nonce,
auth::Outbound::AuthSuccess => panic!("Expected AuthChallenge, got AuthSuccess"), other => panic!("Expected AuthChallenge, got {other:?}"),
}, },
Err(err) => panic!("Expected Ok response, got Err({err:?})"), Err(err) => panic!("Expected Ok response, got Err({err:?})"),
}; };
let signature = sign_useragent_challenge(&new_key, &challenge); let signature = sign_useragent_challenge(&new_key, challenge, &pubkey_bytes);
test_transport test_transport
.send(auth::Inbound::AuthChallengeSolution { .send(auth::Inbound::AuthChallengeSolution {
@@ -354,7 +207,7 @@ pub async fn challenge_auth() {
#[tokio::test] #[tokio::test]
#[test_log::test] #[test_log::test]
pub async fn challenge_auth_rejects_integrity_tag_mismatch_when_unsealed() { pub async fn test_challenge_auth_rejects_integrity_tag_mismatch_when_unsealed() {
let db = db::create_test_pool().await; let db = db::create_test_pool().await;
let actors = GlobalActors::spawn(db.clone()).await.unwrap(); let actors = GlobalActors::spawn(db.clone()).await.unwrap();
@@ -367,68 +220,44 @@ pub async fn challenge_auth_rejects_integrity_tag_mismatch_when_unsealed() {
.unwrap(); .unwrap();
let new_key = MlDsa87::key_gen(&mut rand::rng()); let new_key = MlDsa87::key_gen(&mut rand::rng());
let pubkey_bytes = authn::PublicKey::from(verifying_key(&new_key)).to_bytes(); let pubkey_bytes = new_key.verifying_key().encode().to_vec();
{ {
let mut conn = db.get().await.unwrap(); let mut conn = db.get().await.unwrap();
insert_into(schema::useragent_client::table) 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) .execute(&mut conn)
.await .await
.unwrap(); .unwrap();
} }
let (server_transport, mut test_transport) = start_transport_pair(); let (mut server_transport, mut test_transport) = ChannelTransport::new();
let db_for_task = db.clone(); let db_for_task = db.clone();
let task = tokio::spawn(async move { let task = tokio::spawn(async move {
let mut props = UserAgentConnection::new(db_for_task, actors); let mut props = UserAgentConnection::new(db_for_task, actors);
user_agent::start(&mut props, server_transport, Box::new(NullOobSender)).await auth::authenticate(&mut props, &mut server_transport).await
}); });
test_transport test_transport
.send(auth::Inbound::AuthChallengeRequest { .send(auth::Inbound::AuthChallengeRequest {
pubkey: verifying_key(&new_key).into(), pubkey: new_key.verifying_key().into(),
bootstrap_token: None, bootstrap_token: None,
}) })
.await .await
.unwrap(); .unwrap();
let response = test_transport
.recv()
.await
.expect("should receive challenge");
let challenge = match response {
Ok(resp) => match resp {
auth::Outbound::AuthChallenge { challenge } => challenge,
other => panic!("Expected AuthChallenge, got {other:?}"),
},
Err(err) => panic!("Expected Ok response, got Err({err:?})"),
};
let signature = sign_useragent_challenge(&new_key, &challenge);
test_transport
.send(auth::Inbound::AuthChallengeSolution {
signature: signature.to_bytes(),
})
.await
.unwrap();
let response = test_transport
.recv()
.await
.expect("should receive auth result");
assert!(matches!(response, Ok(auth::Outbound::AuthSuccess)));
assert!(matches!( assert!(matches!(
task.await.unwrap(), task.await.unwrap(),
Err(user_agent::Error::Internal(_)) Err(auth::Error::Internal { .. })
)); ));
} }
#[tokio::test] #[tokio::test]
#[test_log::test] #[test_log::test]
pub async fn challenge_auth_rejects_invalid_signature() { pub async fn test_challenge_auth_rejects_invalid_signature() {
let db = db::create_test_pool().await; let db = db::create_test_pool().await;
let actors = GlobalActors::spawn(db.clone()).await.unwrap(); let actors = GlobalActors::spawn(db.clone()).await.unwrap();
actors actors
@@ -440,12 +269,15 @@ pub async fn challenge_auth_rejects_invalid_signature() {
.unwrap(); .unwrap();
let new_key = MlDsa87::key_gen(&mut rand::rng()); let new_key = MlDsa87::key_gen(&mut rand::rng());
let pubkey_bytes = authn::PublicKey::from(verifying_key(&new_key)).to_bytes(); let pubkey_bytes = new_key.verifying_key().encode().to_vec();
{ {
let mut conn = db.get().await.unwrap(); let mut conn = db.get().await.unwrap();
let id: i32 = insert_into(schema::useragent_client::table) let id: i32 = 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),
))
.returning(schema::useragent_client::id) .returning(schema::useragent_client::id)
.get_result(&mut conn) .get_result(&mut conn)
.await .await
@@ -453,9 +285,12 @@ pub async fn challenge_auth_rejects_invalid_signature() {
integrity::sign_entity( integrity::sign_entity(
&mut conn, &mut conn,
&actors.vault, &actors.vault,
&Credentials { &AuthCredentials {
creds: Credentials {
id, id,
pubkey: verifying_key(&new_key).into(), pubkey: new_key.verifying_key().into(),
},
new_nonce: 1,
}, },
id, id,
) )
@@ -472,7 +307,7 @@ pub async fn challenge_auth_rejects_invalid_signature() {
test_transport test_transport
.send(auth::Inbound::AuthChallengeRequest { .send(auth::Inbound::AuthChallengeRequest {
pubkey: verifying_key(&new_key).into(), pubkey: new_key.verifying_key().into(),
bootstrap_token: None, bootstrap_token: None,
}) })
.await .await
@@ -484,13 +319,13 @@ pub async fn challenge_auth_rejects_invalid_signature() {
.expect("should receive challenge"); .expect("should receive challenge");
let challenge = match response { let challenge = match response {
Ok(resp) => match resp { Ok(resp) => match resp {
auth::Outbound::AuthChallenge { challenge } => challenge, auth::Outbound::AuthChallenge { nonce } => nonce,
auth::Outbound::AuthSuccess => panic!("Expected AuthChallenge, got AuthSuccess"), other => panic!("Expected AuthChallenge, got {other:?}"),
}, },
Err(err) => panic!("Expected Ok response, got Err({err:?})"), Err(err) => panic!("Expected Ok response, got Err({err:?})"),
}; };
let signature = sign_useragent_challenge(&new_key, &tamper_challenge(&challenge)); let signature = sign_useragent_challenge(&new_key, challenge + 1, &pubkey_bytes);
test_transport test_transport
.send(auth::Inbound::AuthChallengeSolution { .send(auth::Inbound::AuthChallengeSolution {

View File

@@ -9,10 +9,8 @@ use arbiter_server::{
}, },
db, db,
peers::user_agent::{ peers::user_agent::{
Credentials, AuthCredentials, Credentials,
vault_gate::{ vault_gate::{Error as VaultGateError, HandleHandshake, HandleUnsealEncryptedKey, VaultGate},
Error as VaultGateError, HandleHandshake, HandleUnsealEncryptedKey, VaultGate,
},
}, },
}; };
@@ -23,11 +21,7 @@ use x25519_dalek::{EphemeralSecret, PublicKey};
async fn setup_sealed_gate( async fn setup_sealed_gate(
seal_key: &[u8], seal_key: &[u8],
) -> ( ) -> (db::DatabasePool, kameo::actor::ActorRef<VaultGate>, oneshot::Receiver<Result<Credentials, VaultGateError>>) {
db::DatabasePool,
kameo::actor::ActorRef<VaultGate>,
oneshot::Receiver<Result<(), VaultGateError>>,
) {
let db = db::create_test_pool().await; let db = db::create_test_pool().await;
let actors = GlobalActors::spawn(db.clone()).await.unwrap(); let actors = GlobalActors::spawn(db.clone()).await.unwrap();
@@ -42,7 +36,10 @@ async fn setup_sealed_gate(
let (promotion_tx, promotion_rx) = oneshot::channel(); let (promotion_tx, promotion_rx) = oneshot::channel();
let pubkey = authn::SigningKey::generate().public_key(); let pubkey = authn::SigningKey::generate().public_key();
let auth_creds = Credentials { id: 1, pubkey }; let auth_creds = AuthCredentials {
creds: Credentials { id: 1, pubkey },
new_nonce: 1,
};
let gate = VaultGate::spawn(VaultGate::new(auth_creds, actors, db.clone(), promotion_tx)); let gate = VaultGate::spawn(VaultGate::new(auth_creds, actors, db.clone(), promotion_tx));
(db, gate, promotion_rx) (db, gate, promotion_rx)
@@ -82,7 +79,7 @@ async fn client_dh_encrypt(
#[tokio::test] #[tokio::test]
#[test_log::test] #[test_log::test]
pub async fn unseal_success() { pub async fn test_unseal_success() {
let seal_key = b"test-seal-key"; let seal_key = b"test-seal-key";
let (_db, gate, _promotion_rx) = setup_sealed_gate(seal_key).await; let (_db, gate, _promotion_rx) = setup_sealed_gate(seal_key).await;
@@ -94,9 +91,8 @@ pub async fn unseal_success() {
#[tokio::test] #[tokio::test]
#[test_log::test] #[test_log::test]
pub async fn unseal_wrong_seal_key() { pub async fn test_unseal_wrong_seal_key() {
let seal_key = b"test-seal-key"; let (_db, gate, _promotion_rx) = setup_sealed_gate(b"correct-key").await;
let (_db, gate, _promotion_rx) = setup_sealed_gate(seal_key).await;
let encrypted_key = client_dh_encrypt(&gate, b"wrong-key").await; let encrypted_key = client_dh_encrypt(&gate, b"wrong-key").await;
@@ -111,9 +107,8 @@ pub async fn unseal_wrong_seal_key() {
#[tokio::test] #[tokio::test]
#[test_log::test] #[test_log::test]
pub async fn unseal_corrupted_ciphertext() { pub async fn test_unseal_corrupted_ciphertext() {
let seal_key = b"test-seal-key"; let (_db, gate, _promotion_rx) = setup_sealed_gate(b"test-key").await;
let (_db, gate, _promotion_rx) = setup_sealed_gate(seal_key).await;
let client_secret = EphemeralSecret::random(); let client_secret = EphemeralSecret::random();
let client_public = PublicKey::from(&client_secret); let client_public = PublicKey::from(&client_secret);
@@ -142,7 +137,7 @@ pub async fn unseal_corrupted_ciphertext() {
#[tokio::test] #[tokio::test]
#[test_log::test] #[test_log::test]
pub async fn unseal_retry_after_invalid_key() { pub async fn test_unseal_retry_after_invalid_key() {
let seal_key = b"real-seal-key"; let seal_key = b"real-seal-key";
let (_db, gate, _promotion_rx) = setup_sealed_gate(seal_key).await; let (_db, gate, _promotion_rx) = setup_sealed_gate(seal_key).await;

View File

@@ -1,4 +1,5 @@
use crate::common; use std::collections::{HashMap, HashSet};
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _}; use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
use arbiter_server::{ use arbiter_server::{
actors::{ actors::{
@@ -11,9 +12,10 @@ use arbiter_server::{
use diesel::{ExpressionMethods as _, QueryDsl, SelectableHelper, dsl::sql_query}; use diesel::{ExpressionMethods as _, QueryDsl, SelectableHelper, dsl::sql_query};
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
use kameo::actor::{ActorRef, Spawn as _}; use kameo::actor::{ActorRef, Spawn as _};
use std::collections::{HashMap, HashSet};
use tokio::task::JoinSet; use tokio::task::JoinSet;
use crate::common;
async fn write_concurrently( async fn write_concurrently(
actor: ActorRef<Vault>, actor: ActorRef<Vault>,
prefix: &'static str, prefix: &'static str,

View File

@@ -1,4 +1,3 @@
use crate::common;
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _}; use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
use arbiter_server::{ use arbiter_server::{
actors::{ actors::{
@@ -12,6 +11,8 @@ use arbiter_server::{
use diesel::{QueryDsl, SelectableHelper}; use diesel::{QueryDsl, SelectableHelper};
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
use crate::common;
#[tokio::test] #[tokio::test]
#[test_log::test] #[test_log::test]
async fn test_bootstrap() { async fn test_bootstrap() {
@@ -86,7 +87,7 @@ async fn test_new_restores_sealed_state() {
.await .await
.unwrap(); .unwrap();
let err = actor2.decrypt(1).await.unwrap_err(); let err = actor2.decrypt(1).await.unwrap_err();
assert!(matches!(err, Error::Sealed)); assert!(matches!(err, Error::NotBootstrapped));
} }
#[tokio::test] #[tokio::test]

View File

@@ -1,4 +1,5 @@
use crate::common; use std::collections::HashSet;
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _}; use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
use arbiter_server::{ use arbiter_server::{
actors::vault::Error, actors::vault::Error,
@@ -8,7 +9,8 @@ use arbiter_server::{
use diesel::{ExpressionMethods as _, QueryDsl, SelectableHelper, dsl::update}; use diesel::{ExpressionMethods as _, QueryDsl, SelectableHelper, dsl::update};
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
use std::collections::HashSet;
use crate::common;
#[tokio::test] #[tokio::test]
#[test_log::test] #[test_log::test]

View File

@@ -7,7 +7,6 @@ import 'package:arbiter/features/identity/pk_manager.dart';
import 'package:arbiter/proto/arbiter.pbgrpc.dart'; import 'package:arbiter/proto/arbiter.pbgrpc.dart';
import 'package:arbiter/proto/user_agent/auth.pb.dart' as ua_auth; import 'package:arbiter/proto/user_agent/auth.pb.dart' as ua_auth;
import 'package:arbiter/proto/user_agent.pb.dart'; import 'package:arbiter/proto/user_agent.pb.dart';
import 'package:arbiter/src/rust/api.dart';
import 'package:grpc/grpc.dart'; import 'package:grpc/grpc.dart';
import 'package:mtcore/markettakers.dart'; import 'package:mtcore/markettakers.dart';
@@ -93,10 +92,7 @@ Future<Connection> connectAndAuthorize(
); );
} }
final challenge = await formatChallenge( final challenge = _formatChallenge(authResponse.challenge, pubkey);
random: authResponse.challenge.random,
timestamp: authResponse.challenge.timestampNanos.toInt(),
);
talker.info( talker.info(
'Received auth challenge, signing with key ${base64Encode(pubkey)}', 'Received auth challenge, signing with key ${base64Encode(pubkey)}',
); );
@@ -168,3 +164,9 @@ Future<Connection> _connect(StoredServerInfo serverInfo) async {
return Connection(channel: channel, tx: tx, rx: rx); return Connection(channel: channel, tx: tx, rx: rx);
} }
List<int> _formatChallenge(ua_auth.AuthChallenge challenge, List<int> pubkey) {
final encodedPubkey = base64Encode(pubkey);
final payload = "${challenge.nonce}:$encodedPubkey";
return utf8.encode(payload);
}

View File

@@ -12,7 +12,6 @@
import 'dart:core' as $core; import 'dart:core' as $core;
import 'package:fixnum/fixnum.dart' as $fixnum;
import 'package:protobuf/protobuf.dart' as $pb; import 'package:protobuf/protobuf.dart' as $pb;
import '../shared/client.pb.dart' as $0; import '../shared/client.pb.dart' as $0;
@@ -95,12 +94,12 @@ class AuthChallengeRequest extends $pb.GeneratedMessage {
class AuthChallenge extends $pb.GeneratedMessage { class AuthChallenge extends $pb.GeneratedMessage {
factory AuthChallenge({ factory AuthChallenge({
$fixnum.Int64? timestampNanos, $core.List<$core.int>? pubkey,
$core.List<$core.int>? random, $core.int? nonce,
}) { }) {
final result = create(); final result = create();
if (timestampNanos != null) result.timestampNanos = timestampNanos; if (pubkey != null) result.pubkey = pubkey;
if (random != null) result.random = random; if (nonce != null) result.nonce = nonce;
return result; return result;
} }
@@ -118,11 +117,9 @@ class AuthChallenge extends $pb.GeneratedMessage {
package: package:
const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.client.auth'), const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.client.auth'),
createEmptyInstance: create) createEmptyInstance: create)
..a<$fixnum.Int64>(
1, _omitFieldNames ? '' : 'timestampNanos', $pb.PbFieldType.OU6,
defaultOrMaker: $fixnum.Int64.ZERO)
..a<$core.List<$core.int>>( ..a<$core.List<$core.int>>(
2, _omitFieldNames ? '' : 'random', $pb.PbFieldType.OY) 1, _omitFieldNames ? '' : 'pubkey', $pb.PbFieldType.OY)
..aI(2, _omitFieldNames ? '' : 'nonce')
..hasRequiredFields = false; ..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
@@ -145,22 +142,22 @@ class AuthChallenge extends $pb.GeneratedMessage {
static AuthChallenge? _defaultInstance; static AuthChallenge? _defaultInstance;
@$pb.TagNumber(1) @$pb.TagNumber(1)
$fixnum.Int64 get timestampNanos => $_getI64(0); $core.List<$core.int> get pubkey => $_getN(0);
@$pb.TagNumber(1) @$pb.TagNumber(1)
set timestampNanos($fixnum.Int64 value) => $_setInt64(0, value); set pubkey($core.List<$core.int> value) => $_setBytes(0, value);
@$pb.TagNumber(1) @$pb.TagNumber(1)
$core.bool hasTimestampNanos() => $_has(0); $core.bool hasPubkey() => $_has(0);
@$pb.TagNumber(1) @$pb.TagNumber(1)
void clearTimestampNanos() => $_clearField(1); void clearPubkey() => $_clearField(1);
@$pb.TagNumber(2) @$pb.TagNumber(2)
$core.List<$core.int> get random => $_getN(1); $core.int get nonce => $_getIZ(1);
@$pb.TagNumber(2) @$pb.TagNumber(2)
set random($core.List<$core.int> value) => $_setBytes(1, value); set nonce($core.int value) => $_setSignedInt32(1, value);
@$pb.TagNumber(2) @$pb.TagNumber(2)
$core.bool hasRandom() => $_has(1); $core.bool hasNonce() => $_has(1);
@$pb.TagNumber(2) @$pb.TagNumber(2)
void clearRandom() => $_clearField(2); void clearNonce() => $_clearField(2);
} }
class AuthChallengeSolution extends $pb.GeneratedMessage { class AuthChallengeSolution extends $pb.GeneratedMessage {

View File

@@ -62,15 +62,15 @@ final $typed_data.Uint8List authChallengeRequestDescriptor = $convert.base64Deco
const AuthChallenge$json = { const AuthChallenge$json = {
'1': 'AuthChallenge', '1': 'AuthChallenge',
'2': [ '2': [
{'1': 'timestamp_nanos', '3': 1, '4': 1, '5': 4, '10': 'timestampNanos'}, {'1': 'pubkey', '3': 1, '4': 1, '5': 12, '10': 'pubkey'},
{'1': 'random', '3': 2, '4': 1, '5': 12, '10': 'random'}, {'1': 'nonce', '3': 2, '4': 1, '5': 5, '10': 'nonce'},
], ],
}; };
/// Descriptor for `AuthChallenge`. Decode as a `google.protobuf.DescriptorProto`. /// Descriptor for `AuthChallenge`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List authChallengeDescriptor = $convert.base64Decode( final $typed_data.Uint8List authChallengeDescriptor = $convert.base64Decode(
'Cg1BdXRoQ2hhbGxlbmdlEicKD3RpbWVzdGFtcF9uYW5vcxgBIAEoBFIOdGltZXN0YW1wTmFub3' 'Cg1BdXRoQ2hhbGxlbmdlEhYKBnB1YmtleRgBIAEoDFIGcHVia2V5EhQKBW5vbmNlGAIgASgFUg'
'MSFgoGcmFuZG9tGAIgASgMUgZyYW5kb20='); 'Vub25jZQ==');
@$core.Deprecated('Use authChallengeSolutionDescriptor instead') @$core.Deprecated('Use authChallengeSolutionDescriptor instead')
const AuthChallengeSolution$json = { const AuthChallengeSolution$json = {

View File

@@ -12,7 +12,6 @@
import 'dart:core' as $core; import 'dart:core' as $core;
import 'package:fixnum/fixnum.dart' as $fixnum;
import 'package:protobuf/protobuf.dart' as $pb; import 'package:protobuf/protobuf.dart' as $pb;
import 'auth.pbenum.dart'; import 'auth.pbenum.dart';
@@ -91,12 +90,10 @@ class AuthChallengeRequest extends $pb.GeneratedMessage {
class AuthChallenge extends $pb.GeneratedMessage { class AuthChallenge extends $pb.GeneratedMessage {
factory AuthChallenge({ factory AuthChallenge({
$fixnum.Int64? timestampNanos, $core.int? nonce,
$core.List<$core.int>? random,
}) { }) {
final result = create(); final result = create();
if (timestampNanos != null) result.timestampNanos = timestampNanos; if (nonce != null) result.nonce = nonce;
if (random != null) result.random = random;
return result; return result;
} }
@@ -114,11 +111,7 @@ class AuthChallenge extends $pb.GeneratedMessage {
package: const $pb.PackageName( package: const $pb.PackageName(
_omitMessageNames ? '' : 'arbiter.user_agent.auth'), _omitMessageNames ? '' : 'arbiter.user_agent.auth'),
createEmptyInstance: create) createEmptyInstance: create)
..a<$fixnum.Int64>( ..aI(1, _omitFieldNames ? '' : 'nonce')
1, _omitFieldNames ? '' : 'timestampNanos', $pb.PbFieldType.OU6,
defaultOrMaker: $fixnum.Int64.ZERO)
..a<$core.List<$core.int>>(
2, _omitFieldNames ? '' : 'random', $pb.PbFieldType.OY)
..hasRequiredFields = false; ..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
@@ -141,22 +134,13 @@ class AuthChallenge extends $pb.GeneratedMessage {
static AuthChallenge? _defaultInstance; static AuthChallenge? _defaultInstance;
@$pb.TagNumber(1) @$pb.TagNumber(1)
$fixnum.Int64 get timestampNanos => $_getI64(0); $core.int get nonce => $_getIZ(0);
@$pb.TagNumber(1) @$pb.TagNumber(1)
set timestampNanos($fixnum.Int64 value) => $_setInt64(0, value); set nonce($core.int value) => $_setSignedInt32(0, value);
@$pb.TagNumber(1) @$pb.TagNumber(1)
$core.bool hasTimestampNanos() => $_has(0); $core.bool hasNonce() => $_has(0);
@$pb.TagNumber(1) @$pb.TagNumber(1)
void clearTimestampNanos() => $_clearField(1); void clearNonce() => $_clearField(1);
@$pb.TagNumber(2)
$core.List<$core.int> get random => $_getN(1);
@$pb.TagNumber(2)
set random($core.List<$core.int> value) => $_setBytes(1, value);
@$pb.TagNumber(2)
$core.bool hasRandom() => $_has(1);
@$pb.TagNumber(2)
void clearRandom() => $_clearField(2);
} }
class AuthChallengeSolution extends $pb.GeneratedMessage { class AuthChallengeSolution extends $pb.GeneratedMessage {

View File

@@ -67,15 +67,13 @@ final $typed_data.Uint8List authChallengeRequestDescriptor = $convert.base64Deco
const AuthChallenge$json = { const AuthChallenge$json = {
'1': 'AuthChallenge', '1': 'AuthChallenge',
'2': [ '2': [
{'1': 'timestamp_nanos', '3': 1, '4': 1, '5': 4, '10': 'timestampNanos'}, {'1': 'nonce', '3': 1, '4': 1, '5': 5, '10': 'nonce'},
{'1': 'random', '3': 2, '4': 1, '5': 12, '10': 'random'},
], ],
}; };
/// Descriptor for `AuthChallenge`. Decode as a `google.protobuf.DescriptorProto`. /// Descriptor for `AuthChallenge`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List authChallengeDescriptor = $convert.base64Decode( final $typed_data.Uint8List authChallengeDescriptor = $convert
'Cg1BdXRoQ2hhbGxlbmdlEicKD3RpbWVzdGFtcF9uYW5vcxgBIAEoBFIOdGltZXN0YW1wTmFub3' .base64Decode('Cg1BdXRoQ2hhbGxlbmdlEhQKBW5vbmNlGAEgASgFUgVub25jZQ==');
'MSFgoGcmFuZG9tGAIgASgMUgZyYW5kb20=');
@$core.Deprecated('Use authChallengeSolutionDescriptor instead') @$core.Deprecated('Use authChallengeSolutionDescriptor instead')
const AuthChallengeSolution$json = { const AuthChallengeSolution$json = {

View File

@@ -6,14 +6,6 @@
import 'frb_generated.dart'; import 'frb_generated.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
Future<Uint8List> formatChallenge({
required List<int> random,
required PlatformInt64 timestamp,
}) => RustLib.instance.api.crateApiFormatChallenge(
random: random,
timestamp: timestamp,
);
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<MldsaKey>> // Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<MldsaKey>>
abstract class MldsaKey implements RustOpaqueInterface { abstract class MldsaKey implements RustOpaqueInterface {
static Future<MldsaKey> fromBytes({required List<int> bytes}) => static Future<MldsaKey> fromBytes({required List<int> bytes}) =>

View File

@@ -64,7 +64,7 @@ class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
String get codegenVersion => '2.12.0'; String get codegenVersion => '2.12.0';
@override @override
int get rustContentHash => 1247923898; int get rustContentHash => -437661335;
static const kDefaultExternalLibraryLoaderConfig = static const kDefaultExternalLibraryLoaderConfig =
ExternalLibraryLoaderConfig( ExternalLibraryLoaderConfig(
@@ -89,11 +89,6 @@ abstract class RustLibApi extends BaseApi {
Future<Uint8List> crateApiMldsaKeyToBytes({required MldsaKey that}); Future<Uint8List> crateApiMldsaKeyToBytes({required MldsaKey that});
Future<Uint8List> crateApiFormatChallenge({
required List<int> random,
required PlatformInt64 timestamp,
});
RustArcIncrementStrongCountFnType RustArcIncrementStrongCountFnType
get rust_arc_increment_strong_count_MldsaKey; get rust_arc_increment_strong_count_MldsaKey;
@@ -272,40 +267,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
TaskConstMeta get kCrateApiMldsaKeyToBytesConstMeta => TaskConstMeta get kCrateApiMldsaKeyToBytesConstMeta =>
const TaskConstMeta(debugName: "MldsaKey_to_bytes", argNames: ["that"]); const TaskConstMeta(debugName: "MldsaKey_to_bytes", argNames: ["that"]);
@override
Future<Uint8List> crateApiFormatChallenge({
required List<int> random,
required PlatformInt64 timestamp,
}) {
return handler.executeNormal(
NormalTask(
callFfi: (port_) {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_list_prim_u_8_loose(random, serializer);
sse_encode_i_64(timestamp, serializer);
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 6,
port: port_,
);
},
codec: SseCodec(
decodeSuccessData: sse_decode_list_prim_u_8_strict,
decodeErrorData: sse_decode_String,
),
constMeta: kCrateApiFormatChallengeConstMeta,
argValues: [random, timestamp],
apiImpl: this,
),
);
}
TaskConstMeta get kCrateApiFormatChallengeConstMeta => const TaskConstMeta(
debugName: "format_challenge",
argNames: ["random", "timestamp"],
);
RustArcIncrementStrongCountFnType RustArcIncrementStrongCountFnType
get rust_arc_increment_strong_count_MldsaKey => wire get rust_arc_increment_strong_count_MldsaKey => wire
.rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey; .rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey;
@@ -353,12 +314,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return raw as String; return raw as String;
} }
@protected
PlatformInt64 dco_decode_i_64(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
return dcoDecodeI64(raw);
}
@protected @protected
List<int> dco_decode_list_prim_u_8_loose(dynamic raw) { List<int> dco_decode_list_prim_u_8_loose(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs // Codec=Dco (DartCObject based), see doc to use other codecs
@@ -439,12 +394,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return utf8.decoder.convert(inner); return utf8.decoder.convert(inner);
} }
@protected
PlatformInt64 sse_decode_i_64(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
return deserializer.buffer.getPlatformInt64();
}
@protected @protected
List<int> sse_decode_list_prim_u_8_loose(SseDeserializer deserializer) { List<int> sse_decode_list_prim_u_8_loose(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs // Codec=Sse (Serialization based), see doc to use other codecs
@@ -542,12 +491,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
sse_encode_list_prim_u_8_strict(utf8.encoder.convert(self), serializer); sse_encode_list_prim_u_8_strict(utf8.encoder.convert(self), serializer);
} }
@protected
void sse_encode_i_64(PlatformInt64 self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
serializer.buffer.putPlatformInt64(self);
}
@protected @protected
void sse_encode_list_prim_u_8_loose( void sse_encode_list_prim_u_8_loose(
List<int> self, List<int> self,

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