12 Commits

Author SHA1 Message Date
Skipper
929d50b589 housekeeping(server): clean too-broad visibility markers and organize imports
Some checks failed
ci/woodpecker/pr/server-audit Pipeline was successful
ci/woodpecker/pr/server-vet Pipeline failed
ci/woodpecker/pr/server-lint Pipeline failed
ci/woodpecker/pr/server-test Pipeline failed
ci/woodpecker/pr/useragent-analyze Pipeline failed
2026-04-18 13:30:09 +02:00
Skipper
70acfc99b5 merge: refactor-integrity-check into main 2026-04-18 13:19:13 +02:00
Skipper
4a8e51ef32 docs: updated to new auth challenge format and removed stale TOCTOU race condition note
Some checks failed
ci/woodpecker/pr/server-lint Pipeline failed
ci/woodpecker/pr/server-audit Pipeline failed
ci/woodpecker/pr/server-vet Pipeline failed
ci/woodpecker/pr/server-test Pipeline was successful
ci/woodpecker/pr/useragent-analyze Pipeline failed
2026-04-17 18:25:55 +02:00
Skipper
9ee86afc19 fix(useragent): now using new challenge format 2026-04-17 18:19:51 +02:00
Skipper
790026e93b fix(server::tests): api surface of auth challenge changed 2026-04-17 17:58:22 +02:00
Skipper
0e09afda5d refactor(server::{useragent::auth, client::auth}): use random based + timestamp nonce instead of monotonic counter in database 2026-04-17 17:44:42 +02:00
Skipper
51e6571d80 refactor(server): now keeps track of useragents, instead of 2026-04-17 00:00:43 +02:00
Skipper
3b828d5874 refactor(server::grpc::vault_gate): standard approach using / traits 2026-04-16 22:15:18 +02:00
Skipper
a6f94e3115 fix(server): sending fixed vault state when on stage 2026-04-16 19:36:41 +02:00
hdbg
f49e995c2f WIP: kameo::messages wiring for transport generalization
Some checks failed
ci/woodpecker/pr/server-test Pipeline failed
ci/woodpecker/pr/server-vet Pipeline failed
ci/woodpecker/pr/server-audit Pipeline failed
ci/woodpecker/pr/server-lint Pipeline failed
ci/woodpecker/pr/useragent-analyze Pipeline failed
2026-04-16 17:18:46 +02:00
62dff3f810 Merge pull request 'refactor(hashing): introduce Hashable derive macro and migrate server types' (#82) from hashing-proc-macro into main
Some checks failed
ci/woodpecker/push/server-audit Pipeline was successful
ci/woodpecker/push/server-lint Pipeline was successful
ci/woodpecker/push/server-vet Pipeline failed
ci/woodpecker/push/server-test Pipeline was successful
Reviewed-on: #82
Reviewed-by: Stas <business@jexter.tech>
2026-04-08 00:18:40 +00:00
CleverWild
6e22f368c9 refactor(hashing): introduce Hashable derive macro and migrate server types
Some checks failed
ci/woodpecker/pr/server-audit Pipeline was successful
ci/woodpecker/pr/server-vet Pipeline failed
ci/woodpecker/pr/server-lint Pipeline was successful
ci/woodpecker/pr/server-test Pipeline was successful
2026-04-08 01:32:59 +02:00
99 changed files with 2022 additions and 1591 deletions

View File

@@ -29,38 +29,23 @@ flowchart TD
A([Client connects]) --> B[Receive AuthChallengeRequest]
B --> C{pubkey in DB?}
C -- yes --> D[Read nonce\nIncrement nonce in DB]
D --> G
C -- yes --> G[Generate AuthChallenge]
C -- no --> E[Ask all UserAgents:\nClientConnectionRequest]
E --> F{First response}
F -- denied --> Z([Reject connection])
F -- approved --> F2[Cancel remaining\nUserAgent requests]
F2 --> F3[INSERT client\nnonce = 1]
F3 --> G[Send AuthChallenge\nwith nonce]
F2 --> F3[INSERT client]
F3 --> G
G --> H[Receive AuthChallengeSolution]
H --> I{Signature valid?}
I -- no --> Z
I -- yes --> J([Session started])
G --> H[Send AuthChallenge\ntimestamp + random bytes]
H --> I[Receive AuthChallengeSolution]
I --> K{Signature valid?}
K -- no --> Z
K -- yes --> J([Session started])
```
### Known Issue: Concurrent Registration Race (TOCTOU)
Two connections presenting the same previously-unknown public key can race through the approval flow simultaneously:
1. Both check the DB → neither is registered.
2. Both request approval from user agents → both receive approval.
3. Both `INSERT` the client record → the second insert silently overwrites the first, resetting the nonce.
This means the first connection's nonce is invalidated by the second, causing its challenge verification to fail. A fix requires either serialising new-client registration (e.g. an in-memory lock keyed on pubkey) or replacing the separate check + insert with an `INSERT OR IGNORE` / upsert guarded by a unique constraint on `public_key`.
### Nonce Semantics
The `program_client.nonce` column stores the **next usable nonce** — i.e. it is always one ahead of the nonce last issued in a challenge.
- **New client:** inserted with `nonce = 1`; the first challenge is issued with `nonce = 0`.
- **Existing client:** the current DB value is read and used as the challenge nonce, then immediately incremented within the same exclusive transaction, preventing replay.
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`.
---

View File

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

View File

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

102
server/Cargo.lock generated
View File

@@ -95,7 +95,7 @@ dependencies = [
"either",
"k256",
"once_cell",
"rand 0.8.5",
"rand 0.8.6",
"secp256k1",
"serde",
"serde_json",
@@ -516,7 +516,7 @@ dependencies = [
"alloy-signer",
"async-trait",
"k256",
"rand 0.8.5",
"rand 0.8.6",
"thiserror 2.0.18",
]
@@ -683,6 +683,7 @@ dependencies = [
"arbiter-crypto",
"arbiter-proto",
"async-trait",
"chrono",
"http",
"rand 0.10.1",
"rustls-webpki",
@@ -696,13 +697,25 @@ dependencies = [
name = "arbiter-crypto"
version = "0.1.0"
dependencies = [
"base64",
"alloy",
"chrono",
"hmac",
"memsafe",
"ml-dsa",
"rand 0.10.1",
"x-wing",
]
[[package]]
name = "arbiter-macros"
version = "0.1.0"
dependencies = [
"arbiter-crypto",
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "arbiter-proto"
version = "0.1.0"
@@ -737,6 +750,7 @@ dependencies = [
"alloy",
"anyhow",
"arbiter-crypto",
"arbiter-macros",
"arbiter-proto",
"arbiter-tokens-registry",
"argon2",
@@ -970,7 +984,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1df2c09229cbc5a028b1d70e00fdb2acee28b1055dfb5ca73eea49c5a25c4e7c"
dependencies = [
"num-traits",
"rand 0.8.5",
"rand 0.8.6",
]
[[package]]
@@ -980,7 +994,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185"
dependencies = [
"num-traits",
"rand 0.8.5",
"rand 0.8.6",
]
[[package]]
@@ -990,7 +1004,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a"
dependencies = [
"num-traits",
"rand 0.8.5",
"rand 0.8.6",
]
[[package]]
@@ -1096,9 +1110,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "aws-lc-rs"
version = "1.16.2"
version = "1.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc"
checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f"
dependencies = [
"aws-lc-sys",
"untrusted 0.7.1",
@@ -1107,9 +1121,9 @@ dependencies = [
[[package]]
name = "aws-lc-sys"
version = "0.39.1"
version = "0.40.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83a25cf98105baa966497416dbd42565ce3a8cf8dbfd59803ec9ad46f3126399"
checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7"
dependencies = [
"cc",
"cmake",
@@ -1247,9 +1261,9 @@ dependencies = [
[[package]]
name = "bitflags"
version = "2.11.0"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
[[package]]
name = "bitvec"
@@ -1520,11 +1534,12 @@ checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
[[package]]
name = "const_format"
version = "0.2.35"
version = "0.2.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad"
checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e"
dependencies = [
"const_format_proc_macros",
"konst",
]
[[package]]
@@ -2241,7 +2256,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534"
dependencies = [
"byteorder",
"rand 0.8.5",
"rand 0.8.6",
"rustc-hex",
"static_assertions",
]
@@ -2660,9 +2675,9 @@ dependencies = [
[[package]]
name = "hyper-rustls"
version = "0.27.8"
version = "0.27.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2b52f86d1d4bc0d6b4e6826d960b1b333217e07d36b882dca570a5e1c48895b"
checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
dependencies = [
"http",
"hyper",
@@ -3125,6 +3140,21 @@ dependencies = [
"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]]
name = "lazy_static"
version = "1.5.0"
@@ -4061,9 +4091,9 @@ checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
[[package]]
name = "rand"
version = "0.8.5"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
dependencies = [
"libc",
"rand_chacha 0.3.1",
@@ -4376,7 +4406,7 @@ dependencies = [
"parity-scale-codec",
"primitive-types",
"proptest",
"rand 0.8.5",
"rand 0.8.6",
"rand 0.9.4",
"rlp",
"ruint-macro",
@@ -4515,9 +4545,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
[[package]]
name = "rustls-webpki"
version = "0.103.11"
version = "0.103.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20a6af516fea4b20eccceaf166e8aa666ac996208e8a644ce3ef5aa783bc7cd4"
checksum = "8279bb85272c9f10811ae6a6c547ff594d6a7f3c6c6b02ee9726d1d0dcfcdd06"
dependencies = [
"aws-lc-rs",
"ring",
@@ -4622,7 +4652,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252"
dependencies = [
"bitcoin_hashes",
"rand 0.8.5",
"rand 0.8.6",
"secp256k1-sys",
"serde",
]
@@ -5295,9 +5325,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.51.1"
version = "1.52.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f66bf9585cda4b724d3e78ab34b73fb2bbaba9011b9bfdf69dc836382ea13b8c"
checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6"
dependencies = [
"bytes",
"libc",
@@ -5711,9 +5741,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "uuid"
version = "1.23.0"
version = "1.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9"
checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -5773,11 +5803,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasip2"
version = "1.0.2+wasi-0.2.9"
version = "1.0.3+wasi-0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
dependencies = [
"wit-bindgen",
"wit-bindgen 0.57.1",
]
[[package]]
@@ -5786,7 +5816,7 @@ version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
dependencies = [
"wit-bindgen",
"wit-bindgen 0.51.0",
]
[[package]]
@@ -5914,9 +5944,9 @@ dependencies = [
[[package]]
name = "webpki-root-certs"
version = "1.0.6"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca"
checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c"
dependencies = [
"rustls-pki-types",
]
@@ -6260,6 +6290,12 @@ dependencies = [
"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]]
name = "wit-bindgen-core"
version = "0.51.0"

View File

@@ -48,4 +48,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_actors = {git = "https://github.com/hdbg/kameo.git", rev = "805b417"}
hmac = "0.12.1"

View File

@@ -24,3 +24,4 @@ http = "1.4.0"
rustls-webpki = { version = "0.103.10", features = ["aws-lc-rs"] }
async-trait.workspace = true
rand.workspace = true
chrono.workspace = true

View File

@@ -1,4 +1,8 @@
use arbiter_crypto::authn::{CLIENT_CONTEXT, SigningKey, format_challenge};
use crate::{
storage::StorageError,
transport::{ClientTransport, next_request_id},
};
use arbiter_crypto::authn::{self, CLIENT_CONTEXT, SigningKey};
use arbiter_proto::{
ClientMetadata,
proto::{
@@ -16,13 +20,12 @@ use arbiter_proto::{
},
};
use crate::{
storage::StorageError,
transport::{ClientTransport, next_request_id},
};
use chrono::DateTime;
#[derive(Debug, thiserror::Error)]
pub enum AuthError {
#[error("Server sent invalid auth challenge")]
InvalidChallenge,
#[error("Auth challenge was not returned by server")]
MissingAuthChallenge,
@@ -98,7 +101,15 @@ async fn send_auth_challenge_solution(
key: &SigningKey,
challenge: AuthChallenge,
) -> std::result::Result<(), AuthError> {
let challenge_payload = format_challenge(challenge.nonce, &challenge.pubkey);
let timestamp = DateTime::from_timestamp_nanos(challenge.timestamp_nanos as i64);
let challenge = authn::AuthChallenge {
nonce: *challenge
.random
.as_array()
.ok_or(AuthError::InvalidChallenge)?,
timestamp,
};
let challenge_payload: Vec<u8> = challenge.format();
let signature = key
.sign_message(&challenge_payload, CLIENT_CONTEXT)
.map_err(|_| AuthError::UnexpectedAuthResponse)?

View File

@@ -1,8 +1,8 @@
use std::io::{self, Write};
use arbiter_client::ArbiterClient;
use arbiter_proto::{ClientMetadata, url::ArbiterUrl};
use std::io::{self, Write};
#[tokio::main]
async fn main() {
println!("Testing connection to Arbiter server...");

View File

@@ -1,21 +1,20 @@
use arbiter_crypto::authn::SigningKey;
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;
#[cfg(feature = "evm")]
use crate::wallets::evm::ArbiterEvmWallet;
use crate::{
StorageError,
auth::{AuthError, authenticate},
storage::{FileSigningKeyStorage, SigningKeyStorage},
transport::{BUFFER_LENGTH, ClientTransport},
};
use arbiter_crypto::authn::SigningKey;
use arbiter_proto::{
ClientMetadata, proto::arbiter_service_client::ArbiterServiceClient, url::ArbiterUrl,
};
#[cfg(feature = "evm")]
use crate::wallets::evm::ArbiterEvmWallet;
use std::sync::Arc;
use tokio::sync::{Mutex, mpsc};
use tokio_stream::wrappers::ReceiverStream;
use tonic::transport::ClientTlsConfig;
#[derive(Debug, thiserror::Error)]
pub enum Error {

View File

@@ -1,5 +1,6 @@
use arbiter_crypto::authn::SigningKey;
use arbiter_proto::home_path;
use std::path::{Path, PathBuf};
#[derive(Debug, thiserror::Error)]

View File

@@ -1,4 +1,5 @@
use arbiter_proto::proto::client::{ClientRequest, ClientResponse};
use std::sync::atomic::{AtomicI32, Ordering};
use tokio::sync::mpsc;

View File

@@ -1,13 +1,4 @@
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 crate::transport::{ClientTransport, next_request_id};
use arbiter_proto::proto::{
client::{
ClientRequest,
@@ -25,7 +16,15 @@ use arbiter_proto::proto::{
shared::evm::TransactionEvalError,
};
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;
/// A typed error payload returned by [`ArbiterEvmWallet`] transaction signing.
///

View File

@@ -6,14 +6,16 @@ edition = "2024"
[dependencies]
ml-dsa = {workspace = true, optional = true }
rand = {workspace = true, optional = true}
base64 = {workspace = true, 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"] }
chrono.workspace = true
[lints]
workspace = true
[features]
default = ["authn", "safecell"]
authn = ["dep:ml-dsa", "dep:rand", "dep:base64"]
authn = ["dep:ml-dsa", "dep:rand"]
safecell = ["dep:memsafe"]

View File

@@ -1,17 +1,55 @@
use std::hash::Hash;
use base64::{Engine as _, prelude::BASE64_STANDARD};
use chrono::{DateTime, Utc};
use hmac::digest::Digest;
use ml_dsa::{
EncodedVerifyingKey, Error, KeyGen, MlDsa87, Seed, Signature as MlDsaSignature,
SigningKey as MlDsaSigningKey, VerifyingKey as MlDsaVerifyingKey, signature::Keypair as _,
};
use rand::RngExt;
pub static CLIENT_CONTEXT: &[u8] = b"arbiter_client";
pub static USERAGENT_CONTEXT: &[u8] = b"arbiter_user_agent";
pub fn format_challenge(nonce: i32, pubkey: &[u8]) -> Vec<u8> {
let concat_form = format!("{}:{}", nonce, BASE64_STANDARD.encode(pubkey));
concat_form.into_bytes()
const NONCE_SIZE: usize = 32;
#[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, ()> {
let random_nonce = nonce.as_array().ok_or(())?;
Ok(AuthChallenge {
nonce: *random_nonce,
timestamp: DateTime::from_timestamp_nanos(timestamp),
})
}
}
pub type KeyParams = MlDsa87;
@@ -19,9 +57,9 @@ pub type KeyParams = MlDsa87;
#[derive(Clone, Debug, PartialEq)]
pub struct PublicKey(Box<MlDsaVerifyingKey<KeyParams>>);
impl Hash for PublicKey {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.to_bytes().hash(state);
impl crate::hashing::Hashable for PublicKey {
fn hash<H: Digest>(&self, hasher: &mut H) {
hasher.update(self.to_bytes());
}
}
@@ -33,21 +71,19 @@ pub struct SigningKey(Box<MlDsaSigningKey<KeyParams>>);
impl PublicKey {
pub fn to_bytes(&self) -> Vec<u8> {
self.0.encode().to_vec()
self.0.encode().0.to_vec()
}
pub fn verify(&self, nonce: i32, context: &[u8], signature: &Signature) -> bool {
self.0.verify_with_context(
&format_challenge(nonce, &self.to_bytes()),
context,
&signature.0,
)
pub fn verify(&self, challenge: &AuthChallenge, context: &[u8], signature: &Signature) -> bool {
let challenge = challenge.format();
self.0
.verify_with_context(&challenge, context, &signature.0)
}
}
impl Signature {
pub fn to_bytes(&self) -> Vec<u8> {
self.0.encode().to_vec()
self.0.encode().0.to_vec()
}
}
@@ -75,11 +111,14 @@ impl SigningKey {
.map(Into::into)
}
pub fn sign_challenge(&self, nonce: i32, context: &[u8]) -> Result<Signature, Error> {
self.sign_message(
&format_challenge(nonce, &self.public_key().to_bytes()),
context,
)
pub fn sign_challenge(
&self,
challenge: &AuthChallenge,
context: &[u8],
) -> Result<Signature, Error> {
let challenge = challenge.format();
self.sign_message(&challenge, context)
}
}
@@ -140,6 +179,8 @@ impl TryFrom<&'_ [u8]> for Signature {
mod tests {
use ml_dsa::{KeyGen, MlDsa87, signature::Keypair as _};
use crate::authn::AuthChallenge;
use super::{CLIENT_CONTEXT, PublicKey, Signature, SigningKey, USERAGENT_CONTEXT};
#[test]
@@ -169,13 +210,13 @@ mod tests {
fn challenge_verification_uses_context_and_canonical_key_bytes() {
let key = SigningKey::generate();
let public_key = key.public_key();
let nonce = 17;
let challenge = AuthChallenge::generate(&mut rand::rng());
let signature = key
.sign_challenge(nonce, CLIENT_CONTEXT)
.sign_challenge(&challenge, CLIENT_CONTEXT)
.expect("signature should be created");
assert!(public_key.verify(nonce, CLIENT_CONTEXT, &signature));
assert!(!public_key.verify(nonce, USERAGENT_CONTEXT, &signature));
assert!(public_key.verify(&challenge, CLIENT_CONTEXT, &signature));
assert!(!public_key.verify(&challenge, USERAGENT_CONTEXT, &signature));
}
#[test]
@@ -185,10 +226,16 @@ mod tests {
assert_eq!(restored.public_key(), original.public_key());
let challenge = AuthChallenge::generate(&mut rand::rng());
let signature = restored
.sign_challenge(9, CLIENT_CONTEXT)
.sign_challenge(&challenge, CLIENT_CONTEXT)
.expect("signature should be created");
assert!(restored.public_key().verify(9, CLIENT_CONTEXT, &signature));
assert!(
restored
.public_key()
.verify(&challenge, CLIENT_CONTEXT, &signature)
);
}
}

View File

@@ -1,21 +1,26 @@
use hmac::digest::Digest;
use std::collections::HashSet;
pub use hmac::digest::Digest;
/// 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 {
fn hash<H: Digest>(&self, hasher: &mut H);
}
macro_rules! impl_numeric {
($($t:ty),*) => {
$(
impl Hashable for $t {
fn hash<H: Digest>(&self, hasher: &mut H) {
hasher.update(&self.to_be_bytes());
($($t:ty),*) => {
$(
impl Hashable for $t {
fn hash<H: Digest>(&self, hasher: &mut H) {
hasher.update(&self.to_be_bytes());
}
}
}
)*
};
)*
};
}
impl_numeric!(u8, u16, u32, u64, i8, i16, i32, i64);

View File

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

View File

@@ -1,7 +1,9 @@
use std::ops::{Deref, DerefMut};
use std::{any::type_name, fmt};
use memsafe::MemSafe;
use std::{
any::type_name,
fmt,
ops::{Deref, DerefMut},
};
pub trait SafeCellHandle<T> {
type CellRead<'a>: Deref<Target = T>

View File

@@ -0,0 +1,18 @@
[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

@@ -0,0 +1,131 @@
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: &syn::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

@@ -0,0 +1,10 @@
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

@@ -0,0 +1,19 @@
pub(crate) struct ToPath(pub(crate) &'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) => {{
#[cfg(test)]
#[expect(unused_imports)]
use $path as _;
ToPath(stringify!($path))
}};
}
pub(crate) const HASHABLE_TRAIT_PATH: ToPath = ensure_path!(::arbiter_crypto::hashing::Hashable);
pub(crate) const HMAC_DIGEST_PATH: ToPath = ensure_path!(::arbiter_crypto::hashing::Digest);

View File

@@ -54,10 +54,9 @@
//! as a closed outbound channel.
//! - [`Bi::recv`] returns `None` when the underlying transport closes.
//! - Message translation is intentionally out of scope for this module.
use std::marker::PhantomData;
use async_trait::async_trait;
use kameo::{error::Infallible, prelude::*};
use std::marker::PhantomData;
/// Errors returned by transport adapters implementing [`Bi`].
#[derive(thiserror::Error, Debug)]
@@ -191,3 +190,29 @@ where
}
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 futures::StreamExt;
use tokio::sync::mpsc;
use tokio_stream::wrappers::ReceiverStream;
use super::{Bi, Receiver, Sender};
pub struct GrpcSender<Outbound> {
tx: mpsc::Sender<Result<Outbound, tonic::Status>>,
}

View File

@@ -1,7 +1,6 @@
use std::fmt::Display;
use base64::{Engine as _, prelude::BASE64_URL_SAFE};
use rustls_pki_types::CertificateDer;
use std::fmt::Display;
const ARBITER_URL_SCHEME: &str = "arbiter";
const CERT_QUERY_KEY: &str = "cert";

View File

@@ -18,6 +18,7 @@ diesel-async = { version = "0.8.0", features = [
] }
arbiter-proto.path = "../arbiter-proto"
arbiter-crypto.path = "../arbiter-crypto"
arbiter-macros.path = "../arbiter-macros"
tracing.workspace = true
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tonic.workspace = true
@@ -44,7 +45,7 @@ restructed = "0.2.2"
strum = { version = "0.28.0", features = ["derive"] }
pem = "3.0.6"
sha2.workspace = true
hmac = "0.12"
hmac.workspace = true
spki.workspace = true
alloy.workspace = true
prost-types.workspace = true

View File

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

View File

@@ -1,13 +1,13 @@
use crate::db::{self, DatabasePool, schema};
use arbiter_proto::{BOOTSTRAP_PATH, home_path};
use diesel::QueryDsl;
use diesel_async::RunQueryDsl;
use kameo::{Actor, messages};
use rand::{RngExt, distr::Alphanumeric, make_rng, rngs::StdRng};
use subtle::ConstantTimeEq as _;
use thiserror::Error;
use crate::db::{self, DatabasePool, schema};
const TOKEN_LENGTH: usize = 64;
pub async fn generate_token() -> Result<String, std::io::Error> {

View File

@@ -1,11 +1,3 @@
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::{
actors::vault::{CreateNew, Decrypt, Vault},
crypto::integrity,
@@ -24,6 +16,14 @@ use crate::{
};
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
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};
pub use crate::evm::safe_signer;
#[derive(Debug, thiserror::Error)]

View File

@@ -1,11 +1,3 @@
use std::ops::ControlFlow;
use kameo::{
Actor, messages,
prelude::{ActorId, ActorRef, ActorStopReason, Context, WeakActorRef},
reply::ReplySender,
};
use crate::{
actors::flow_coordinator::ApprovalError,
peers::{
@@ -14,6 +6,13 @@ use crate::{
},
};
use kameo::{
Actor, messages,
prelude::{ActorId, ActorRef, ActorStopReason, Context, WeakActorRef},
reply::ReplySender,
};
use std::ops::ControlFlow;
pub struct Args {
pub client: ClientProfile,
pub user_agents: Vec<ActorRef<UserAgentSession>>,

View File

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

View File

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

View File

@@ -1,57 +1,61 @@
use alloy::primitives::map::HashMap;
use arbiter_crypto::authn;
use kameo::{error::Infallible, prelude::*};
use crate::peers::user_agent::UserAgentSession;
use crate::{db::DatabasePool, peers::user_agent::{Credentials, UserAgentSession}};
use super::vault::{Vault, events as vault_events};
pub struct Args {
pub vault: ActorRef<Vault>,
pub pool: DatabasePool,
}
use kameo::{
Actor,
actor::{ActorId, ActorRef},
error::Infallible,
messages,
prelude::{ActorStopReason, Context, WeakActorRef},
};
use std::{collections::HashMap, ops::ControlFlow};
use tracing::info;
#[derive(Default)]
pub struct UserAgentRegistry {
vault: ActorRef<Vault>,
pool: DatabasePool,
connected: HashMap<Credentials, ActorRef<UserAgentSession>>,
connected: HashMap<ActorId, 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 {
type Args = Args;
type Args = Self;
type Error = Infallible;
async fn on_start(args: Self::Args, actor_ref: ActorRef<Self>) -> Result<Self, Self::Error> {
Ok(Self {
vault: args.vault,
pool: args.pool,
connected: HashMap::default(),
})
async fn on_start(args: Self::Args, _: ActorRef<Self>) -> Result<Self, Self::Error> {
Ok(args)
}
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,3 +1,17 @@
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 diesel::{
ExpressionMethods as _, OptionalExtension, QueryDsl, SelectableHelper,
@@ -10,18 +24,6 @@ use kameo_actors::message_bus::{MessageBus, Publish};
use strum::{EnumDiscriminants, IntoDiscriminant};
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 {
#[derive(Clone, Copy)]
@@ -213,7 +215,7 @@ impl Vault {
});
info!("Vault bootstrapped successfully");
self.events.tell(Publish(events::Bootstrapped)).await;
let _ = self.events.tell(Publish(events::Bootstrapped)).await;
Ok(())
}
@@ -269,7 +271,7 @@ impl Vault {
});
info!("Vault unsealed successfully");
self.events.tell(Publish(events::Unsealed)).await;
let _ = self.events.tell(Publish(events::Unsealed)).await;
Ok(())
}
@@ -399,7 +401,7 @@ impl Vault {
self.state = State::Sealed {
root_key_history_id: *root_key_history_id,
};
self.events.tell(Publish(events::VaultResealed)).await;
let _ = self.events.tell(Publish(events::VaultResealed)).await;
Ok(())
}
}

View File

@@ -1,13 +1,12 @@
use std::sync::Arc;
use thiserror::Error;
use crate::{
actors::GlobalActors,
context::tls::TlsManager,
db::{self},
};
use std::sync::Arc;
use thiserror::Error;
pub mod tls;
#[derive(Error, Debug)]

View File

@@ -1,17 +1,3 @@
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::{
self,
models::{NewTlsHistory, TlsHistory},
@@ -21,6 +7,18 @@ 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 = {
let line_ending = match cfg!(target_family = "windows") {
true => pem::LineEnding::CRLF,

View File

@@ -1,5 +1,4 @@
use argon2::password_hash::Salt as ArgonSalt;
use rand::{
Rng as _, SeedableRng,
rngs::{StdRng, SysRng},
@@ -63,7 +62,7 @@ mod tests {
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
#[test]
pub fn derive_seal_key_deterministic() {
fn derive_seal_key_deterministic() {
static PASSWORD: &[u8] = b"password";
let password = SafeCell::new(PASSWORD.to_vec());
let password2 = SafeCell::new(PASSWORD.to_vec());
@@ -79,7 +78,7 @@ mod tests {
}
#[test]
pub fn successful_derive() {
fn successful_derive() {
static PASSWORD: &[u8] = b"password";
let password = SafeCell::new(PASSWORD.to_vec());
let salt = generate_salt();
@@ -93,7 +92,7 @@ mod tests {
#[test]
// We should fuzz this
pub fn test_nonce_increment() {
fn test_nonce_increment() {
let mut nonce = Nonce([0u8; NONCE_LENGTH]);
nonce.increment();

View File

@@ -1,25 +1,18 @@
use crate::{
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},
actors::vault::{self, GetState, SignIntegrity, Vault, VerifyIntegrity},
db::{
self,
models::{IntegrityEnvelope, NewIntegrityEnvelope},
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)]
pub enum Error {
@@ -194,9 +187,7 @@ pub async fn verify_entity<E: Integrable>(
Ok(false) => Err(Error::MacMismatch {
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),
}
}
@@ -212,8 +203,6 @@ mod tests {
use diesel_async::RunQueryDsl;
use kameo::{actor::ActorRef, prelude::Spawn};
use sha2::Digest;
use crate::{
actors::{
GlobalActors,
@@ -223,21 +212,12 @@ mod tests {
};
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
use super::hashing::Hashable;
use super::{Error, Integrable, sign_entity, verify_entity};
#[derive(Clone)]
#[derive(Clone, arbiter_macros::Hashable)]
struct DummyEntity {
payload_version: i32,
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 {
const KIND: &'static str = "dummy_entity";
}

View File

@@ -1,4 +1,5 @@
use std::ops::Deref as _;
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
use encryption::v1::{Nonce, Salt};
use argon2::{Algorithm, Argon2};
use chacha20poly1305::{
@@ -9,14 +10,11 @@ use rand::{
Rng as _, SeedableRng as _,
rngs::{StdRng, SysRng},
};
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
use std::ops::Deref as _;
pub mod encryption;
pub mod integrity;
use encryption::v1::{Nonce, Salt};
pub struct KeyCell(pub SafeCell<Key>);
impl From<SafeCell<Key>> for KeyCell {
fn from(value: SafeCell<Key>) -> Self {
@@ -144,7 +142,7 @@ mod tests {
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
#[test]
pub fn encrypt_decrypt() {
fn encrypt_decrypt() {
static PASSWORD: &[u8] = b"password";
let password = SafeCell::new(PASSWORD.to_vec());
let salt = generate_salt();

View File

@@ -5,7 +5,6 @@ use diesel_async::{
sync_connection_wrapper::SyncConnectionWrapper,
};
use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations};
use thiserror::Error;
use tracing::info;

View File

@@ -1,12 +1,12 @@
#![allow(unused)]
#![allow(clippy::all)]
use crate::db::schema::{
self, aead_encrypted, arbiter_settings, evm_basic_grant, evm_ether_transfer_grant,
evm_ether_transfer_grant_target, evm_ether_transfer_limit, evm_token_transfer_grant,
evm_token_transfer_log, evm_token_transfer_volume_limit, evm_transaction_log, evm_wallet,
integrity_envelope, root_key_history, tls_history,
};
use chrono::{DateTime, Utc};
use diesel::{prelude::*, sqlite::Sqlite};
use restructed::Models;
@@ -195,7 +195,6 @@ pub struct ProgramClientMetadataHistory {
#[diesel(table_name = schema::program_client, check_for_backend(Sqlite))]
pub struct ProgramClient {
pub id: i32,
pub nonce: i32,
pub public_key: Vec<u8>,
pub metadata_id: i32,
pub created_at: SqliteTimestamp,
@@ -206,7 +205,6 @@ pub struct ProgramClient {
#[diesel(table_name = schema::useragent_client, check_for_backend(Sqlite))]
pub struct UseragentClient {
pub id: i32,
pub nonce: i32,
pub public_key: Vec<u8>,
pub created_at: SqliteTimestamp,
pub updated_at: SqliteTimestamp,

View File

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

View File

@@ -1,15 +1,3 @@
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::{
actors::vault::Vault,
crypto::integrity,
@@ -27,6 +15,18 @@ 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;
mod utils;

View File

@@ -1,4 +1,8 @@
use std::fmt::Display;
use crate::{
crypto::integrity::v1::Integrable,
db::models::{self, EvmBasicGrant, EvmWalletAccess},
evm::utils,
};
use alloy::primitives::{Address, Bytes, ChainId, U256};
use chrono::{DateTime, Duration, Utc};
@@ -6,15 +10,9 @@ use diesel::{
ExpressionMethods as _, QueryDsl, SelectableHelper, result::QueryResult, sqlite::Sqlite,
};
use diesel_async::{AsyncConnection, RunQueryDsl};
use std::fmt::Display;
use thiserror::Error;
use crate::{
crypto::integrity::v1::Integrable,
db::models::{self, EvmBasicGrant, EvmWalletAccess},
evm::utils,
};
pub mod ether_transfer;
pub mod token_transfers;
@@ -127,19 +125,19 @@ pub enum SpecificMeaning {
TokenTransfer(token_transfers::Meaning),
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, arbiter_macros::Hashable)]
pub struct TransactionRateLimit {
pub count: u32,
pub window: Duration,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, arbiter_macros::Hashable)]
pub struct VolumeRateLimit {
pub max_volume: U256,
pub window: Duration,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[derive(Clone, Debug, PartialEq, Eq, Hash, arbiter_macros::Hashable)]
pub struct SharedGrantSettings {
pub wallet_access_id: i32,
pub chain: ChainId,
@@ -200,7 +198,7 @@ pub enum SpecificGrant {
TokenTransfer(token_transfers::Settings),
}
#[derive(Debug)]
#[derive(Debug, arbiter_macros::Hashable)]
pub struct CombinedSettings<PolicyGrant> {
pub shared: SharedGrantSettings,
pub specific: PolicyGrant,
@@ -219,38 +217,3 @@ impl<P: Integrable> Integrable for CombinedSettings<P> {
const KIND: &'static str = P::KIND;
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,30 +1,33 @@
use std::collections::HashMap;
use std::fmt::Display;
use alloy::primitives::{Address, U256};
use chrono::{DateTime, Duration, Utc};
use diesel::dsl::{auto_type, insert_into};
use diesel::sqlite::Sqlite;
use diesel::{ExpressionMethods, JoinOnDsl, prelude::*};
use diesel_async::{AsyncConnection, RunQueryDsl};
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 super::{DatabaseID, EvalContext, EvalViolation};
use crate::{
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::{self, 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 chrono::{DateTime, Duration, Utc};
use diesel::{
ExpressionMethods, JoinOnDsl,
dsl::{auto_type, insert_into},
prelude::*,
sqlite::Sqlite,
};
use diesel_async::{AsyncConnection, RunQueryDsl};
use std::{collections::HashMap, fmt::Display};
#[auto_type]
fn grant_join() -> _ {
evm_ether_transfer_grant::table.inner_join(
@@ -32,8 +35,6 @@ fn grant_join() -> _ {
)
}
use super::{DatabaseID, EvalContext, EvalViolation};
// Plain ether transfer
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Meaning {
@@ -52,7 +53,7 @@ impl From<Meaning> for SpecificMeaning {
}
// A grant for ether transfers, which can be scoped to specific target addresses and volume limits
#[derive(Debug, Clone)]
#[derive(Debug, Clone, arbiter_macros::Hashable)]
pub struct Settings {
pub target: Vec<Address>,
pub limit: VolumeRateLimit,
@@ -61,15 +62,6 @@ impl Integrable for Settings {
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 {
fn from(val: Settings) -> SpecificGrant {
SpecificGrant::EtherTransfer(val)

View File

@@ -1,25 +1,26 @@
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 chrono::{Duration, Utc};
use diesel::{SelectableHelper, insert_into};
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 CHAIN_ID: u64 = 1;
@@ -340,7 +341,7 @@ proptest::proptest! {
) {
use rand::{SeedableRng, seq::SliceRandom};
use sha2::Digest;
use crate::crypto::integrity::hashing::Hashable;
use arbiter_crypto::hashing::Hashable;
let addrs: Vec<Address> = raw_addrs.iter().map(|b| Address::from(*b)).collect();
let mut shuffled = addrs.clone();

View File

@@ -1,16 +1,4 @@
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 super::{DatabaseID, EvalContext, EvalViolation};
use crate::{
crypto::integrity::Integrable,
db::models::{
@@ -18,20 +6,34 @@ use crate::{
NewEvmTokenTransferGrant, NewEvmTokenTransferLog, NewEvmTokenTransferVolumeLimit,
SqliteTimestamp,
},
db::schema::{
evm_basic_grant, evm_token_transfer_grant, evm_token_transfer_log,
evm_token_transfer_volume_limit,
},
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::{
primitives::{Address, U256},
sol_types::SolCall,
};
use arbiter_tokens_registry::evm::nonfungible::{self, TokenInfo};
use chrono::{DateTime, Duration, Utc};
use diesel::dsl::{auto_type, insert_into};
use diesel::sqlite::Sqlite;
use diesel::{ExpressionMethods, prelude::*};
use diesel::{
ExpressionMethods,
dsl::{auto_type, insert_into},
prelude::*,
sqlite::Sqlite,
};
use diesel_async::{AsyncConnection, RunQueryDsl};
use super::{DatabaseID, EvalContext, EvalViolation};
use std::collections::HashMap;
#[auto_type]
fn grant_join() -> _ {
@@ -62,7 +64,7 @@ impl From<Meaning> for SpecificMeaning {
}
// A grant for token transfers, which can be scoped to specific target addresses and volume limits
#[derive(Debug, Clone)]
#[derive(Debug, Clone, arbiter_macros::Hashable)]
pub struct Settings {
pub token_contract: Address,
pub target: Option<Address>,
@@ -72,16 +74,6 @@ impl Integrable for Settings {
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 {
fn from(val: Settings) -> SpecificGrant {
SpecificGrant::TokenTransfer(val)

View File

@@ -1,25 +1,28 @@
use alloy::primitives::{Address, Bytes, U256, address};
use alloy::sol_types::SolCall;
use super::{Settings, TokenTransfer};
use crate::{
db::{
self, DatabaseConnection,
models::{EvmBasicGrant, EvmWalletAccess, NewEvmBasicGrant, SqliteTimestamp},
schema::evm_basic_grant,
},
evm::{
abi::IERC20::transferCall,
policies::{
CombinedSettings, EvalContext, EvalViolation, Grant, Policy, SharedGrantSettings,
VolumeRateLimit,
},
utils,
},
};
use alloy::{
primitives::{Address, Bytes, U256, address},
sol_types::SolCall,
};
use chrono::{Duration, Utc};
use diesel::{SelectableHelper, insert_into};
use diesel_async::RunQueryDsl;
use crate::db::{
self, DatabaseConnection,
models::{EvmBasicGrant, EvmWalletAccess, NewEvmBasicGrant, SqliteTimestamp},
schema::evm_basic_grant,
};
use crate::evm::{
abi::IERC20::transferCall,
policies::{
CombinedSettings, EvalContext, EvalViolation, Grant, Policy, SharedGrantSettings,
VolumeRateLimit,
},
utils,
};
use super::{Settings, TokenTransfer};
// DAI on Ethereum mainnet — present in the static token registry
const CHAIN_ID: u64 = 1;
const DAI: Address = address!("6B175474E89094C44Da98b954EedeAC495271d0F");
@@ -419,7 +422,7 @@ proptest::proptest! {
) {
use rand::{SeedableRng, seq::SliceRandom};
use sha2::Digest;
use crate::crypto::integrity::hashing::Hashable;
use arbiter_crypto::hashing::Hashable;
let limits: Vec<VolumeRateLimit> = raw_limits
.iter()

View File

@@ -1,4 +1,4 @@
use std::sync::Mutex;
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
use alloy::{
consensus::SignableTransaction,
@@ -6,9 +6,9 @@ use alloy::{
primitives::{Address, B256, ChainId, Signature},
signers::{Error, Result, Signer, SignerSync, utils::secret_key_to_address},
};
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
use async_trait::async_trait;
use k256::ecdsa::{self, RecoveryId, SigningKey, signature::hazmat::PrehashSigner};
use std::sync::Mutex;
/// An Ethereum signer that stores its secp256k1 secret key inside a
/// hardware-protected [`MemSafe`] cell.

View File

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

View File

@@ -1,3 +1,7 @@
use crate::{
grpc::request_tracker::RequestTracker,
peers::client::{ClientConnection, session::ClientSession},
};
use arbiter_proto::{
proto::client::{
ClientRequest, ClientResponse, client_request::Payload as ClientRequestPayload,
@@ -5,15 +9,11 @@ use arbiter_proto::{
},
transport::{Receiver, Sender, grpc::GrpcBi},
};
use kameo::actor::{ActorRef, Spawn as _};
use tonic::Status;
use tracing::{info, warn};
use crate::{
grpc::request_tracker::RequestTracker,
peers::client::{ClientConnection, session::ClientSession},
};
mod auth;
mod evm;
mod inbound;

View File

@@ -1,3 +1,7 @@
use crate::{
grpc::request_tracker::RequestTracker,
peers::client::{self, ClientConnection, auth},
};
use arbiter_crypto::authn;
use arbiter_proto::{
ClientMetadata,
@@ -17,22 +21,18 @@ use arbiter_proto::{
},
transport::{Bi, Error as TransportError, Receiver, Sender, grpc::GrpcBi},
};
use async_trait::async_trait;
use tonic::Status;
use tracing::warn;
use crate::{
grpc::request_tracker::RequestTracker,
peers::client::{self, ClientConnection, auth},
};
pub struct AuthTransportAdapter<'a> {
pub(super) struct AuthTransportAdapter<'a> {
bi: &'a mut GrpcBi<ClientRequest, ClientResponse>,
request_tracker: &'a mut RequestTracker,
}
impl<'a> AuthTransportAdapter<'a> {
pub fn new(
pub(super) fn new(
bi: &'a mut GrpcBi<ClientRequest, ClientResponse>,
request_tracker: &'a mut RequestTracker,
) -> Self {
@@ -44,10 +44,14 @@ impl<'a> AuthTransportAdapter<'a> {
fn response_to_proto(response: auth::Outbound) -> AuthResponsePayload {
match response {
auth::Outbound::AuthChallenge { pubkey, nonce } => {
auth::Outbound::AuthChallenge { challenge } => {
AuthResponsePayload::Challenge(ProtoAuthChallenge {
pubkey: pubkey.to_bytes(),
nonce,
timestamp_nanos: challenge
.timestamp
.timestamp_nanos_opt()
.expect("timestamp within range")
as u64,
random: challenge.nonce.to_vec(),
})
}
auth::Outbound::AuthSuccess => {
@@ -193,7 +197,7 @@ fn client_metadata_from_proto(metadata: ProtoClientInfo) -> ClientMetadata {
}
}
pub async fn start(
pub(super) async fn start(
conn: &mut ClientConnection,
bi: &mut GrpcBi<ClientRequest, ClientResponse>,
request_tracker: &mut RequestTracker,

View File

@@ -1,3 +1,10 @@
use crate::{
grpc::{
Convert, TryConvert,
common::inbound::{RawEvmAddress, RawEvmTransaction},
},
peers::client::session::{ClientSession, HandleSignTransaction, SignTransactionRpcError},
};
use arbiter_proto::proto::{
client::{
client_response::Payload as ClientResponsePayload,
@@ -11,18 +18,11 @@ use arbiter_proto::proto::{
evm_sign_transaction_response::Result as EvmSignTransactionResult,
},
};
use kameo::actor::ActorRef;
use tonic::Status;
use tracing::warn;
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 {
payload: Some(payload),

View File

@@ -1,3 +1,7 @@
use crate::{
actors::vault::VaultState,
peers::client::session::{ClientSession, Error, HandleQueryVaultState},
};
use arbiter_proto::proto::{
client::{
client_response::Payload as ClientResponsePayload,
@@ -8,15 +12,11 @@ use arbiter_proto::proto::{
},
shared::VaultState as ProtoVaultState,
};
use kameo::{actor::ActorRef, error::SendError};
use tonic::Status;
use tracing::warn;
use crate::{
actors::vault::VaultState,
peers::client::session::{ClientSession, Error, HandleQueryVaultState},
};
pub(super) async fn dispatch(
actor: &ActorRef<ClientSession>,
req: proto_vault::Request,

View File

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

View File

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

View File

@@ -1,4 +1,10 @@
use alloy::primitives::U256;
use crate::{
evm::{
PolicyError, VetError,
policies::{EvalViolation, SpecificMeaning},
},
grpc::Convert,
};
use arbiter_proto::proto::{
evm::{
EvmError as ProtoEvmError,
@@ -14,13 +20,7 @@ use arbiter_proto::proto::{
},
};
use crate::{
evm::{
PolicyError, VetError,
policies::{EvalViolation, SpecificMeaning},
},
grpc::Convert,
};
use alloy::primitives::U256;
fn u256_to_proto_bytes(value: U256) -> Vec<u8> {
value.to_be_bytes::<32>().to_vec()

View File

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

View File

@@ -1,12 +1,12 @@
use tonic::Status;
#[derive(Default)]
pub struct RequestTracker {
pub(super) struct RequestTracker {
next_request_id: i32,
}
impl RequestTracker {
pub fn request(&mut self, id: i32) -> Result<i32, Status> {
pub(super) fn request(&mut self, id: i32) -> Result<i32, Status> {
if id < self.next_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.
// -1 offset is needed because request() increments the next_request_id after returning the current request id.
pub fn current_request_id(&self) -> i32 {
pub(super) fn current_request_id(&self) -> i32 {
self.next_request_id - 1
}
}

View File

@@ -1,5 +1,7 @@
use tokio::sync::{mpsc, oneshot};
use crate::{
grpc::request_tracker::RequestTracker,
peers::user_agent::{OutOfBand, UserAgentConnection, UserAgentSession},
};
use arbiter_proto::{
proto::user_agent::{
UserAgentRequest, UserAgentResponse,
@@ -8,20 +10,13 @@ use arbiter_proto::{
},
transport::{Error as TransportError, Receiver, Sender, grpc::GrpcBi},
};
use async_trait::async_trait;
use kameo::actor::{ActorRef, Spawn as _};
use kameo::actor::ActorRef;
use tokio::sync::mpsc;
use tonic::Status;
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 evm;
mod inbound;
@@ -129,115 +124,22 @@ pub async fn start(
) {
let mut request_tracker = RequestTracker::default();
let auth_creds = match auth::start(&mut conn, &mut bi, &mut request_tracker).await {
Ok(creds) => creds,
Err(e) => {
warn!(error = ?e, "Authentication failed");
return;
}
};
info!(pubkey = ?auth_creds.creds.pubkey, "User authenticated successfully");
let creds = if integrity::is_signing_available(&conn.actors.vault)
.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;
}
}
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) => {
warn!(error = ?e, "User agent connection failed");
return;
}
}
}
};
info!("User agent session established");
dispatch_loop(bi, actor.clone(), oob_receiver, request_tracker).await;
actor.kill();
}

View File

@@ -1,3 +1,4 @@
use crate::{grpc::request_tracker::RequestTracker, peers::user_agent::auth};
use arbiter_crypto::authn;
use arbiter_proto::{
proto::user_agent::{
@@ -13,22 +14,18 @@ use arbiter_proto::{
},
transport::{Bi, Error as TransportError, Receiver, Sender, grpc::GrpcBi},
};
use async_trait::async_trait;
use tonic::Status;
use tracing::warn;
use crate::{
grpc::request_tracker::RequestTracker,
peers::user_agent::{AuthCredentials, UserAgentConnection, auth},
};
pub struct AuthTransportAdapter<'a> {
pub(super) struct AuthTransportAdapter<'a> {
pub(super) bi: &'a mut GrpcBi<UserAgentRequest, UserAgentResponse>,
pub(super) request_tracker: &'a mut RequestTracker,
}
impl<'a> AuthTransportAdapter<'a> {
pub fn new(
pub(super) fn new(
bi: &'a mut GrpcBi<UserAgentRequest, UserAgentResponse>,
request_tracker: &'a mut RequestTracker,
) -> Self {
@@ -77,8 +74,15 @@ impl Sender<Result<auth::Outbound, auth::Error>> for AuthTransportAdapter<'_> {
) -> Result<(), TransportError> {
use auth::{Error, Outbound};
let payload = match item {
Ok(Outbound::AuthChallenge { nonce }) => {
AuthResponsePayload::Challenge(ProtoAuthChallenge { nonce })
Ok(Outbound::AuthChallenge { challenge }) => {
AuthResponsePayload::Challenge(ProtoAuthChallenge {
timestamp_nanos: challenge
.timestamp
.timestamp_nanos_opt()
.expect("timestamp within range")
as u64,
random: challenge.nonce.to_vec(),
})
}
Ok(Outbound::AuthSuccess) => {
AuthResponsePayload::Result(ProtoAuthResult::Success.into())
@@ -178,12 +182,3 @@ impl Receiver<auth::Inbound> 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,3 +1,17 @@
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::{
evm::{
EvmError as ProtoEvmError, EvmGrantCreateRequest, EvmGrantCreateResponse,
@@ -18,25 +32,11 @@ use arbiter_proto::proto::{
user_agent_response::Payload as UserAgentResponsePayload,
},
};
use kameo::actor::ActorRef;
use tonic::Status;
use tracing::warn;
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 {
payload: Some(payload),

View File

@@ -1,26 +1,26 @@
use alloy::primitives::{Address, U256};
use arbiter_proto::proto::evm::{
EtherTransferSettings as ProtoEtherTransferSettings, SharedSettings as ProtoSharedSettings,
SpecificGrant as ProtoSpecificGrant, TokenTransferSettings as ProtoTokenTransferSettings,
TransactionRateLimit as ProtoTransactionRateLimit, VolumeRateLimit as ProtoVolumeRateLimit,
specific_grant::Grant as ProtoSpecificGrantType,
};
use arbiter_proto::proto::user_agent::sdk_client::{
WalletAccess, WalletAccessEntry as SdkClientWalletAccess,
};
use chrono::{DateTime, TimeZone, Utc};
use prost_types::Timestamp as ProtoTimestamp;
use tonic::Status;
use crate::db::models::{CoreEvmWalletAccess, NewEvmWalletAccess};
use crate::grpc::Convert;
use crate::{
db::models::{CoreEvmWalletAccess, NewEvmWalletAccess},
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,
SpecificGrant as ProtoSpecificGrant, TokenTransferSettings as ProtoTokenTransferSettings,
TransactionRateLimit as ProtoTransactionRateLimit, VolumeRateLimit as ProtoVolumeRateLimit,
specific_grant::Grant as ProtoSpecificGrantType,
},
proto::user_agent::sdk_client::{WalletAccess, WalletAccessEntry as SdkClientWalletAccess},
};
use alloy::primitives::{Address, U256};
use chrono::{DateTime, TimeZone, Utc};
use prost_types::Timestamp as ProtoTimestamp;
use tonic::Status;
fn address_from_bytes(bytes: Vec<u8>) -> Result<Address, Status> {
if bytes.len() != 20 {

View File

@@ -1,3 +1,8 @@
use crate::{
db::models::EvmWalletAccess,
evm::policies::{SharedGrantSettings, SpecificGrant, TransactionRateLimit, VolumeRateLimit},
grpc::Convert,
};
use arbiter_proto::proto::{
evm::{
EtherTransferSettings as ProtoEtherTransferSettings, SharedSettings as ProtoSharedSettings,
@@ -7,15 +12,10 @@ use arbiter_proto::proto::{
},
user_agent::sdk_client::{WalletAccess, WalletAccessEntry as ProtoSdkClientWalletAccess},
};
use chrono::{DateTime, Utc};
use prost_types::Timestamp as ProtoTimestamp;
use crate::{
db::models::EvmWalletAccess,
evm::policies::{SharedGrantSettings, SpecificGrant, TransactionRateLimit, VolumeRateLimit},
grpc::Convert,
};
impl Convert for DateTime<Utc> {
type Output = ProtoTimestamp;

View File

@@ -1,3 +1,14 @@
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_proto::proto::{
shared::ClientInfo as ProtoClientMetadata,
@@ -16,22 +27,11 @@ use arbiter_proto::proto::{
user_agent_response::Payload as UserAgentResponsePayload,
},
};
use kameo::actor::ActorRef;
use tonic::Status;
use tracing::{info, warn};
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 {
payload: Some(payload),

View File

@@ -1,16 +1,21 @@
use arbiter_proto::proto::shared::VaultState as ProtoVaultState;
use arbiter_proto::proto::user_agent::{
user_agent_response::Payload as UserAgentResponsePayload,
vault::{self as proto_vault, request::Payload as VaultRequestPayload, response::Payload as VaultResponsePayload},
};
use kameo::actor::ActorRef;
use tonic::Status;
use tracing::warn;
use crate::{
actors::vault::VaultState,
peers::user_agent::{UserAgentSession, session::handlers::HandleQueryVaultState},
};
use arbiter_proto::{
proto::shared::VaultState as ProtoVaultState,
proto::user_agent::{
user_agent_response::Payload as UserAgentResponsePayload,
vault::{
self as proto_vault, request::Payload as VaultRequestPayload,
response::Payload as VaultResponsePayload,
},
},
};
use kameo::actor::ActorRef;
use tonic::Status;
use tracing::warn;
fn wrap_vault_response(payload: VaultResponsePayload) -> UserAgentResponsePayload {
UserAgentResponsePayload::Vault(proto_vault::Response {

View File

@@ -1,57 +1,16 @@
use arbiter_proto::{
proto::user_agent::{
user_agent_request::Payload as UserAgentRequestPayload,
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 super::auth::AuthTransportAdapter;
use crate::{
grpc::TryConvert,
peers::user_agent::vault_gate::{self as vault_gate},
};
use arbiter_proto::transport::{Bi, Error as TransportError, Receiver, Sender};
use async_trait::async_trait;
use tonic::Status;
use tracing::warn;
use super::auth::AuthTransportAdapter;
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
}
}
mod inbound;
mod outbound;
#[async_trait]
impl Receiver<vault_gate::Inbound> for AuthTransportAdapter<'_> {
@@ -60,7 +19,10 @@ impl Receiver<vault_gate::Inbound> for AuthTransportAdapter<'_> {
let request = match self.bi_mut().recv().await? {
Ok(request) => request,
Err(error) => {
warn!(?error, "Failed to receive user agent request during vault gate");
warn!(
?error,
"Failed to receive user agent request during vault gate"
);
return None;
}
};
@@ -78,86 +40,12 @@ impl Receiver<vault_gate::Inbound> for AuthTransportAdapter<'_> {
return None;
};
let vault_req = match payload {
UserAgentRequestPayload::Vault(req) => req,
_ => {
let _ = self
.bi_mut()
.send(Err(Status::permission_denied(
"Only vault operations are permitted before unsealing",
)))
.await;
match payload.try_convert() {
Ok(inbound) => return Some(inbound),
Err(status) => {
let _ = self.bi_mut().send(Err(status)).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,
},
));
}
}
}
}
@@ -180,55 +68,10 @@ impl Sender<Result<vault_gate::Outbound, vault_gate::Error>> for AuthTransportAd
}
};
let payload = match outbound {
vault_gate::Outbound::HandleHandshake(Ok(response)) => {
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
match outbound.try_convert() {
Ok(payload) => self.send_response_payload(payload).await,
Err(status) => self.bi_mut().send(Err(status)).await,
}
}
}

View File

@@ -0,0 +1,129 @@
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 {
UserAgentRequestPayload::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 {
VaultRequestPayload::QueryState(_) => Ok(vault_gate::Inbound::HandleVaultState),
VaultRequestPayload::Unseal(req) => req.try_convert(),
VaultRequestPayload::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 {
UnsealRequestPayload::Start(start) => start.try_convert(),
UnsealRequestPayload::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

@@ -0,0 +1,115 @@
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;
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 Convert for VaultState {
type Output = UserAgentResponsePayload;
fn convert(self) -> UserAgentResponsePayload {
let proto_state = match self {
VaultState::Unbootstrapped => ProtoVaultState::Unbootstrapped,
VaultState::Sealed => ProtoVaultState::Sealed,
VaultState::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 {
vault_gate::Outbound::HandleVaultState(result) => result
.map_err(|err| {
warn!(?err, "vault state query failed");
Status::internal("Failed to query vault state")
})
.map(VaultState::convert),
vault_gate::Outbound::HandleHandshake(result) => result
.map_err(|err| {
warn!(?err, "handshake failed");
Status::internal("Failed to start unseal flow")
})
.map(vault_gate::HandshakeResponse::convert),
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 Err(Status::internal("Failed to unseal vault"));
}
};
Ok(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 Err(Status::internal("Failed to bootstrap vault"));
}
};
Ok(wrap_bootstrap_response(proto_result))
}
}
}
}

View File

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

View File

@@ -1,17 +1,4 @@
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 super::{ClientConnection, ClientCredentials, ClientProfile};
use crate::{
actors::{
GlobalActors,
@@ -25,8 +12,20 @@ use crate::{
schema::program_client,
},
};
use arbiter_crypto::authn::{self, AuthChallenge, CLIENT_CONTEXT};
use arbiter_proto::{
ClientMetadata,
transport::{Bi, expect_message},
};
use super::{ClientConnection, ClientCredentials, ClientProfile};
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;
#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
pub enum Error {
@@ -74,19 +73,14 @@ pub enum Inbound {
#[derive(Debug, Clone)]
pub enum Outbound {
AuthChallenge {
pubkey: authn::PublicKey,
nonce: i32,
},
AuthChallenge { challenge: AuthChallenge },
AuthSuccess,
}
/// 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(
async fn get_client_id(
db: &db::DatabasePool,
pubkey: &authn::PublicKey,
) -> Result<Option<(i32, i32)>, Error> {
) -> Result<Option<i32>, Error> {
let pubkey_bytes = pubkey.to_bytes();
let mut conn = db.get().await.map_err(|e| {
error!(error = ?e, "Database pool error");
@@ -94,8 +88,8 @@ async fn get_current_nonce_and_id(
})?;
program_client::table
.filter(program_client::public_key.eq(&pubkey_bytes))
.select((program_client::id, program_client::nonce))
.first::<(i32, i32)>(&mut conn)
.select(program_client::id)
.first::<i32>(&mut conn)
.await
.optional()
.map_err(|e| {
@@ -114,7 +108,7 @@ async fn verify_integrity(
Error::DatabasePoolUnavailable
})?;
let (id, nonce) = get_current_nonce_and_id(db, pubkey).await?.ok_or_else(|| {
let id = get_client_id(db, pubkey).await?.ok_or_else(|| {
error!("Client not found during integrity verification");
Error::DatabaseOperationFailed
})?;
@@ -124,7 +118,6 @@ async fn verify_integrity(
vault,
&ClientCredentials {
pubkey: pubkey.clone(),
nonce,
},
id,
)
@@ -142,53 +135,6 @@ async fn verify_integrity(
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> {
let result = actors
.flow_coordinator
@@ -228,8 +174,6 @@ async fn insert_client(
let vault = vault.clone();
let pubkey = pubkey.clone();
Box::pin(async move {
const NONCE_START: i32 = 1;
let metadata_id = insert_into(client_metadata::table)
.values((
client_metadata::name.eq(&metadata.name),
@@ -244,7 +188,6 @@ async fn insert_client(
.values((
program_client::public_key.eq(pubkey.to_bytes()),
program_client::metadata_id.eq(metadata_id),
program_client::nonce.eq(NONCE_START),
))
.on_conflict_do_nothing()
.returning(program_client::id)
@@ -256,7 +199,6 @@ async fn insert_client(
&vault,
&ClientCredentials {
pubkey: pubkey.clone(),
nonce: NONCE_START,
},
client_id,
)
@@ -346,15 +288,14 @@ async fn sync_client_metadata(
async fn challenge_client<T>(
transport: &mut T,
pubkey: authn::PublicKey,
nonce: i32,
challenge: AuthChallenge,
) -> Result<(), Error>
where
T: Bi<Inbound, Result<Outbound, Error>> + ?Sized,
{
transport
.send(Ok(Outbound::AuthChallenge {
pubkey: pubkey.clone(),
nonce,
challenge: challenge.clone(),
}))
.await
.map_err(|e| {
@@ -372,7 +313,7 @@ where
Error::Transport
})?;
if !pubkey.verify(nonce, CLIENT_CONTEXT, &signature) {
if !pubkey.verify(&challenge, CLIENT_CONTEXT, &signature) {
error!("Challenge solution verification failed");
return Err(Error::InvalidChallengeSolution);
}
@@ -388,8 +329,8 @@ where
return Err(Error::Transport);
};
let client_id = match get_current_nonce_and_id(&props.db, &pubkey).await? {
Some((id, _)) => {
let client_id = match get_client_id(&props.db, &pubkey).await? {
Some(id) => {
verify_integrity(&props.db, &props.actors.vault, &pubkey).await?;
id
}
@@ -407,8 +348,9 @@ where
};
sync_client_metadata(&props.db, client_id, &metadata).await?;
let challenge_nonce = create_nonce(&props.db, &props.actors.vault, &pubkey).await?;
challenge_client(transport, pubkey, challenge_nonce).await?;
let challenge = AuthChallenge::generate(&mut rand::rng());
challenge_client(transport, pubkey, challenge).await?;
transport
.send(Ok(Outbound::AuthSuccess))

View File

@@ -1,37 +1,28 @@
use crate::{
actors::GlobalActors, crypto::integrity::Integrable, db, peers::client::session::ClientSession,
};
use arbiter_crypto::authn;
use arbiter_macros::Hashable;
use arbiter_proto::{ClientMetadata, transport::Bi};
use kameo::actor::Spawn;
use tracing::{error, info};
use crate::{
actors::GlobalActors,
crypto::integrity::{Integrable, hashing::Hashable},
db,
peers::client::session::ClientSession,
};
#[derive(Debug, Clone)]
pub struct ClientProfile {
pub pubkey: authn::PublicKey,
pub metadata: ClientMetadata,
}
#[derive(Hashable)]
pub struct ClientCredentials {
pub pubkey: authn::PublicKey,
pub nonce: i32,
}
impl Integrable for ClientCredentials {
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(crate) db: db::DatabasePool,
pub(crate) actors: GlobalActors,

View File

@@ -1,8 +1,4 @@
use kameo::{Actor, messages};
use tracing::error;
use alloy::{consensus::TxEip1559, primitives::Address, signers::Signature};
use super::ClientConnection;
use crate::{
actors::{
GlobalActors,
@@ -14,7 +10,9 @@ use crate::{
evm::VetError,
};
use super::ClientConnection;
use alloy::{consensus::TxEip1559, primitives::Address, signers::Signature};
use kameo::{Actor, messages};
use tracing::error;
pub struct ClientSession {
props: ClientConnection,

View File

@@ -1,11 +1,11 @@
use arbiter_crypto::authn;
use super::{Credentials, UserAgentConnection};
use arbiter_crypto::authn::{self, AuthChallenge};
use arbiter_proto::transport::Bi;
use state::*;
use tracing::error;
mod state;
use state::*;
use super::{AuthCredentials, UserAgentConnection};
#[derive(Debug, Clone)]
pub enum Inbound {
@@ -44,7 +44,7 @@ impl From<diesel::result::Error> for Error {
#[derive(Debug, Clone)]
pub enum Outbound {
AuthChallenge { nonce: i32 },
AuthChallenge { challenge: AuthChallenge },
AuthSuccess,
}
@@ -52,12 +52,11 @@ fn parse_auth_event(payload: Inbound) -> AuthEvents {
match payload {
Inbound::AuthChallengeRequest {
pubkey,
bootstrap_token: None,
} => AuthEvents::AuthRequest(ChallengeRequest { pubkey }),
Inbound::AuthChallengeRequest {
bootstrap_token,
} => AuthEvents::AuthRequest(ChallengeRequest {
pubkey,
bootstrap_token: Some(token),
} => AuthEvents::BootstrapAuthRequest(BootstrapAuthRequest { pubkey, token }),
bootstrap_token,
}),
Inbound::AuthChallengeSolution { signature } => {
AuthEvents::ReceivedSolution(ChallengeSolution {
solution: signature,
@@ -69,14 +68,13 @@ fn parse_auth_event(payload: Inbound) -> AuthEvents {
pub async fn authenticate<T>(
props: &mut UserAgentConnection,
transport: &mut T,
) -> Result<AuthCredentials, Error>
) -> Result<Credentials, Error>
where
T: Bi<Inbound, Result<Outbound, Error>> + Send + ?Sized,
{
let mut state = AuthStateMachine::new(AuthContext::new(props, transport));
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 {
return Err(Error::Transport);
};

View File

@@ -1,36 +1,32 @@
use super::super::{AuthCredentials, Credentials, UserAgentConnection};
use arbiter_crypto::authn::{self, USERAGENT_CONTEXT};
use super::{
super::{Credentials, UserAgentConnection},
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 diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl, sqlite::Sqlite, update};
use diesel_async::{AsyncConnection, RunQueryDsl};
use kameo::actor::ActorRef;
use diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl};
use diesel_async::RunQueryDsl;
use tracing::error;
use super::Error;
use crate::peers::user_agent::auth::Outbound;
use crate::{
actors::{bootstrap::ConsumeToken, vault::Vault},
crypto::integrity,
db::{DatabasePool, schema::useragent_client},
};
pub struct ChallengeRequest {
pub pubkey: authn::PublicKey,
pub(super) struct ChallengeRequest {
pub(super) pubkey: authn::PublicKey,
pub(super) bootstrap_token: Option<String>,
}
pub struct BootstrapAuthRequest {
pub pubkey: authn::PublicKey,
pub token: String,
pub(super) struct ChallengeContext {
pub(super) challenge: AuthChallenge,
pub(super) pubkey: authn::PublicKey,
pub(super) bootstrap_token: Option<String>,
}
pub struct ChallengeContext {
pub id: i32,
pub challenge_nonce: i32,
pub key: authn::PublicKey,
}
pub struct ChallengeSolution {
pub solution: Vec<u8>,
pub(super) struct ChallengeSolution {
pub(super) solution: Vec<u8>,
}
smlang::statemachine!(
@@ -38,119 +34,28 @@ smlang::statemachine!(
custom_error: true,
transitions: {
*Init + AuthRequest(ChallengeRequest) / async prepare_challenge = SentChallenge(ChallengeContext),
Init + BootstrapAuthRequest(BootstrapAuthRequest) / async verify_bootstrap_token = AuthOk(AuthCredentials),
SentChallenge(ChallengeContext) + ReceivedSolution(ChallengeSolution) / async verify_solution = AuthOk(AuthCredentials),
SentChallenge(ChallengeContext) + ReceivedSolution(ChallengeSolution) / async verify_solution = AuthOk(Credentials),
}
);
const NONCE_START: i32 = 1;
/// 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| {
async fn get_client_id(db: &DatabasePool, pubkey: &authn::PublicKey) -> Result<Option<i32>, Error> {
let mut conn = db.get().await.map_err(|e| {
error!(error = ?e, "Database pool error");
Error::internal("Database unavailable")
})?;
db_conn
.exclusive_transaction(|conn| {
Box::pin(async move {
useragent_client::table
.filter(useragent_client::public_key.eq(key.to_bytes()))
.select((useragent_client::id, useragent_client::nonce))
.first::<(i32, i32)>(conn)
.await
})
})
useragent_client::table
.filter(useragent_client::public_key.eq(pubkey.to_bytes()))
.select(useragent_client::id)
.first::<i32>(&mut conn)
.await
.optional()
.map_err(|e| {
error!(error = ?e, "Database error");
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")
})
}
async fn register_key(db: &DatabasePool, pubkey: &authn::PublicKey) -> Result<i32, Error> {
let pubkey_bytes = pubkey.to_bytes();
let mut conn = db.get().await.map_err(|e| {
@@ -159,10 +64,7 @@ async fn register_key(db: &DatabasePool, pubkey: &authn::PublicKey) -> Result<i3
})?;
let id: i32 = diesel::insert_into(useragent_client::table)
.values((
useragent_client::public_key.eq(pubkey_bytes),
useragent_client::nonce.eq(NONCE_START),
))
.values((useragent_client::public_key.eq(pubkey_bytes),))
.returning(useragent_client::id)
.get_result(&mut conn)
.await
@@ -174,13 +76,13 @@ async fn register_key(db: &DatabasePool, pubkey: &authn::PublicKey) -> Result<i3
Ok(id)
}
pub struct AuthContext<'a, T: ?Sized> {
pub(super) struct AuthContext<'a, T: ?Sized> {
pub(super) conn: &'a mut UserAgentConnection,
pub(super) transport: &'a mut T,
}
impl<'a, T: ?Sized> AuthContext<'a, T> {
pub fn new(conn: &'a mut UserAgentConnection, transport: &'a mut T) -> Self {
pub(super) fn new(conn: &'a mut UserAgentConnection, transport: &'a mut T) -> Self {
Self { conn, transport }
}
}
@@ -193,38 +95,25 @@ where
async fn prepare_challenge(
&mut self,
ChallengeRequest { pubkey }: ChallengeRequest,
ChallengeRequest {
pubkey,
bootstrap_token,
}: ChallengeRequest,
) -> Result<ChallengeContext, Self::Error> {
let is_signing = integrity::is_signing_available(&self.conn.actors.vault)
.await
.unwrap_or(false);
if is_signing {
verify_integrity(&self.conn.db, &self.conn.actors.vault, &pubkey).await?;
// Verify pubkey is registered (unless bootstrapping)
if bootstrap_token.is_none() {
let id = get_client_id(&self.conn.db, &pubkey).await?;
if id.is_none() {
return Err(Error::UnregisteredPublicKey);
}
}
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?;
let challenge = AuthChallenge::generate(&mut rand::rng());
self.transport
.send(Ok(Outbound::AuthChallenge { nonce }))
.send(Ok(Outbound::AuthChallenge {
challenge: challenge.clone(),
}))
.await
.map_err(|e| {
error!(?e, "Failed to send auth challenge");
@@ -232,98 +121,78 @@ where
})?;
Ok(ChallengeContext {
id,
challenge_nonce: nonce,
key: pubkey,
challenge,
pubkey,
bootstrap_token,
})
}
#[allow(missing_docs)]
#[allow(clippy::result_unit_err)]
async fn verify_bootstrap_token(
&mut self,
BootstrapAuthRequest { pubkey, token }: BootstrapAuthRequest,
) -> Result<AuthCredentials, Self::Error> {
let token_ok: bool = self
.conn
.actors
.bootstrapper
.ask(ConsumeToken {
token: token.clone(),
})
.await
.map_err(|e| {
error!(?e, "Failed to consume bootstrap token");
Error::internal("Failed to consume bootstrap token")
})?;
if !token_ok {
error!("Invalid bootstrap token provided");
return Err(Error::InvalidBootstrapToken);
}
match token_ok {
true => {
let id = register_key(&self.conn.db, &pubkey).await?;
self.transport
.send(Ok(Outbound::AuthSuccess))
.await
.map_err(|_| Error::Transport)?;
Ok(AuthCredentials {
creds: Credentials { id, pubkey },
new_nonce: NONCE_START,
})
}
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,
challenge,
pubkey,
bootstrap_token,
}: &ChallengeContext,
ChallengeSolution { solution }: ChallengeSolution,
) -> Result<AuthCredentials, Self::Error> {
) -> 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 = key.verify(*challenge_nonce, USERAGENT_CONTEXT, &signature);
let valid = pubkey.verify(challenge, 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)
}
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
.conn
.actors
.bootstrapper
.ask(ConsumeToken {
token: token.clone(),
})
.await
.map_err(|e| {
error!(?e, "Failed to consume bootstrap token");
Error::internal("Failed to consume bootstrap token")
})?;
if !token_ok {
error!("Invalid bootstrap token provided");
self.transport
.send(Err(Error::InvalidBootstrapToken))
.await
.map_err(|_| Error::Transport)?;
return Err(Error::InvalidBootstrapToken);
}
register_key(&self.conn.db, pubkey).await?
}
None => get_client_id(&self.conn.db, pubkey)
.await?
.ok_or(Error::UnregisteredPublicKey)?,
};
self.transport
.send(Ok(Outbound::AuthSuccess))
.await
.map_err(|_| Error::Transport)?;
Ok(Credentials {
id,
pubkey: pubkey.clone(),
})
}
}

View File

@@ -1,58 +1,35 @@
use crate::{
actors::GlobalActors,
crypto::integrity::{self, Integrable},
db,
actors::{
GlobalActors,
vault::{GetState, Vault},
},
crypto::integrity::{self, AttestationStatus, Integrable},
db::{self, DatabaseError, DatabasePool},
peers::client::ClientProfile,
};
use arbiter_crypto::authn;
use arbiter_macros::Hashable;
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 crate::crypto::integrity::hashing::Hashable;
use kameo::actor::{ActorRef, Spawn as _};
use tokio::sync::oneshot;
use tracing::{error, warn};
pub use auth::authenticate;
pub use session::UserAgentSession;
pub mod auth;
pub mod session;
pub mod vault_gate;
#[derive(Debug, Clone, Hash)]
#[derive(Debug, Clone, Hashable)]
pub struct Credentials {
pub id: i32,
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);
}
}
#[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 {
impl Integrable for Credentials {
const KIND: &'static str = "useragent_credentials";
}
@@ -83,6 +60,8 @@ pub enum Error {
VaultGate(#[from] vault_gate::Error),
#[error("transport closed unexpectedly")]
Transport,
#[error("database error: {0}")]
Database(DatabaseError),
#[error("internal: {0}")]
Internal(String),
}
@@ -93,38 +72,44 @@ impl From<auth::Error> for Error {
}
}
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 auth_creds = authenticate(props, &mut transport).await?;
let creds = if integrity::is_signing_available(&props.actors.vault)
async fn verify_integrity(
db: &DatabasePool,
vault: &ActorRef<Vault>,
credentials: &Credentials,
) -> Result<(), Error> {
let mut conn = db
.get()
.await
.unwrap_or(false)
{
auth_creds.creds
} else {
run_vault_gate(props, &mut transport, auth_creds).await?
};
.map_err(|_| Error::Internal("DB unavailable".into()))?;
match integrity::verify_entity(&mut conn, &vault, credentials, credentials.id).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()))
}
}
}
Ok(UserAgentSession::spawn(UserAgentSession::new(
props.clone(),
creds,
oob_sender,
)))
async fn should_run_gate(vault: &ActorRef<Vault>) -> Result<bool, Error> {
let vault_state = vault
.ask(GetState {})
.await
.map_err(|_| Error::Internal("Failed to contact the vault".into()))?;
Ok(!matches!(
vault_state,
crate::actors::vault::VaultState::Unsealed
))
}
async fn run_vault_gate<T>(
props: &UserAgentConnection,
transport: &mut T,
auth_creds: AuthCredentials,
) -> Result<Credentials, Error>
auth_creds: Credentials,
) -> Result<(), Error>
where
T: Bi<vault_gate::Inbound, Result<vault_gate::Outbound, vault_gate::Error>> + Send + ?Sized,
{
@@ -173,3 +158,28 @@ where
gate.kill();
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,37 +1,21 @@
use std::sync::Mutex;
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::{
use super::{Error, UserAgentSession};
use crate::{
actors::evm::{
ClientSignTransaction, Generate, ListWallets, SignTransactionError as EvmSignError,
UseragentCreateGrant, UseragentListGrants,
},
vault::{self, Bootstrap, TryUnseal},
};
use crate::db::models::{
EvmWalletAccess, NewEvmWalletAccess, ProgramClient, ProgramClientMetadata,
};
use crate::evm::policies::{Grant, SpecificGrant};
use crate::{
actors::flow_coordinator::client_connect_approval::ClientApprovalAnswer,
actors::vault::VaultState,
db::models::{EvmWalletAccess, NewEvmWalletAccess, ProgramClient, ProgramClientMetadata},
evm::policies::{Grant, SpecificGrant},
};
use arbiter_crypto::authn;
use super::{Error, UserAgentSession};
use alloy::{consensus::TxEip1559, primitives::Address, signers::Signature};
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)]
pub enum SignTransactionError {

View File

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

View File

@@ -1,4 +1,15 @@
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 state::*;
use chacha20poly1305::{AeadInPlace, KeyInit as _, XChaCha20Poly1305, XNonce};
use kameo::{Actor, error::SendError, messages, prelude::Message};
use kameo_actors::message_bus::Register;
@@ -7,17 +18,6 @@ use tracing::{error, info};
use x25519_dalek::{EphemeralSecret, PublicKey, SharedSecret};
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)]
pub enum Error {
@@ -43,8 +43,8 @@ pub struct HandshakeResponse {
}
pub struct VaultGate {
pub auth_creds: AuthCredentials,
pub promotion_tx: Option<oneshot::Sender<Result<Credentials, Error>>>,
pub auth_creds: Credentials,
pub promotion_tx: Option<oneshot::Sender<Result<(), Error>>>,
pub state: State,
pub actors: GlobalActors,
pub db: DatabasePool,
@@ -52,10 +52,10 @@ pub struct VaultGate {
impl VaultGate {
pub fn new(
auth_creds: AuthCredentials,
auth_creds: Credentials,
actors: GlobalActors,
db: DatabasePool,
promotion_tx: oneshot::Sender<Result<Credentials, Error>>,
promotion_tx: oneshot::Sender<Result<(), Error>>,
) -> Self {
Self {
auth_creds,
@@ -228,6 +228,18 @@ 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 {
@@ -239,14 +251,23 @@ impl Message<events::Bootstrapped> for VaultGate {
ctx: &mut kameo::prelude::Context<Self, Self::Reply>,
) -> Self::Reply {
let result = async {
let mut conn = self.db.get().await.map_err(|_| Error::internal("DB unavailable"))?;
integrity::sign_entity(&mut conn, &self.actors.vault, &self.auth_creds, self.auth_creds.creds.id)
let mut conn = self
.db
.get()
.await
.map_err(|e| {
error!(?e, "Failed to sign integrity envelope on bootstrap");
Error::internal("Integrity sign failed")
})?;
Ok(self.auth_creds.creds.clone())
.map_err(|_| Error::internal("DB unavailable"))?;
integrity::sign_entity(
&mut conn,
&self.actors.vault,
&self.auth_creds,
self.auth_creds.id,
)
.await
.map_err(|e| {
error!(?e, "Failed to sign integrity envelope on bootstrap");
Error::internal("Integrity sign failed")
})?;
Ok(())
}
.await;
@@ -265,30 +286,8 @@ impl Message<events::Unsealed> for VaultGate {
_: events::Unsealed,
ctx: &mut kameo::prelude::Context<Self, 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() {
let _ = tx.send(result);
let _ = tx.send(Ok(()));
}
ctx.stop();
}

View File

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

View File

@@ -1,21 +1,23 @@
use super::common::ChannelTransport;
use arbiter_crypto::{
authn::{self, CLIENT_CONTEXT, format_challenge},
authn::{self, AuthChallenge, CLIENT_CONTEXT},
safecell::{SafeCell, SafeCellHandle as _},
};
use arbiter_proto::ClientMetadata;
use arbiter_proto::transport::{Receiver, Sender};
use arbiter_proto::{
ClientMetadata,
transport::{Receiver, Sender},
};
use arbiter_server::{
actors::{GlobalActors, vault::Bootstrap},
crypto::integrity,
db::{self, schema},
peers::client::{ClientConnection, ClientCredentials, auth, connect_client},
};
use diesel::{ExpressionMethods as _, NullableExpressionMethods as _, QueryDsl as _, insert_into};
use diesel_async::RunQueryDsl;
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 {
ClientMetadata {
name: name.to_owned(),
@@ -58,7 +60,6 @@ async fn insert_registered_client(
&actors.vault,
&ClientCredentials {
pubkey: pubkey.into(),
nonce: 1,
},
client_id,
)
@@ -66,12 +67,8 @@ async fn insert_registered_client(
.unwrap();
}
fn sign_client_challenge(
key: &SigningKey<MlDsa87>,
nonce: i32,
pubkey: &authn::PublicKey,
) -> authn::Signature {
let challenge = format_challenge(nonce, &pubkey.to_bytes());
fn sign_client_challenge(key: &SigningKey<MlDsa87>, challenge: &AuthChallenge) -> authn::Signature {
let challenge = challenge.format();
key.signing_key()
.sign_deterministic(&challenge, CLIENT_CONTEXT)
.unwrap()
@@ -86,10 +83,7 @@ async fn insert_bootstrap_sentinel_useragent(db: &db::DatabasePool) {
.to_vec();
insert_into(schema::useragent_client::table)
.values((
schema::useragent_client::public_key.eq(sentinel_key),
schema::useragent_client::key_type.eq(1i32),
))
.values((schema::useragent_client::public_key.eq(sentinel_key),))
.execute(&mut conn)
.await
.unwrap();
@@ -111,7 +105,7 @@ async fn spawn_test_actors(db: &db::DatabasePool) -> GlobalActors {
#[tokio::test]
#[test_log::test]
pub async fn test_unregistered_pubkey_rejected() {
async fn test_unregistered_pubkey_rejected() {
let db = db::create_test_pool().await;
let (server_transport, mut test_transport) = ChannelTransport::new();
@@ -138,7 +132,7 @@ pub async fn test_unregistered_pubkey_rejected() {
#[tokio::test]
#[test_log::test]
pub async fn test_challenge_auth() {
async fn test_challenge_auth() {
let db = db::create_test_pool().await;
let actors = spawn_test_actors(&db).await;
@@ -175,14 +169,14 @@ pub async fn test_challenge_auth() {
.expect("should receive challenge");
let challenge = match response {
Ok(resp) => match resp {
auth::Outbound::AuthChallenge { pubkey, nonce } => (pubkey, nonce),
auth::Outbound::AuthChallenge { challenge } => challenge,
other => panic!("Expected AuthChallenge, got {other:?}"),
},
Err(err) => panic!("Expected Ok response, got Err({err:?})"),
};
// Sign the challenge and send solution
let signature = sign_client_challenge(&new_key, challenge.1, &challenge.0);
let signature = sign_client_challenge(&new_key, &challenge);
test_transport
.send(auth::Inbound::AuthChallengeSolution { signature })
@@ -205,7 +199,7 @@ pub async fn test_challenge_auth() {
#[tokio::test]
#[test_log::test]
pub async fn test_metadata_unchanged_does_not_append_history() {
async fn test_metadata_unchanged_does_not_append_history() {
let db = db::create_test_pool().await;
let actors = spawn_test_actors(&db).await;
let new_key = MlDsa87::key_gen(&mut rand::rng());
@@ -230,11 +224,11 @@ pub async fn test_metadata_unchanged_does_not_append_history() {
.unwrap();
let response = test_transport.recv().await.unwrap().unwrap();
let (pubkey, nonce) = match response {
auth::Outbound::AuthChallenge { pubkey, nonce } => (pubkey, nonce),
let challenge = match response {
auth::Outbound::AuthChallenge { challenge } => challenge,
other => panic!("Expected AuthChallenge, got {other:?}"),
};
let signature = sign_client_challenge(&new_key, nonce, &pubkey);
let signature = sign_client_challenge(&new_key, &challenge);
test_transport
.send(auth::Inbound::AuthChallengeSolution { signature })
.await
@@ -262,7 +256,7 @@ pub async fn test_metadata_unchanged_does_not_append_history() {
#[tokio::test]
#[test_log::test]
pub async fn test_metadata_change_appends_history_and_repoints_binding() {
async fn test_metadata_change_appends_history_and_repoints_binding() {
let db = db::create_test_pool().await;
let actors = spawn_test_actors(&db).await;
let new_key = MlDsa87::key_gen(&mut rand::rng());
@@ -292,11 +286,11 @@ pub async fn test_metadata_change_appends_history_and_repoints_binding() {
.unwrap();
let response = test_transport.recv().await.unwrap().unwrap();
let (pubkey, nonce) = match response {
auth::Outbound::AuthChallenge { pubkey, nonce } => (pubkey, nonce),
let challenge = match response {
auth::Outbound::AuthChallenge { challenge } => challenge,
other => panic!("Expected AuthChallenge, got {other:?}"),
};
let signature = sign_client_challenge(&new_key, nonce, &pubkey);
let signature = sign_client_challenge(&new_key, &challenge);
test_transport
.send(auth::Inbound::AuthChallengeSolution { signature })
.await
@@ -349,7 +343,7 @@ pub async fn test_metadata_change_appends_history_and_repoints_binding() {
#[tokio::test]
#[test_log::test]
pub async fn test_challenge_auth_rejects_integrity_tag_mismatch() {
async fn test_challenge_auth_rejects_integrity_tag_mismatch() {
let db = db::create_test_pool().await;
let actors = spawn_test_actors(&db).await;

View File

@@ -11,7 +11,7 @@ use diesel_async::RunQueryDsl;
use tokio::sync::mpsc;
#[allow(dead_code)]
pub async fn bootstrapped_vault(db: &db::DatabasePool) -> Vault {
pub(crate) async fn bootstrapped_vault(db: &db::DatabasePool) -> Vault {
let mut actor = Vault::new(db.clone(), GlobalActors::spawn_message_bus())
.await
.unwrap();
@@ -23,7 +23,7 @@ pub async fn bootstrapped_vault(db: &db::DatabasePool) -> Vault {
}
#[allow(dead_code)]
pub async fn root_key_history_id(db: &db::DatabasePool) -> i32 {
pub(crate) async fn root_key_history_id(db: &db::DatabasePool) -> i32 {
let mut conn = db.get().await.unwrap();
let id = schema::arbiter_settings::table
.select(schema::arbiter_settings::root_key_id)
@@ -34,14 +34,14 @@ pub async fn root_key_history_id(db: &db::DatabasePool) -> i32 {
}
#[allow(dead_code)]
pub struct ChannelTransport<T, Y> {
pub(crate) struct ChannelTransport<T, Y> {
receiver: mpsc::Receiver<T>,
sender: mpsc::Sender<Y>,
}
impl<T, Y> ChannelTransport<T, Y> {
#[allow(dead_code)]
pub fn new() -> (Self, ChannelTransport<Y, T>) {
pub(crate) fn new() -> (Self, ChannelTransport<Y, T>) {
let (tx1, rx1) = mpsc::channel(10);
let (tx2, rx2) = mpsc::channel(10);
(

View File

@@ -1,36 +1,153 @@
use super::common::ChannelTransport;
use arbiter_crypto::{
authn::{self, USERAGENT_CONTEXT, format_challenge},
authn::{self, AuthChallenge, USERAGENT_CONTEXT},
safecell::{SafeCell, SafeCellHandle as _},
};
use arbiter_proto::transport::{Receiver, Sender};
use arbiter_proto::transport::{Error as TransportError, Receiver, Sender};
use arbiter_server::{
actors::{GlobalActors, bootstrap::GetToken, vault::Bootstrap},
crypto::integrity,
db::{self, schema},
peers::user_agent::{AuthCredentials, Credentials, UserAgentConnection, auth},
peers::user_agent::{self, Credentials, UserAgentConnection, auth, vault_gate},
};
use async_trait::async_trait;
use diesel::{ExpressionMethods as _, QueryDsl, insert_into};
use diesel_async::RunQueryDsl;
use ml_dsa::{KeyGen, MlDsa87, SigningKey, signature::Keypair as _};
use super::common::ChannelTransport;
use tokio::sync::mpsc;
fn sign_useragent_challenge(
key: &SigningKey<MlDsa87>,
nonce: i32,
pubkey_bytes: &[u8],
challenge: &AuthChallenge,
) -> authn::Signature {
let challenge = format_challenge(nonce, pubkey_bytes);
let challenge = challenge.format();
key.signing_key()
.sign_deterministic(&challenge, USERAGENT_CONTEXT)
.unwrap()
.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]
#[test_log::test]
pub async fn test_bootstrap_token_auth() {
async fn test_bootstrap_token_auth() {
let db = db::create_test_pool().await;
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
actors
@@ -58,14 +175,29 @@ pub async fn test_bootstrap_token_auth() {
.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
.unwrap();
let response = test_transport
.recv()
.await
.expect("should receive auth result");
match response {
Ok(auth::Outbound::AuthSuccess) => {}
other => panic!("Expected AuthSuccess, got {other:?}"),
}
assert!(matches!(response, Ok(auth::Outbound::AuthSuccess)));
task.await.unwrap().unwrap();
@@ -80,7 +212,7 @@ pub async fn test_bootstrap_token_auth() {
#[tokio::test]
#[test_log::test]
pub async fn test_bootstrap_invalid_token_auth() {
async fn test_bootstrap_invalid_token_auth() {
let db = db::create_test_pool().await;
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
@@ -100,6 +232,23 @@ pub async fn test_bootstrap_invalid_token_auth() {
.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
.unwrap();
assert!(matches!(
task.await.unwrap(),
Err(auth::Error::InvalidBootstrapToken)
@@ -116,7 +265,7 @@ pub async fn test_bootstrap_invalid_token_auth() {
#[tokio::test]
#[test_log::test]
pub async fn test_challenge_auth() {
async fn test_challenge_auth() {
let db = db::create_test_pool().await;
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
actors
@@ -133,10 +282,7 @@ pub async fn test_challenge_auth() {
{
let mut conn = db.get().await.unwrap();
let id: i32 = insert_into(schema::useragent_client::table)
.values((
schema::useragent_client::public_key.eq(pubkey_bytes.clone()),
schema::useragent_client::key_type.eq(1i32),
))
.values((schema::useragent_client::public_key.eq(pubkey_bytes.clone()),))
.returning(schema::useragent_client::id)
.get_result(&mut conn)
.await
@@ -144,12 +290,9 @@ pub async fn test_challenge_auth() {
integrity::sign_entity(
&mut conn,
&actors.vault,
&AuthCredentials {
creds: Credentials {
id,
pubkey: new_key.verifying_key().into(),
},
new_nonce: 1,
&Credentials {
id,
pubkey: new_key.verifying_key().into(),
},
id,
)
@@ -178,13 +321,13 @@ pub async fn test_challenge_auth() {
.expect("should receive challenge");
let challenge = match response {
Ok(resp) => match resp {
auth::Outbound::AuthChallenge { nonce } => nonce,
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, &pubkey_bytes);
let signature = sign_useragent_challenge(&new_key, &challenge);
test_transport
.send(auth::Inbound::AuthChallengeSolution {
@@ -207,7 +350,7 @@ pub async fn test_challenge_auth() {
#[tokio::test]
#[test_log::test]
pub async fn test_challenge_auth_rejects_integrity_tag_mismatch_when_unsealed() {
async fn test_challenge_auth_rejects_integrity_tag_mismatch_when_unsealed() {
let db = db::create_test_pool().await;
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
@@ -225,20 +368,17 @@ pub async fn test_challenge_auth_rejects_integrity_tag_mismatch_when_unsealed()
{
let mut conn = db.get().await.unwrap();
insert_into(schema::useragent_client::table)
.values((
schema::useragent_client::public_key.eq(pubkey_bytes.clone()),
schema::useragent_client::key_type.eq(1i32),
))
.values((schema::useragent_client::public_key.eq(pubkey_bytes.clone()),))
.execute(&mut conn)
.await
.unwrap();
}
let (mut server_transport, mut test_transport) = ChannelTransport::new();
let (server_transport, mut test_transport) = start_transport_pair();
let db_for_task = db.clone();
let task = tokio::spawn(async move {
let mut props = UserAgentConnection::new(db_for_task, actors);
auth::authenticate(&mut props, &mut server_transport).await
user_agent::start(&mut props, server_transport, Box::new(NullOobSender)).await
});
test_transport
@@ -249,15 +389,42 @@ pub async fn test_challenge_auth_rejects_integrity_tag_mismatch_when_unsealed()
.await
.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!(
task.await.unwrap(),
Err(auth::Error::Internal { .. })
Err(user_agent::Error::Internal(_))
));
}
#[tokio::test]
#[test_log::test]
pub async fn test_challenge_auth_rejects_invalid_signature() {
async fn test_challenge_auth_rejects_invalid_signature() {
let db = db::create_test_pool().await;
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
actors
@@ -274,10 +441,7 @@ pub async fn test_challenge_auth_rejects_invalid_signature() {
{
let mut conn = db.get().await.unwrap();
let id: i32 = insert_into(schema::useragent_client::table)
.values((
schema::useragent_client::public_key.eq(pubkey_bytes.clone()),
schema::useragent_client::key_type.eq(1i32),
))
.values((schema::useragent_client::public_key.eq(pubkey_bytes.clone()),))
.returning(schema::useragent_client::id)
.get_result(&mut conn)
.await
@@ -285,12 +449,9 @@ pub async fn test_challenge_auth_rejects_invalid_signature() {
integrity::sign_entity(
&mut conn,
&actors.vault,
&AuthCredentials {
creds: Credentials {
id,
pubkey: new_key.verifying_key().into(),
},
new_nonce: 1,
&Credentials {
id,
pubkey: new_key.verifying_key().into(),
},
id,
)
@@ -319,13 +480,13 @@ pub async fn test_challenge_auth_rejects_invalid_signature() {
.expect("should receive challenge");
let challenge = match response {
Ok(resp) => match resp {
auth::Outbound::AuthChallenge { nonce } => nonce,
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 + 1, &pubkey_bytes);
let signature = sign_useragent_challenge(&new_key, &tamper_challenge(&challenge));
test_transport
.send(auth::Inbound::AuthChallengeSolution {

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,6 +7,7 @@ import 'package:arbiter/features/identity/pk_manager.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.pb.dart';
import 'package:arbiter/src/rust/api.dart';
import 'package:grpc/grpc.dart';
import 'package:mtcore/markettakers.dart';
@@ -92,7 +93,10 @@ Future<Connection> connectAndAuthorize(
);
}
final challenge = _formatChallenge(authResponse.challenge, pubkey);
final challenge = await formatChallenge(
random: authResponse.challenge.random,
timestamp: authResponse.challenge.timestampNanos.toInt(),
);
talker.info(
'Received auth challenge, signing with key ${base64Encode(pubkey)}',
);
@@ -164,9 +168,3 @@ Future<Connection> _connect(StoredServerInfo serverInfo) async {
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,6 +12,7 @@
import 'dart:core' as $core;
import 'package:fixnum/fixnum.dart' as $fixnum;
import 'package:protobuf/protobuf.dart' as $pb;
import '../shared/client.pb.dart' as $0;
@@ -94,12 +95,12 @@ class AuthChallengeRequest extends $pb.GeneratedMessage {
class AuthChallenge extends $pb.GeneratedMessage {
factory AuthChallenge({
$core.List<$core.int>? pubkey,
$core.int? nonce,
$fixnum.Int64? timestampNanos,
$core.List<$core.int>? random,
}) {
final result = create();
if (pubkey != null) result.pubkey = pubkey;
if (nonce != null) result.nonce = nonce;
if (timestampNanos != null) result.timestampNanos = timestampNanos;
if (random != null) result.random = random;
return result;
}
@@ -117,9 +118,11 @@ class AuthChallenge extends $pb.GeneratedMessage {
package:
const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.client.auth'),
createEmptyInstance: create)
..a<$fixnum.Int64>(
1, _omitFieldNames ? '' : 'timestampNanos', $pb.PbFieldType.OU6,
defaultOrMaker: $fixnum.Int64.ZERO)
..a<$core.List<$core.int>>(
1, _omitFieldNames ? '' : 'pubkey', $pb.PbFieldType.OY)
..aI(2, _omitFieldNames ? '' : 'nonce')
2, _omitFieldNames ? '' : 'random', $pb.PbFieldType.OY)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
@@ -142,22 +145,22 @@ class AuthChallenge extends $pb.GeneratedMessage {
static AuthChallenge? _defaultInstance;
@$pb.TagNumber(1)
$core.List<$core.int> get pubkey => $_getN(0);
$fixnum.Int64 get timestampNanos => $_getI64(0);
@$pb.TagNumber(1)
set pubkey($core.List<$core.int> value) => $_setBytes(0, value);
set timestampNanos($fixnum.Int64 value) => $_setInt64(0, value);
@$pb.TagNumber(1)
$core.bool hasPubkey() => $_has(0);
$core.bool hasTimestampNanos() => $_has(0);
@$pb.TagNumber(1)
void clearPubkey() => $_clearField(1);
void clearTimestampNanos() => $_clearField(1);
@$pb.TagNumber(2)
$core.int get nonce => $_getIZ(1);
$core.List<$core.int> get random => $_getN(1);
@$pb.TagNumber(2)
set nonce($core.int value) => $_setSignedInt32(1, value);
set random($core.List<$core.int> value) => $_setBytes(1, value);
@$pb.TagNumber(2)
$core.bool hasNonce() => $_has(1);
$core.bool hasRandom() => $_has(1);
@$pb.TagNumber(2)
void clearNonce() => $_clearField(2);
void clearRandom() => $_clearField(2);
}
class AuthChallengeSolution extends $pb.GeneratedMessage {

View File

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

View File

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

View File

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

View File

@@ -6,6 +6,14 @@
import 'frb_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>>
abstract class MldsaKey implements RustOpaqueInterface {
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';
@override
int get rustContentHash => -437661335;
int get rustContentHash => 1247923898;
static const kDefaultExternalLibraryLoaderConfig =
ExternalLibraryLoaderConfig(
@@ -89,6 +89,11 @@ abstract class RustLibApi extends BaseApi {
Future<Uint8List> crateApiMldsaKeyToBytes({required MldsaKey that});
Future<Uint8List> crateApiFormatChallenge({
required List<int> random,
required PlatformInt64 timestamp,
});
RustArcIncrementStrongCountFnType
get rust_arc_increment_strong_count_MldsaKey;
@@ -267,6 +272,40 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
TaskConstMeta get kCrateApiMldsaKeyToBytesConstMeta =>
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
get rust_arc_increment_strong_count_MldsaKey => wire
.rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMldsaKey;
@@ -314,6 +353,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
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
List<int> dco_decode_list_prim_u_8_loose(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
@@ -394,6 +439,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
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
List<int> sse_decode_list_prim_u_8_loose(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
@@ -491,6 +542,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
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
void sse_encode_list_prim_u_8_loose(
List<int> self,

View File

@@ -46,6 +46,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
String dco_decode_String(dynamic raw);
@protected
PlatformInt64 dco_decode_i_64(dynamic raw);
@protected
List<int> dco_decode_list_prim_u_8_loose(dynamic raw);
@@ -85,6 +88,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
String sse_decode_String(SseDeserializer deserializer);
@protected
PlatformInt64 sse_decode_i_64(SseDeserializer deserializer);
@protected
List<int> sse_decode_list_prim_u_8_loose(SseDeserializer deserializer);
@@ -136,6 +142,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_String(String self, SseSerializer serializer);
@protected
void sse_encode_i_64(PlatformInt64 self, SseSerializer serializer);
@protected
void sse_encode_list_prim_u_8_loose(List<int> self, SseSerializer serializer);

View File

@@ -48,6 +48,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
String dco_decode_String(dynamic raw);
@protected
PlatformInt64 dco_decode_i_64(dynamic raw);
@protected
List<int> dco_decode_list_prim_u_8_loose(dynamic raw);
@@ -87,6 +90,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
String sse_decode_String(SseDeserializer deserializer);
@protected
PlatformInt64 sse_decode_i_64(SseDeserializer deserializer);
@protected
List<int> sse_decode_list_prim_u_8_loose(SseDeserializer deserializer);
@@ -138,6 +144,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_String(String self, SseSerializer serializer);
@protected
void sse_encode_i_64(PlatformInt64 self, SseSerializer serializer);
@protected
void sse_encode_list_prim_u_8_loose(List<int> self, SseSerializer serializer);

4
useragent/mise.toml Normal file
View File

@@ -0,0 +1,4 @@
[tasks.codegen]
run = '''
flutter_rust_bridge_codegen generate
'''

View File

@@ -232,10 +232,11 @@ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
name = "arbiter-crypto"
version = "0.1.0"
dependencies = [
"base64",
"chrono",
"memsafe",
"ml-dsa",
"rand",
"x-wing",
]
[[package]]
@@ -486,12 +487,6 @@ dependencies = [
"rustc-demangle",
]
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "base64ct"
version = "1.8.3"
@@ -713,10 +708,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
dependencies = [
"cfg-if",
"cpufeatures",
"cpufeatures 0.3.0",
"rand_core",
]
[[package]]
name = "chrono"
version = "0.4.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
dependencies = [
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"wasm-bindgen",
"windows-link",
]
[[package]]
name = "clipboard-win"
version = "5.4.1"
@@ -837,6 +846,15 @@ dependencies = [
"libc",
]
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]]
name = "cpufeatures"
version = "0.3.0"
@@ -884,6 +902,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710"
dependencies = [
"hybrid-array",
"rand_core",
]
[[package]]
@@ -901,6 +920,32 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f"
[[package]]
name = "curve25519-dalek"
version = "5.0.0-pre.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "335f1947f241137a14106b6f5acc5918a5ede29c9d71d3f2cb1678d5075d9fc3"
dependencies = [
"cfg-if",
"cpufeatures 0.2.17",
"curve25519-dalek-derive",
"fiat-crypto",
"rustc_version",
"subtle",
"zeroize",
]
[[package]]
name = "curve25519-dalek-derive"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "dart-sys"
version = "4.1.5"
@@ -1314,6 +1359,12 @@ dependencies = [
"bytemuck",
]
[[package]]
name = "fiat-crypto"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24"
[[package]]
name = "flate2"
version = "1.1.9"
@@ -1773,10 +1824,35 @@ version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3944cf8cf766b40e2a1a333ee5e9b563f854d5fa49d6a8ca2764e97c6eddb214"
dependencies = [
"ctutils",
"typenum",
"zeroize",
]
[[package]]
name = "iana-time-zone"
version = "0.1.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "icu_collections"
version = "2.2.0"
@@ -2005,7 +2081,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa"
dependencies = [
"cfg-if",
"cpufeatures",
"cpufeatures 0.3.0",
]
[[package]]
name = "kem"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01737161ba802849cfd486b5bd209d38ba4943494c249a8126005170c7621edd"
dependencies = [
"crypto-common 0.2.1",
"rand_core",
]
[[package]]
@@ -2207,12 +2293,27 @@ dependencies = [
"zeroize",
]
[[package]]
name = "ml-kem"
version = "0.3.0-rc.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04437cb1a66c0b78740927b76cc61f218344b9f6ef3dd430e283274a718ef0e9"
dependencies = [
"hybrid-array",
"kem",
"module-lattice",
"rand_core",
"sha3",
"zeroize",
]
[[package]]
name = "module-lattice"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "164eb3faeaecbd14b0b2a917c1b4d0c035097a9c559b0bed85c2cdd032bc8faa"
dependencies = [
"ctutils",
"hybrid-array",
"num-traits",
"zeroize",
@@ -3488,6 +3589,12 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "2.0.117"
@@ -4761,6 +4868,20 @@ version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
[[package]]
name = "x-wing"
version = "0.1.0-rc.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e17d0d5f4d1f26b9b9e7477af1d3bef960e1d1fb64edab7912fde472a8a8432e"
dependencies = [
"kem",
"ml-kem",
"rand_core",
"sha3",
"x25519-dalek",
"zeroize",
]
[[package]]
name = "x11-dl"
version = "2.21.0"
@@ -4793,6 +4914,17 @@ version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd"
[[package]]
name = "x25519-dalek"
version = "3.0.0-pre.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3d5d6ff67acd3945b933e592bfa7143db4fcbb2f871754b6b9fbd7847fc5aea"
dependencies = [
"curve25519-dalek",
"rand_core",
"zeroize",
]
[[package]]
name = "xcursor"
version = "0.3.10"

View File

@@ -1,13 +1,15 @@
use anyhow::anyhow;
use arbiter_crypto::authn::{self, AuthChallenge, USERAGENT_CONTEXT};
use flutter_rust_bridge::frb;
use arbiter_crypto::authn::{self, USERAGENT_CONTEXT};
#[frb(opaque)]
pub struct MldsaKey(authn::SigningKey);
impl MldsaKey {
pub fn from_bytes(bytes: &[u8]) -> anyhow::Result<Self> {
let bytes: [u8; 32] = bytes.try_into().map_err(|_| anyhow!("Invalid key length"))?;
let bytes: [u8; 32] = bytes
.try_into()
.map_err(|_| anyhow!("Invalid key length"))?;
Ok(Self(authn::SigningKey::from_seed(bytes)))
}
@@ -26,4 +28,11 @@ impl MldsaKey {
pub fn get_public_key(&self) -> Vec<u8> {
self.0.public_key().to_bytes().to_vec()
}
}
}
pub fn format_challenge(random: Vec<u8>, timestamp: i64) -> Result<Vec<u8>, String> {
let challenge = AuthChallenge::from_parts(&random, timestamp)
.map_err(|_| "Invalid nonce length".to_string())?;
Ok(challenge.format())
}

View File

@@ -39,7 +39,7 @@ flutter_rust_bridge::frb_generated_boilerplate!(
default_rust_auto_opaque = RustAutoOpaqueMoi,
);
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0";
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -437661335;
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 1247923898;
// Section: executor
@@ -267,6 +267,40 @@ fn wire__crate__api__MldsaKey_to_bytes_impl(
},
)
}
fn wire__crate__api__format_challenge_impl(
port_: flutter_rust_bridge::for_generated::MessagePort,
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
rust_vec_len_: i32,
data_len_: i32,
) {
FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::<flutter_rust_bridge::for_generated::SseCodec, _, _>(
flutter_rust_bridge::for_generated::TaskInfo {
debug_name: "format_challenge",
port: Some(port_),
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
},
move || {
let message = unsafe {
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
ptr_,
rust_vec_len_,
data_len_,
)
};
let mut deserializer =
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
let api_random = <Vec<u8>>::sse_decode(&mut deserializer);
let api_timestamp = <i64>::sse_decode(&mut deserializer);
deserializer.end();
move |context| {
transform_result_sse::<_, String>((move || {
let output_ok = crate::api::format_challenge(api_random, api_timestamp)?;
Ok(output_ok)
})())
}
},
)
}
// Section: related_funcs
@@ -312,6 +346,13 @@ impl SseDecode for String {
}
}
impl SseDecode for i64 {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
deserializer.cursor.read_i64::<NativeEndian>().unwrap()
}
}
impl SseDecode for Vec<u8> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
@@ -371,6 +412,7 @@ fn pde_ffi_dispatcher_primary_impl(
3 => wire__crate__api__MldsaKey_get_public_key_impl(port, ptr, rust_vec_len, data_len),
4 => wire__crate__api__MldsaKey_sign_impl(port, ptr, rust_vec_len, data_len),
5 => wire__crate__api__MldsaKey_to_bytes_impl(port, ptr, rust_vec_len, data_len),
6 => wire__crate__api__format_challenge_impl(port, ptr, rust_vec_len, data_len),
_ => unreachable!(),
}
}
@@ -436,6 +478,13 @@ impl SseEncode for String {
}
}
impl SseEncode for i64 {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
serializer.cursor.write_i64::<NativeEndian>(self).unwrap();
}
}
impl SseEncode for Vec<u8> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {