Compare commits
15 Commits
Client-key
...
8cc15dde53
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8cc15dde53 | ||
|
|
77f47e7436 | ||
|
|
0f8cc9033c | ||
|
|
366a58f5eb | ||
|
|
5141ac4f55 | ||
|
|
f6f4f81acb | ||
| 68ff52139b | |||
| d30c79e4ea | |||
| 726dcb96e4 | |||
|
|
ff51d26d54 | ||
|
|
390f8cd547 | ||
|
|
018c030ee2 | ||
|
|
86052c9350 | ||
|
|
805d5691d6 | ||
|
|
b77f6ed656 |
@@ -24,4 +24,4 @@ steps:
|
||||
- mise install rust
|
||||
- mise install protoc
|
||||
- mise install cargo:cargo-nextest
|
||||
- mise exec cargo:cargo-nextest -- cargo nextest run --no-fail-fast --all-features
|
||||
- mise exec cargo:cargo-nextest -- cargo nextest run --no-fail-fast
|
||||
@@ -67,18 +67,7 @@ The `program_client.nonce` column stores the **next usable nonce** — i.e. it i
|
||||
## Cryptography
|
||||
|
||||
### Authentication
|
||||
- **Client protocol:** ed25519
|
||||
|
||||
### User-Agent Authentication
|
||||
|
||||
User-agent authentication supports multiple signature schemes because platform-provided "hardware-bound" keys do not expose a uniform algorithm across operating systems and hardware.
|
||||
|
||||
- **Supported schemes:** RSA, Ed25519, ECDSA (secp256k1)
|
||||
- **Why:** the user agent authenticates with keys backed by platform facilities, and those facilities differ by platform
|
||||
- **Apple Silicon Secure Enclave / Secure Element:** ECDSA-only in practice
|
||||
- **Windows Hello / TPM 2.0:** currently RSA-backed in our integration
|
||||
|
||||
This is why the user-agent auth protocol carries an explicit `KeyType`, while the SDK client protocol remains fixed to ed25519.
|
||||
- **Signature scheme:** ed25519
|
||||
|
||||
### Encryption at Rest
|
||||
- **Scheme:** Symmetric AEAD — currently **XChaCha20-Poly1305**
|
||||
@@ -159,7 +148,7 @@ The central abstraction is the `Policy` trait. Each implementation handles one s
|
||||
Every grant has two layers:
|
||||
|
||||
- **Shared (`evm_basic_grant`)** — wallet, chain, validity period, gas fee caps, transaction count rate limit. One row per grant regardless of type.
|
||||
- **Specific** — policy-owned tables (`evm_ether_transfer_grant`, `evm_token_transfer_grant`) holding type-specific configuration.
|
||||
- **Specific** — policy-owned tables (`evm_ether_transfer_grant`, `evm_token_transfer_grant`, etc.) holding type-specific configuration.
|
||||
|
||||
`find_all_grants` uses a `#[diesel::auto_type]` base join between the specific and shared tables, then batch-loads related rows (targets, volume limits) in two additional queries to avoid N+1.
|
||||
|
||||
@@ -182,6 +171,7 @@ These are checked centrally in `check_shared_constraints` before policy evaluati
|
||||
- **Only EIP-1559 transactions are supported.** Legacy and EIP-2930 types are rejected outright.
|
||||
- **No opaque-calldata (unknown contract) grant type.** The architecture describes a category for unrecognised contracts, but no policy implements it yet. Any transaction that is not a plain ETH transfer or a known ERC-20 transfer is unconditionally rejected.
|
||||
- **Token registry is static.** Tokens are recognised only if they appear in the hard-coded `arbiter_tokens_registry` crate. There is no mechanism to register additional contracts at runtime.
|
||||
- **Nonce management is not implemented.** The architecture lists nonce deduplication as a core responsibility, but no nonce tracking or enforcement exists yet.
|
||||
|
||||
---
|
||||
|
||||
@@ -189,5 +179,5 @@ These are checked centrally in `check_shared_constraints` before policy evaluati
|
||||
|
||||
The unsealed root key must be held in a hardened memory cell resistant to dumps, page swaps, and hibernation.
|
||||
|
||||
- **Current:** A dedicated memory-protection abstraction is in place, with `memsafe` used behind that abstraction today
|
||||
- **Planned:** Additional backends can be introduced behind the same abstraction, including a custom implementation based on `mlock` (Unix) and `VirtualProtect` (Windows)
|
||||
- **Current:** Using the `memsafe` crate as an interim solution
|
||||
- **Planned:** Custom implementation based on `mlock` (Unix) and `VirtualProtect` (Windows)
|
||||
|
||||
3
server/Cargo.lock
generated
3
server/Cargo.lock
generated
@@ -724,7 +724,6 @@ name = "arbiter-server"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"alloy",
|
||||
"anyhow",
|
||||
"arbiter-proto",
|
||||
"arbiter-tokens-registry",
|
||||
"argon2",
|
||||
@@ -738,11 +737,11 @@ dependencies = [
|
||||
"ed25519-dalek",
|
||||
"fatality",
|
||||
"futures",
|
||||
"hmac",
|
||||
"insta",
|
||||
"k256",
|
||||
"kameo",
|
||||
"memsafe",
|
||||
"miette",
|
||||
"pem",
|
||||
"prost-types",
|
||||
"rand 0.10.0",
|
||||
|
||||
@@ -22,6 +22,7 @@ chrono = { version = "0.4.44", features = ["serde"] }
|
||||
rand = "0.10.0"
|
||||
rustls = { version = "0.23.37", features = ["aws-lc-rs"] }
|
||||
smlang = "0.8.0"
|
||||
miette = { version = "7.6.0", features = ["fancy", "serde"] }
|
||||
thiserror = "2.0.18"
|
||||
async-trait = "0.1.89"
|
||||
futures = "0.3.32"
|
||||
@@ -42,4 +43,3 @@ k256 = { version = "0.13.4", features = ["ecdsa", "pkcs8"] }
|
||||
rsa = { version = "0.9", features = ["sha2"] }
|
||||
sha2 = "0.10"
|
||||
spki = "0.7"
|
||||
miette = { version = "7.6.0", features = ["fancy", "serde"] }
|
||||
@@ -122,7 +122,9 @@ async fn receive_auth_confirmation(
|
||||
.await
|
||||
.map_err(|_| AuthError::UnexpectedAuthResponse)?;
|
||||
|
||||
let payload = response.payload.ok_or(AuthError::UnexpectedAuthResponse)?;
|
||||
let payload = response
|
||||
.payload
|
||||
.ok_or(AuthError::UnexpectedAuthResponse)?;
|
||||
match payload {
|
||||
ClientResponsePayload::Auth(response) => match response.payload {
|
||||
Some(AuthResponsePayload::Result(result))
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
use std::io::{self, Write};
|
||||
|
||||
use arbiter_client::ArbiterClient;
|
||||
@@ -21,6 +22,8 @@ async fn main() {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
||||
let url = match ArbiterUrl::try_from(input) {
|
||||
Ok(url) => url,
|
||||
Err(err) => {
|
||||
@@ -29,7 +32,7 @@ async fn main() {
|
||||
}
|
||||
};
|
||||
|
||||
println!("{:#?}", url);
|
||||
println!("{:#?}", url);
|
||||
|
||||
let metadata = ClientMetadata {
|
||||
name: "arbiter-client test_connect".to_string(),
|
||||
@@ -41,4 +44,4 @@ async fn main() {
|
||||
Ok(_) => println!("Connected and authenticated successfully."),
|
||||
Err(err) => eprintln!("Failed to connect: {:#?}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,11 @@
|
||||
use arbiter_proto::{
|
||||
ClientMetadata, proto::arbiter_service_client::ArbiterServiceClient, url::ArbiterUrl,
|
||||
};
|
||||
use arbiter_proto::{ClientMetadata, proto::arbiter_service_client::ArbiterServiceClient, url::ArbiterUrl};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{Mutex, mpsc};
|
||||
use tokio_stream::wrappers::ReceiverStream;
|
||||
use tonic::transport::ClientTlsConfig;
|
||||
|
||||
use crate::{
|
||||
StorageError,
|
||||
auth::{AuthError, authenticate},
|
||||
storage::{FileSigningKeyStorage, SigningKeyStorage},
|
||||
transport::{BUFFER_LENGTH, ClientTransport},
|
||||
StorageError, auth::{AuthError, authenticate}, storage::{FileSigningKeyStorage, SigningKeyStorage}, transport::{BUFFER_LENGTH, ClientTransport}
|
||||
};
|
||||
|
||||
#[cfg(feature = "evm")]
|
||||
@@ -35,6 +30,7 @@ pub enum Error {
|
||||
|
||||
#[error("Storage error")]
|
||||
Storage(#[from] StorageError),
|
||||
|
||||
}
|
||||
|
||||
pub struct ArbiterClient {
|
||||
@@ -65,11 +61,10 @@ impl ArbiterClient {
|
||||
let anchor = webpki::anchor_from_trusted_cert(&url.ca_cert)?.to_owned();
|
||||
let tls = ClientTlsConfig::new().trust_anchor(anchor);
|
||||
|
||||
let channel =
|
||||
tonic::transport::Channel::from_shared(format!("https://{}:{}", url.host, url.port))?
|
||||
.tls_config(tls)?
|
||||
.connect()
|
||||
.await?;
|
||||
let channel = tonic::transport::Channel::from_shared(format!("https://{}:{}", url.host, url.port))?
|
||||
.tls_config(tls)?
|
||||
.connect()
|
||||
.await?;
|
||||
|
||||
let mut client = ArbiterServiceClient::new(channel);
|
||||
let (tx, rx) = mpsc::channel(BUFFER_LENGTH);
|
||||
|
||||
@@ -9,4 +9,4 @@ pub use client::{ArbiterClient, Error};
|
||||
pub use storage::{FileSigningKeyStorage, SigningKeyStorage, StorageError};
|
||||
|
||||
#[cfg(feature = "evm")]
|
||||
pub use wallets::evm::{ArbiterEvmSignTransactionError, ArbiterEvmWallet};
|
||||
pub use wallets::evm::ArbiterEvmWallet;
|
||||
|
||||
@@ -10,48 +10,14 @@ use tokio::sync::Mutex;
|
||||
|
||||
use arbiter_proto::proto::{
|
||||
client::{
|
||||
ClientRequest,
|
||||
client_request::Payload as ClientRequestPayload,
|
||||
ClientRequest, client_request::Payload as ClientRequestPayload,
|
||||
client_response::Payload as ClientResponsePayload,
|
||||
evm::{
|
||||
self as proto_evm, request::Payload as EvmRequestPayload,
|
||||
response::Payload as EvmResponsePayload,
|
||||
},
|
||||
},
|
||||
evm::{
|
||||
EvmSignTransactionRequest,
|
||||
evm_sign_transaction_response::Result as EvmSignTransactionResult,
|
||||
},
|
||||
shared::evm::TransactionEvalError,
|
||||
evm::evm_sign_transaction_response::Result as EvmSignTransactionResult,
|
||||
};
|
||||
|
||||
use crate::transport::{ClientTransport, next_request_id};
|
||||
|
||||
/// A typed error payload returned by [`ArbiterEvmWallet`] transaction signing.
|
||||
///
|
||||
/// This is wrapped into `alloy::signers::Error::Other`, so consumers can downcast by [`TryFrom`] and
|
||||
/// interpret the concrete policy evaluation failure instead of parsing strings.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum ArbiterEvmSignTransactionError {
|
||||
#[error("transaction rejected by policy: {0:?}")]
|
||||
PolicyEval(TransactionEvalError),
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a Error> for &'a ArbiterEvmSignTransactionError {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: &'a Error) -> Result<Self, Self::Error> {
|
||||
if let Error::Other(inner) = value
|
||||
&& let Some(eval_error) = inner.downcast_ref()
|
||||
{
|
||||
Ok(eval_error)
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ArbiterEvmWallet {
|
||||
transport: Arc<Mutex<ClientTransport>>,
|
||||
address: Address,
|
||||
@@ -130,14 +96,12 @@ impl TxSigner<Signature> for ArbiterEvmWallet {
|
||||
transport
|
||||
.send(ClientRequest {
|
||||
request_id,
|
||||
payload: Some(ClientRequestPayload::Evm(proto_evm::Request {
|
||||
payload: Some(EvmRequestPayload::SignTransaction(
|
||||
EvmSignTransactionRequest {
|
||||
wallet_address: self.address.to_vec(),
|
||||
rlp_transaction,
|
||||
},
|
||||
)),
|
||||
})),
|
||||
payload: Some(ClientRequestPayload::EvmSignTransaction(
|
||||
arbiter_proto::proto::evm::EvmSignTransactionRequest {
|
||||
wallet_address: self.address.to_vec(),
|
||||
rlp_transaction,
|
||||
},
|
||||
)),
|
||||
})
|
||||
.await
|
||||
.map_err(|_| Error::other("failed to send evm sign transaction request"))?;
|
||||
@@ -157,21 +121,12 @@ impl TxSigner<Signature> for ArbiterEvmWallet {
|
||||
.payload
|
||||
.ok_or_else(|| Error::other("missing evm sign transaction response payload"))?;
|
||||
|
||||
let ClientResponsePayload::Evm(proto_evm::Response {
|
||||
payload: Some(payload),
|
||||
}) = payload
|
||||
else {
|
||||
let ClientResponsePayload::EvmSignTransaction(response) = payload else {
|
||||
return Err(Error::other(
|
||||
"unexpected response payload for evm sign transaction request",
|
||||
));
|
||||
};
|
||||
|
||||
let EvmResponsePayload::SignTransaction(response) = payload else {
|
||||
return Err(Error::other(
|
||||
"unexpected evm response payload for sign transaction request",
|
||||
));
|
||||
};
|
||||
|
||||
let result = response
|
||||
.result
|
||||
.ok_or_else(|| Error::other("missing evm sign transaction result"))?;
|
||||
@@ -181,9 +136,9 @@ impl TxSigner<Signature> for ArbiterEvmWallet {
|
||||
Signature::try_from(signature.as_slice())
|
||||
.map_err(|_| Error::other("invalid signature returned by server"))
|
||||
}
|
||||
EvmSignTransactionResult::EvalError(eval_error) => Err(Error::other(
|
||||
ArbiterEvmSignTransactionError::PolicyEval(eval_error),
|
||||
)),
|
||||
EvmSignTransactionResult::EvalError(eval_error) => Err(Error::other(format!(
|
||||
"transaction rejected by policy: {eval_error:?}"
|
||||
))),
|
||||
EvmSignTransactionResult::Error(code) => Err(Error::other(format!(
|
||||
"server failed to sign transaction with error code {code}"
|
||||
))),
|
||||
|
||||
@@ -7,6 +7,7 @@ const ARBITER_URL_SCHEME: &str = "arbiter";
|
||||
const CERT_QUERY_KEY: &str = "cert";
|
||||
const BOOTSTRAP_TOKEN_QUERY_KEY: &str = "bootstrap_token";
|
||||
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ArbiterUrl {
|
||||
pub host: String,
|
||||
|
||||
@@ -25,6 +25,7 @@ tonic.features = ["tls-aws-lc"]
|
||||
tokio.workspace = true
|
||||
rustls.workspace = true
|
||||
smlang.workspace = true
|
||||
miette.workspace = true
|
||||
thiserror.workspace = true
|
||||
fatality = "0.1.1"
|
||||
diesel_migrations = { version = "2.3.1", features = ["sqlite"] }
|
||||
@@ -48,12 +49,10 @@ pem = "3.0.6"
|
||||
k256.workspace = true
|
||||
rsa.workspace = true
|
||||
sha2.workspace = true
|
||||
hmac = "0.12"
|
||||
spki.workspace = true
|
||||
alloy.workspace = true
|
||||
prost-types.workspace = true
|
||||
arbiter-tokens-registry.path = "../arbiter-tokens-registry"
|
||||
anyhow = "1.0.102"
|
||||
|
||||
[dev-dependencies]
|
||||
insta = "1.46.3"
|
||||
|
||||
@@ -47,7 +47,6 @@ 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,
|
||||
pubkey_integrity_tag blob,
|
||||
key_type integer not null default(1), -- 1=Ed25519, 2=ECDSA(secp256k1)
|
||||
created_at integer not null default(unixepoch ('now')),
|
||||
updated_at integer not null default(unixepoch ('now'))
|
||||
|
||||
@@ -2,7 +2,7 @@ use arbiter_proto::{BOOTSTRAP_PATH, home_path};
|
||||
use diesel::QueryDsl;
|
||||
use diesel_async::RunQueryDsl;
|
||||
use kameo::{Actor, messages};
|
||||
|
||||
use miette::Diagnostic;
|
||||
use rand::{RngExt, distr::Alphanumeric, make_rng, rngs::StdRng};
|
||||
use thiserror::Error;
|
||||
|
||||
@@ -25,15 +25,18 @@ pub async fn generate_token() -> Result<String, std::io::Error> {
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
#[derive(Error, Debug, Diagnostic)]
|
||||
pub enum Error {
|
||||
#[error("Database error: {0}")]
|
||||
#[diagnostic(code(arbiter_server::bootstrap::database))]
|
||||
Database(#[from] db::PoolError),
|
||||
|
||||
#[error("Database query error: {0}")]
|
||||
#[diagnostic(code(arbiter_server::bootstrap::database_query))]
|
||||
Query(#[from] diesel::result::Error),
|
||||
|
||||
#[error("I/O error: {0}")]
|
||||
#[diagnostic(code(arbiter_server::bootstrap::io))]
|
||||
Io(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
|
||||
@@ -287,7 +287,10 @@ where
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn authenticate<T>(props: &mut ClientConnection, transport: &mut T) -> Result<i32, Error>
|
||||
pub async fn authenticate<T>(
|
||||
props: &mut ClientConnection,
|
||||
transport: &mut T,
|
||||
) -> Result<i32, Error>
|
||||
where
|
||||
T: Bi<Inbound, Result<Outbound, Error>> + Send + ?Sized,
|
||||
{
|
||||
@@ -316,7 +319,7 @@ where
|
||||
|
||||
sync_client_metadata(&props.db, info.id, &metadata).await?;
|
||||
challenge_client(transport, pubkey, info.current_nonce).await?;
|
||||
|
||||
|
||||
transport
|
||||
.send(Ok(Outbound::AuthSuccess))
|
||||
.await
|
||||
|
||||
@@ -20,7 +20,10 @@ pub struct ClientConnection {
|
||||
|
||||
impl ClientConnection {
|
||||
pub fn new(db: db::DatabasePool, actors: GlobalActors) -> Self {
|
||||
Self { db, actors }
|
||||
Self {
|
||||
db,
|
||||
actors,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,10 +6,11 @@ use alloy::{consensus::TxEip1559, primitives::Address, signers::Signature};
|
||||
use crate::{
|
||||
actors::{
|
||||
GlobalActors,
|
||||
client::ClientConnection,
|
||||
client::ClientConnection, flow_coordinator::RegisterClient,
|
||||
|
||||
evm::{ClientSignTransaction, SignTransactionError},
|
||||
flow_coordinator::RegisterClient,
|
||||
keyholder::KeyHolderState,
|
||||
|
||||
},
|
||||
db,
|
||||
evm::VetError,
|
||||
@@ -94,10 +95,7 @@ impl Actor for ClientSession {
|
||||
impl ClientSession {
|
||||
pub fn new_test(db: db::DatabasePool, actors: GlobalActors) -> Self {
|
||||
let props = ClientConnection::new(db, actors);
|
||||
Self {
|
||||
props,
|
||||
client_id: 0,
|
||||
}
|
||||
Self { props, client_id: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,36 +25,45 @@ use crate::{
|
||||
|
||||
pub use crate::evm::safe_signer;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[derive(Debug, thiserror::Error, miette::Diagnostic)]
|
||||
pub enum SignTransactionError {
|
||||
#[error("Wallet not found")]
|
||||
#[diagnostic(code(arbiter::evm::sign::wallet_not_found))]
|
||||
WalletNotFound,
|
||||
|
||||
#[error("Database error: {0}")]
|
||||
#[diagnostic(code(arbiter::evm::sign::database))]
|
||||
Database(#[from] DatabaseError),
|
||||
|
||||
#[error("Keyholder error: {0}")]
|
||||
#[diagnostic(code(arbiter::evm::sign::keyholder))]
|
||||
Keyholder(#[from] crate::actors::keyholder::Error),
|
||||
|
||||
#[error("Keyholder mailbox error")]
|
||||
#[diagnostic(code(arbiter::evm::sign::keyholder_send))]
|
||||
KeyholderSend,
|
||||
|
||||
#[error("Signing error: {0}")]
|
||||
#[diagnostic(code(arbiter::evm::sign::signing))]
|
||||
Signing(#[from] alloy::signers::Error),
|
||||
|
||||
#[error("Policy error: {0}")]
|
||||
#[diagnostic(code(arbiter::evm::sign::vet))]
|
||||
Vet(#[from] evm::VetError),
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[derive(Debug, thiserror::Error, miette::Diagnostic)]
|
||||
pub enum Error {
|
||||
#[error("Keyholder error: {0}")]
|
||||
#[diagnostic(code(arbiter::evm::keyholder))]
|
||||
Keyholder(#[from] crate::actors::keyholder::Error),
|
||||
|
||||
#[error("Keyholder mailbox error")]
|
||||
#[diagnostic(code(arbiter::evm::keyholder_send))]
|
||||
KeyholderSend,
|
||||
|
||||
#[error("Database error: {0}")]
|
||||
#[diagnostic(code(arbiter::evm::database))]
|
||||
Database(#[from] DatabaseError),
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ use crate::actors::{
|
||||
pub struct Args {
|
||||
pub client: ClientProfile,
|
||||
pub user_agents: Vec<ActorRef<UserAgentSession>>,
|
||||
pub reply: ReplySender<Result<bool, ApprovalError>>,
|
||||
pub reply: ReplySender<Result<bool, ApprovalError>>
|
||||
}
|
||||
|
||||
pub struct ClientApprovalController {
|
||||
@@ -39,11 +39,7 @@ impl Actor for ClientApprovalController {
|
||||
type Error = ();
|
||||
|
||||
async fn on_start(
|
||||
Args {
|
||||
client,
|
||||
mut user_agents,
|
||||
reply,
|
||||
}: Self::Args,
|
||||
Args { client, mut user_agents, reply }: Self::Args,
|
||||
actor_ref: ActorRef<Self>,
|
||||
) -> Result<Self, Self::Error> {
|
||||
let this = Self {
|
||||
|
||||
@@ -1,21 +1,52 @@
|
||||
use std::ops::Deref as _;
|
||||
|
||||
use argon2::{Algorithm, Argon2};
|
||||
use argon2::{Algorithm, Argon2, password_hash::Salt as ArgonSalt};
|
||||
use chacha20poly1305::{
|
||||
AeadInPlace, Key, KeyInit as _, XChaCha20Poly1305, XNonce,
|
||||
aead::{AeadMut, Error, Payload},
|
||||
};
|
||||
use rand::{
|
||||
Rng as _, SeedableRng as _,
|
||||
Rng as _, SeedableRng,
|
||||
rngs::{StdRng, SysRng},
|
||||
};
|
||||
|
||||
use crate::safe_cell::{SafeCell, SafeCellHandle as _};
|
||||
|
||||
pub mod encryption;
|
||||
pub mod integrity;
|
||||
pub const ROOT_KEY_TAG: &[u8] = "arbiter/seal/v1".as_bytes();
|
||||
pub const TAG: &[u8] = "arbiter/private-key/v1".as_bytes();
|
||||
|
||||
use encryption::v1::{Nonce, Salt};
|
||||
pub const NONCE_LENGTH: usize = 24;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Nonce([u8; NONCE_LENGTH]);
|
||||
impl Nonce {
|
||||
pub fn increment(&mut self) {
|
||||
for i in (0..self.0.len()).rev() {
|
||||
if self.0[i] == 0xFF {
|
||||
self.0[i] = 0;
|
||||
} else {
|
||||
self.0[i] += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_vec(&self) -> Vec<u8> {
|
||||
self.0.to_vec()
|
||||
}
|
||||
}
|
||||
impl<'a> TryFrom<&'a [u8]> for Nonce {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
|
||||
if value.len() != NONCE_LENGTH {
|
||||
return Err(());
|
||||
}
|
||||
let mut nonce = [0u8; NONCE_LENGTH];
|
||||
nonce.copy_from_slice(value);
|
||||
Ok(Self(nonce))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct KeyCell(pub SafeCell<Key>);
|
||||
impl From<SafeCell<Key>> for KeyCell {
|
||||
@@ -102,9 +133,22 @@ impl KeyCell {
|
||||
}
|
||||
}
|
||||
|
||||
pub type Salt = [u8; ArgonSalt::RECOMMENDED_LENGTH];
|
||||
|
||||
pub fn generate_salt() -> Salt {
|
||||
let mut salt = Salt::default();
|
||||
#[allow(
|
||||
clippy::unwrap_used,
|
||||
reason = "Rng failure is unrecoverable and should panic"
|
||||
)]
|
||||
let mut rng = StdRng::try_from_rng(&mut SysRng).unwrap();
|
||||
rng.fill_bytes(&mut salt);
|
||||
salt
|
||||
}
|
||||
|
||||
/// User password might be of different length, have not enough entropy, etc...
|
||||
/// Derive a fixed-length key from the password using Argon2id, which is designed for password hashing and key derivation.
|
||||
pub fn derive_key(mut password: SafeCell<Vec<u8>>, salt: &Salt) -> KeyCell {
|
||||
pub fn derive_seal_key(mut password: SafeCell<Vec<u8>>, salt: &Salt) -> KeyCell {
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let params = argon2::Params::new(262_144, 3, 4, None).unwrap();
|
||||
let hasher = Argon2::new(Algorithm::Argon2id, argon2::Version::V0x13, params);
|
||||
@@ -127,11 +171,37 @@ pub fn derive_key(mut password: SafeCell<Vec<u8>>, salt: &Salt) -> KeyCell {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
derive_key,
|
||||
encryption::v1::{Nonce, generate_salt},
|
||||
};
|
||||
use crate::safe_cell::{SafeCell, SafeCellHandle as _};
|
||||
use super::*;
|
||||
use crate::safe_cell::SafeCell;
|
||||
|
||||
#[test]
|
||||
pub fn derive_seal_key_deterministic() {
|
||||
static PASSWORD: &[u8] = b"password";
|
||||
let password = SafeCell::new(PASSWORD.to_vec());
|
||||
let password2 = SafeCell::new(PASSWORD.to_vec());
|
||||
let salt = generate_salt();
|
||||
|
||||
let mut key1 = derive_seal_key(password, &salt);
|
||||
let mut key2 = derive_seal_key(password2, &salt);
|
||||
|
||||
let key1_reader = key1.0.read();
|
||||
let key2_reader = key2.0.read();
|
||||
|
||||
assert_eq!(key1_reader.deref(), key2_reader.deref());
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn successful_derive() {
|
||||
static PASSWORD: &[u8] = b"password";
|
||||
let password = SafeCell::new(PASSWORD.to_vec());
|
||||
let salt = generate_salt();
|
||||
|
||||
let mut key = derive_seal_key(password, &salt);
|
||||
let key_reader = key.0.read();
|
||||
let key_ref = key_reader.deref();
|
||||
|
||||
assert_ne!(key_ref.as_slice(), &[0u8; 32][..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn encrypt_decrypt() {
|
||||
@@ -139,7 +209,7 @@ mod tests {
|
||||
let password = SafeCell::new(PASSWORD.to_vec());
|
||||
let salt = generate_salt();
|
||||
|
||||
let mut key = derive_key(password, &salt);
|
||||
let mut key = derive_seal_key(password, &salt);
|
||||
let nonce = Nonce(*b"unique nonce 123 1231233"); // 24 bytes for XChaCha20Poly1305
|
||||
let associated_data = b"associated data";
|
||||
let mut buffer = b"secret data".to_vec();
|
||||
@@ -156,4 +226,18 @@ mod tests {
|
||||
let buffer = buffer.read();
|
||||
assert_eq!(*buffer, b"secret data");
|
||||
}
|
||||
|
||||
#[test]
|
||||
// We should fuzz this
|
||||
pub fn test_nonce_increment() {
|
||||
let mut nonce = Nonce([0u8; NONCE_LENGTH]);
|
||||
nonce.increment();
|
||||
|
||||
assert_eq!(
|
||||
nonce.0,
|
||||
[
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,14 +8,7 @@ use kameo::{Actor, Reply, messages};
|
||||
use strum::{EnumDiscriminants, IntoDiscriminant};
|
||||
use tracing::{error, info};
|
||||
|
||||
use crate::{
|
||||
crypto::{
|
||||
KeyCell, derive_key,
|
||||
encryption::v1::{self, Nonce},
|
||||
integrity::v1::compute_integrity_tag,
|
||||
},
|
||||
safe_cell::SafeCell,
|
||||
};
|
||||
use crate::safe_cell::SafeCell;
|
||||
use crate::{
|
||||
db::{
|
||||
self,
|
||||
@@ -24,6 +17,9 @@ use crate::{
|
||||
},
|
||||
safe_cell::SafeCellHandle as _,
|
||||
};
|
||||
use encryption::v1::{self, KeyCell, Nonce};
|
||||
|
||||
pub mod encryption;
|
||||
|
||||
#[derive(Default, EnumDiscriminants)]
|
||||
#[strum_discriminants(derive(Reply), vis(pub), name(KeyHolderState))]
|
||||
@@ -39,28 +35,36 @@ enum State {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[derive(Debug, thiserror::Error, miette::Diagnostic)]
|
||||
pub enum Error {
|
||||
#[error("Keyholder is already bootstrapped")]
|
||||
#[diagnostic(code(arbiter::keyholder::already_bootstrapped))]
|
||||
AlreadyBootstrapped,
|
||||
#[error("Keyholder is not bootstrapped")]
|
||||
#[diagnostic(code(arbiter::keyholder::not_bootstrapped))]
|
||||
NotBootstrapped,
|
||||
#[error("Invalid key provided")]
|
||||
#[diagnostic(code(arbiter::keyholder::invalid_key))]
|
||||
InvalidKey,
|
||||
|
||||
#[error("Requested aead entry not found")]
|
||||
#[diagnostic(code(arbiter::keyholder::aead_not_found))]
|
||||
NotFound,
|
||||
|
||||
#[error("Encryption error: {0}")]
|
||||
#[diagnostic(code(arbiter::keyholder::encryption_error))]
|
||||
Encryption(#[from] chacha20poly1305::aead::Error),
|
||||
|
||||
#[error("Database error: {0}")]
|
||||
#[diagnostic(code(arbiter::keyholder::database_error))]
|
||||
DatabaseConnection(#[from] db::PoolError),
|
||||
|
||||
#[error("Database transaction error: {0}")]
|
||||
#[diagnostic(code(arbiter::keyholder::database_transaction_error))]
|
||||
DatabaseTransaction(#[from] diesel::result::Error),
|
||||
|
||||
#[error("Broken database")]
|
||||
#[diagnostic(code(arbiter::keyholder::broken_database))]
|
||||
BrokenDatabase,
|
||||
}
|
||||
|
||||
@@ -110,13 +114,14 @@ impl KeyHolder {
|
||||
.first(conn)
|
||||
.await?;
|
||||
|
||||
let mut nonce = Nonce::try_from(current_nonce.as_slice()).map_err(|_| {
|
||||
error!(
|
||||
"Broken database: invalid nonce for root key history id={}",
|
||||
root_key_id
|
||||
);
|
||||
Error::BrokenDatabase
|
||||
})?;
|
||||
let mut nonce =
|
||||
v1::Nonce::try_from(current_nonce.as_slice()).map_err(|_| {
|
||||
error!(
|
||||
"Broken database: invalid nonce for root key history id={}",
|
||||
root_key_id
|
||||
);
|
||||
Error::BrokenDatabase
|
||||
})?;
|
||||
nonce.increment();
|
||||
|
||||
update(schema::root_key_history::table)
|
||||
@@ -139,12 +144,12 @@ impl KeyHolder {
|
||||
return Err(Error::AlreadyBootstrapped);
|
||||
}
|
||||
let salt = v1::generate_salt();
|
||||
let mut seal_key = derive_key(seal_key_raw, &salt);
|
||||
let mut seal_key = v1::derive_seal_key(seal_key_raw, &salt);
|
||||
let mut root_key = KeyCell::new_secure_random();
|
||||
|
||||
// Zero nonces are fine because they are one-time
|
||||
let root_key_nonce = Nonce::default();
|
||||
let data_encryption_nonce = Nonce::default();
|
||||
let root_key_nonce = v1::Nonce::default();
|
||||
let data_encryption_nonce = v1::Nonce::default();
|
||||
|
||||
let root_key_ciphertext: Vec<u8> = root_key.0.read_inline(|reader| {
|
||||
let root_key_reader = reader.as_slice();
|
||||
@@ -219,7 +224,7 @@ impl KeyHolder {
|
||||
error!("Broken database: invalid salt for root key");
|
||||
Error::BrokenDatabase
|
||||
})?;
|
||||
let mut seal_key = derive_key(seal_key_raw, &salt);
|
||||
let mut seal_key = v1::derive_seal_key(seal_key_raw, &salt);
|
||||
|
||||
let mut root_key = SafeCell::new(current_key.ciphertext.clone());
|
||||
|
||||
@@ -239,7 +244,7 @@ impl KeyHolder {
|
||||
|
||||
self.state = State::Unsealed {
|
||||
root_key_history_id: current_key.id,
|
||||
root_key: KeyCell::try_from(root_key).map_err(|err| {
|
||||
root_key: v1::KeyCell::try_from(root_key).map_err(|err| {
|
||||
error!(?err, "Broken database: invalid encryption key size");
|
||||
Error::BrokenDatabase
|
||||
})?,
|
||||
@@ -250,22 +255,7 @@ impl KeyHolder {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Signs a generic integrity payload using the vault-derived integrity key
|
||||
#[message]
|
||||
pub fn sign_integrity_tag(
|
||||
&mut self,
|
||||
purpose_tag: Vec<u8>,
|
||||
data_parts: Vec<Vec<u8>>,
|
||||
) -> Result<Vec<u8>, Error> {
|
||||
let State::Unsealed { root_key, .. } = &mut self.state else {
|
||||
return Err(Error::NotBootstrapped);
|
||||
};
|
||||
|
||||
let tag =
|
||||
compute_integrity_tag(root_key, &purpose_tag, data_parts.iter().map(Vec::as_slice));
|
||||
Ok(tag.to_vec())
|
||||
}
|
||||
|
||||
// Decrypts the `aead_encrypted` entry with the given ID and returns the plaintext
|
||||
#[message]
|
||||
pub async fn decrypt(&mut self, aead_id: i32) -> Result<SafeCell<Vec<u8>>, Error> {
|
||||
let State::Unsealed { root_key, .. } = &mut self.state else {
|
||||
@@ -301,7 +291,6 @@ impl KeyHolder {
|
||||
let State::Unsealed {
|
||||
root_key,
|
||||
root_key_history_id,
|
||||
..
|
||||
} = &mut self.state
|
||||
else {
|
||||
return Err(Error::NotBootstrapped);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use kameo::actor::{ActorRef, Spawn};
|
||||
use miette::Diagnostic;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{
|
||||
@@ -16,12 +17,14 @@ pub mod flow_coordinator;
|
||||
pub mod keyholder;
|
||||
pub mod user_agent;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
#[derive(Error, Debug, Diagnostic)]
|
||||
pub enum SpawnError {
|
||||
#[error("Failed to spawn Bootstrapper actor")]
|
||||
#[diagnostic(code(SpawnError::Bootstrapper))]
|
||||
Bootstrapper(#[from] bootstrap::Error),
|
||||
|
||||
#[error("Failed to spawn KeyHolder actor")]
|
||||
#[diagnostic(code(SpawnError::KeyHolder))]
|
||||
KeyHolder(#[from] keyholder::Error),
|
||||
}
|
||||
|
||||
|
||||
@@ -1,27 +1,17 @@
|
||||
use arbiter_proto::transport::Bi;
|
||||
use diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl, update};
|
||||
use diesel_async::RunQueryDsl;
|
||||
use kameo::error::SendError;
|
||||
use tracing::error;
|
||||
|
||||
use super::Error;
|
||||
use crate::{
|
||||
actors::{
|
||||
bootstrap::ConsumeToken,
|
||||
keyholder::{self, SignIntegrityTag},
|
||||
user_agent::{AuthPublicKey, UserAgentConnection, auth::Outbound},
|
||||
},
|
||||
crypto::integrity::v1::USERAGENT_INTEGRITY_TAG,
|
||||
db::schema,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AttestationStatus {
|
||||
Attested,
|
||||
NotAttested,
|
||||
Unavailable,
|
||||
}
|
||||
|
||||
pub struct ChallengeRequest {
|
||||
pub pubkey: AuthPublicKey,
|
||||
}
|
||||
@@ -50,11 +40,7 @@ smlang::statemachine!(
|
||||
}
|
||||
);
|
||||
|
||||
async fn create_nonce(
|
||||
db: &crate::db::DatabasePool,
|
||||
pubkey_bytes: &[u8],
|
||||
key_type: crate::db::models::KeyType,
|
||||
) -> Result<i32, Error> {
|
||||
async fn create_nonce(db: &crate::db::DatabasePool, pubkey_bytes: &[u8]) -> Result<i32, Error> {
|
||||
let mut db_conn = db.get().await.map_err(|e| {
|
||||
error!(error = ?e, "Database pool error");
|
||||
Error::internal("Database unavailable")
|
||||
@@ -64,14 +50,12 @@ async fn create_nonce(
|
||||
Box::pin(async move {
|
||||
let current_nonce = schema::useragent_client::table
|
||||
.filter(schema::useragent_client::public_key.eq(pubkey_bytes.to_vec()))
|
||||
.filter(schema::useragent_client::key_type.eq(key_type))
|
||||
.select(schema::useragent_client::nonce)
|
||||
.first::<i32>(conn)
|
||||
.await?;
|
||||
|
||||
update(schema::useragent_client::table)
|
||||
.filter(schema::useragent_client::public_key.eq(pubkey_bytes.to_vec()))
|
||||
.filter(schema::useragent_client::key_type.eq(key_type))
|
||||
.set(schema::useragent_client::nonce.eq(current_nonce + 1))
|
||||
.execute(conn)
|
||||
.await?;
|
||||
@@ -91,11 +75,7 @@ async fn create_nonce(
|
||||
})
|
||||
}
|
||||
|
||||
async fn register_key(
|
||||
db: &crate::db::DatabasePool,
|
||||
pubkey: &AuthPublicKey,
|
||||
integrity_tag: Option<Vec<u8>>,
|
||||
) -> Result<(), Error> {
|
||||
async fn register_key(db: &crate::db::DatabasePool, pubkey: &AuthPublicKey) -> Result<(), Error> {
|
||||
let pubkey_bytes = pubkey.to_stored_bytes();
|
||||
let key_type = pubkey.key_type();
|
||||
let mut conn = db.get().await.map_err(|e| {
|
||||
@@ -108,7 +88,6 @@ async fn register_key(
|
||||
schema::useragent_client::public_key.eq(pubkey_bytes),
|
||||
schema::useragent_client::nonce.eq(1),
|
||||
schema::useragent_client::key_type.eq(key_type),
|
||||
schema::useragent_client::pubkey_integrity_tag.eq(integrity_tag),
|
||||
))
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
@@ -141,15 +120,8 @@ where
|
||||
&mut self,
|
||||
ChallengeRequest { pubkey }: ChallengeRequest,
|
||||
) -> Result<ChallengeContext, Self::Error> {
|
||||
match self.verify_pubkey_attestation_status(&pubkey).await? {
|
||||
AttestationStatus::Attested | AttestationStatus::Unavailable => {}
|
||||
AttestationStatus::NotAttested => {
|
||||
return Err(Error::InvalidChallengeSolution);
|
||||
}
|
||||
}
|
||||
|
||||
let stored_bytes = pubkey.to_stored_bytes();
|
||||
let nonce = create_nonce(&self.conn.db, &stored_bytes, pubkey.key_type()).await?;
|
||||
let nonce = create_nonce(&self.conn.db, &stored_bytes).await?;
|
||||
|
||||
self.transport
|
||||
.send(Ok(Outbound::AuthChallenge { nonce }))
|
||||
@@ -189,15 +161,7 @@ where
|
||||
return Err(Error::InvalidBootstrapToken);
|
||||
}
|
||||
|
||||
let integrity_tag = self
|
||||
.try_sign_pubkey_integrity_tag(&pubkey)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!(?err, "Failed to sign user-agent pubkey integrity tag");
|
||||
Error::internal("Failed to sign user-agent pubkey integrity tag")
|
||||
})?;
|
||||
|
||||
register_key(&self.conn.db, &pubkey, integrity_tag).await?;
|
||||
register_key(&self.conn.db, &pubkey).await?;
|
||||
|
||||
self.transport
|
||||
.send(Ok(Outbound::AuthSuccess))
|
||||
@@ -246,111 +210,16 @@ where
|
||||
}
|
||||
};
|
||||
|
||||
match valid {
|
||||
true => {
|
||||
self.transport
|
||||
.send(Ok(Outbound::AuthSuccess))
|
||||
.await
|
||||
.map_err(|_| Error::Transport)?;
|
||||
Ok(key.clone())
|
||||
}
|
||||
false => {
|
||||
self.transport
|
||||
.send(Err(Error::InvalidChallengeSolution))
|
||||
.await
|
||||
.map_err(|_| Error::Transport)?;
|
||||
Err(Error::InvalidChallengeSolution)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> AuthContext<'_, T>
|
||||
where
|
||||
T: Bi<super::Inbound, Result<super::Outbound, Error>> + Send,
|
||||
{
|
||||
async fn try_sign_pubkey_integrity_tag(
|
||||
&self,
|
||||
pubkey: &AuthPublicKey,
|
||||
) -> Result<Option<Vec<u8>>, Error> {
|
||||
let signed = self
|
||||
.conn
|
||||
.actors
|
||||
.key_holder
|
||||
.ask(SignIntegrityTag {
|
||||
purpose_tag: USERAGENT_INTEGRITY_TAG.to_vec(),
|
||||
data_parts: vec![
|
||||
(pubkey.key_type() as i32).to_be_bytes().to_vec(),
|
||||
pubkey.to_stored_bytes(),
|
||||
],
|
||||
})
|
||||
.await;
|
||||
|
||||
match signed {
|
||||
Ok(tag) => Ok(Some(tag)),
|
||||
Err(SendError::HandlerError(keyholder::Error::NotBootstrapped)) => Ok(None),
|
||||
Err(SendError::HandlerError(err)) => {
|
||||
error!(
|
||||
?err,
|
||||
"Keyholder failed to sign user-agent pubkey integrity tag"
|
||||
);
|
||||
Err(Error::internal(
|
||||
"Keyholder failed to sign user-agent pubkey integrity tag",
|
||||
))
|
||||
}
|
||||
Err(err) => {
|
||||
error!(
|
||||
?err,
|
||||
"Failed to contact keyholder for user-agent pubkey integrity tag"
|
||||
);
|
||||
Err(Error::internal(
|
||||
"Failed to contact keyholder for user-agent pubkey integrity tag",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn verify_pubkey_attestation_status(
|
||||
&self,
|
||||
pubkey: &AuthPublicKey,
|
||||
) -> Result<AttestationStatus, Error> {
|
||||
let stored_tag: Option<Option<Vec<u8>>> = {
|
||||
let mut conn = self.conn.db.get().await.map_err(|e| {
|
||||
error!(error = ?e, "Database pool error");
|
||||
Error::internal("Database unavailable")
|
||||
})?;
|
||||
|
||||
schema::useragent_client::table
|
||||
.filter(schema::useragent_client::public_key.eq(pubkey.to_stored_bytes()))
|
||||
.filter(schema::useragent_client::key_type.eq(pubkey.key_type()))
|
||||
.select(schema::useragent_client::pubkey_integrity_tag)
|
||||
.first::<Option<Vec<u8>>>(&mut conn)
|
||||
.await
|
||||
.optional()
|
||||
.map_err(|e| {
|
||||
error!(error = ?e, "Database error");
|
||||
Error::internal("Database operation failed")
|
||||
})?
|
||||
};
|
||||
|
||||
let Some(stored_tag) = stored_tag else {
|
||||
return Err(Error::UnregisteredPublicKey);
|
||||
};
|
||||
|
||||
let Some(expected_tag) = self.try_sign_pubkey_integrity_tag(pubkey).await? else {
|
||||
return Ok(AttestationStatus::Unavailable);
|
||||
};
|
||||
|
||||
match stored_tag {
|
||||
Some(stored_tag) if stored_tag == expected_tag => Ok(AttestationStatus::Attested),
|
||||
Some(_) => {
|
||||
error!("User-agent pubkey integrity tag mismatch");
|
||||
Ok(AttestationStatus::NotAttested)
|
||||
}
|
||||
None => {
|
||||
error!("Missing pubkey integrity tag for registered key while vault is unsealed");
|
||||
Ok(AttestationStatus::NotAttested)
|
||||
}
|
||||
if !valid {
|
||||
error!("Invalid challenge solution signature");
|
||||
return Err(Error::InvalidChallengeSolution);
|
||||
}
|
||||
|
||||
self.transport
|
||||
.send(Ok(Outbound::AuthSuccess))
|
||||
.await
|
||||
.map_err(|_| Error::Transport)?;
|
||||
|
||||
Ok(key.clone())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ 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 kameo::messages;
|
||||
use tracing::{error, info};
|
||||
use x25519_dalek::{EphemeralSecret, PublicKey};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use miette::Diagnostic;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{
|
||||
@@ -10,24 +11,30 @@ use crate::{
|
||||
|
||||
pub mod tls;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
#[derive(Error, Debug, Diagnostic)]
|
||||
pub enum InitError {
|
||||
#[error("Database setup failed: {0}")]
|
||||
#[diagnostic(code(arbiter_server::init::database_setup))]
|
||||
DatabaseSetup(#[from] db::DatabaseSetupError),
|
||||
|
||||
#[error("Connection acquire failed: {0}")]
|
||||
#[diagnostic(code(arbiter_server::init::database_pool))]
|
||||
DatabasePool(#[from] db::PoolError),
|
||||
|
||||
#[error("Database query error: {0}")]
|
||||
#[diagnostic(code(arbiter_server::init::database_query))]
|
||||
DatabaseQuery(#[from] diesel::result::Error),
|
||||
|
||||
#[error("TLS initialization failed: {0}")]
|
||||
#[diagnostic(code(arbiter_server::init::tls_init))]
|
||||
Tls(#[from] tls::InitError),
|
||||
|
||||
#[error("Actor spawn failed: {0}")]
|
||||
#[diagnostic(code(arbiter_server::init::actor_spawn))]
|
||||
ActorSpawn(#[from] crate::actors::SpawnError),
|
||||
|
||||
#[error("I/O Error: {0}")]
|
||||
#[diagnostic(code(arbiter_server::init::io))]
|
||||
Io(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::{net::Ipv4Addr, string::FromUtf8Error};
|
||||
use std::{net::IpAddr, string::FromUtf8Error};
|
||||
|
||||
use diesel::{ExpressionMethods as _, QueryDsl, SelectableHelper as _};
|
||||
use diesel_async::{AsyncConnection, RunQueryDsl};
|
||||
|
||||
use miette::Diagnostic;
|
||||
use pem::Pem;
|
||||
use rcgen::{
|
||||
BasicConstraints, Certificate, CertificateParams, CertifiedIssuer, DistinguishedName, DnType,
|
||||
@@ -29,24 +29,30 @@ const ENCODE_CONFIG: pem::EncodeConfig = {
|
||||
pem::EncodeConfig::new().set_line_ending(line_ending)
|
||||
};
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
#[derive(Error, Debug, Diagnostic)]
|
||||
pub enum InitError {
|
||||
#[error("Key generation error during TLS initialization: {0}")]
|
||||
#[diagnostic(code(arbiter_server::tls_init::key_generation))]
|
||||
KeyGeneration(#[from] rcgen::Error),
|
||||
|
||||
#[error("Key invalid format: {0}")]
|
||||
#[diagnostic(code(arbiter_server::tls_init::key_invalid_format))]
|
||||
KeyInvalidFormat(#[from] FromUtf8Error),
|
||||
|
||||
#[error("Key deserialization error: {0}")]
|
||||
#[diagnostic(code(arbiter_server::tls_init::key_deserialization))]
|
||||
KeyDeserializationError(rcgen::Error),
|
||||
|
||||
#[error("Database error during TLS initialization: {0}")]
|
||||
#[diagnostic(code(arbiter_server::tls_init::database_error))]
|
||||
DatabaseError(#[from] diesel::result::Error),
|
||||
|
||||
#[error("Pem deserialization error during TLS initialization: {0}")]
|
||||
#[diagnostic(code(arbiter_server::tls_init::pem_deserialization))]
|
||||
PemDeserializationError(#[from] rustls::pki_types::pem::Error),
|
||||
|
||||
#[error("Database pool acquire error during TLS initialization: {0}")]
|
||||
#[diagnostic(code(arbiter_server::tls_init::database_pool_acquire))]
|
||||
DatabasePoolAcquire(#[from] db::PoolError),
|
||||
}
|
||||
|
||||
@@ -110,7 +116,9 @@ impl TlsCa {
|
||||
];
|
||||
params
|
||||
.subject_alt_names
|
||||
.push(SanType::IpAddress(Ipv4Addr::LOCALHOST.into()));
|
||||
.push(SanType::IpAddress(IpAddr::from([
|
||||
127, 0, 0, 1,
|
||||
])));
|
||||
|
||||
let mut dn = DistinguishedName::new();
|
||||
dn.push(DnType::CommonName, "Arbiter Instance Leaf");
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
use argon2::password_hash::Salt as ArgonSalt;
|
||||
|
||||
use rand::{
|
||||
Rng as _, SeedableRng,
|
||||
rngs::{StdRng, SysRng},
|
||||
};
|
||||
|
||||
pub const ROOT_KEY_TAG: &[u8] = "arbiter/seal/v1".as_bytes();
|
||||
pub const TAG: &[u8] = "arbiter/private-key/v1".as_bytes();
|
||||
|
||||
pub const NONCE_LENGTH: usize = 24;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Nonce(pub [u8; NONCE_LENGTH]);
|
||||
impl Nonce {
|
||||
pub fn increment(&mut self) {
|
||||
for i in (0..self.0.len()).rev() {
|
||||
if self.0[i] == 0xFF {
|
||||
self.0[i] = 0;
|
||||
} else {
|
||||
self.0[i] += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_vec(&self) -> Vec<u8> {
|
||||
self.0.to_vec()
|
||||
}
|
||||
}
|
||||
impl<'a> TryFrom<&'a [u8]> for Nonce {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
|
||||
if value.len() != NONCE_LENGTH {
|
||||
return Err(());
|
||||
}
|
||||
let mut nonce = [0u8; NONCE_LENGTH];
|
||||
nonce.copy_from_slice(value);
|
||||
Ok(Self(nonce))
|
||||
}
|
||||
}
|
||||
|
||||
pub type Salt = [u8; ArgonSalt::RECOMMENDED_LENGTH];
|
||||
|
||||
pub fn generate_salt() -> Salt {
|
||||
let mut salt = Salt::default();
|
||||
#[allow(
|
||||
clippy::unwrap_used,
|
||||
reason = "Rng failure is unrecoverable and should panic"
|
||||
)]
|
||||
let mut rng = StdRng::try_from_rng(&mut SysRng).unwrap();
|
||||
rng.fill_bytes(&mut salt);
|
||||
salt
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::ops::Deref as _;
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
crypto::derive_key,
|
||||
safe_cell::{SafeCell, SafeCellHandle as _},
|
||||
};
|
||||
|
||||
#[test]
|
||||
pub fn derive_seal_key_deterministic() {
|
||||
static PASSWORD: &[u8] = b"password";
|
||||
let password = SafeCell::new(PASSWORD.to_vec());
|
||||
let password2 = SafeCell::new(PASSWORD.to_vec());
|
||||
let salt = generate_salt();
|
||||
|
||||
let mut key1 = derive_key(password, &salt);
|
||||
let mut key2 = derive_key(password2, &salt);
|
||||
|
||||
let key1_reader = key1.0.read();
|
||||
let key2_reader = key2.0.read();
|
||||
|
||||
assert_eq!(key1_reader.deref(), key2_reader.deref());
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn successful_derive() {
|
||||
static PASSWORD: &[u8] = b"password";
|
||||
let password = SafeCell::new(PASSWORD.to_vec());
|
||||
let salt = generate_salt();
|
||||
|
||||
let mut key = derive_key(password, &salt);
|
||||
let key_reader = key.0.read();
|
||||
let key_ref = key_reader.deref();
|
||||
|
||||
assert_ne!(key_ref.as_slice(), &[0u8; 32][..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
// We should fuzz this
|
||||
pub fn test_nonce_increment() {
|
||||
let mut nonce = Nonce([0u8; NONCE_LENGTH]);
|
||||
nonce.increment();
|
||||
|
||||
assert_eq!(
|
||||
nonce.0,
|
||||
[
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
pub mod v1;
|
||||
@@ -1,78 +0,0 @@
|
||||
use crate::{crypto::KeyCell, safe_cell::SafeCellHandle as _};
|
||||
use chacha20poly1305::Key;
|
||||
use hmac::Mac as _;
|
||||
|
||||
pub const USERAGENT_INTEGRITY_DERIVE_TAG: &[u8] = "arbiter/useragent/integrity-key/v1".as_bytes();
|
||||
pub const USERAGENT_INTEGRITY_TAG: &[u8] = "arbiter/useragent/pubkey-entry/v1".as_bytes();
|
||||
|
||||
/// Computes an integrity tag for a specific domain and payload shape.
|
||||
pub fn compute_integrity_tag<'a, I>(
|
||||
integrity_key: &mut KeyCell,
|
||||
purpose_tag: &[u8],
|
||||
data_parts: I,
|
||||
) -> [u8; 32]
|
||||
where
|
||||
I: IntoIterator<Item = &'a [u8]>,
|
||||
{
|
||||
type HmacSha256 = hmac::Hmac<sha2::Sha256>;
|
||||
|
||||
let mut output_tag = [0u8; 32];
|
||||
integrity_key.0.read_inline(|integrity_key_bytes: &Key| {
|
||||
let mut mac = <HmacSha256 as hmac::Mac>::new_from_slice(integrity_key_bytes.as_ref())
|
||||
.expect("HMAC key initialization must not fail for 32-byte key");
|
||||
mac.update(purpose_tag);
|
||||
for data_part in data_parts {
|
||||
mac.update(data_part);
|
||||
}
|
||||
output_tag.copy_from_slice(&mac.finalize().into_bytes());
|
||||
});
|
||||
|
||||
output_tag
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
crypto::{derive_key, encryption::v1::generate_salt},
|
||||
safe_cell::{SafeCell, SafeCellHandle as _},
|
||||
};
|
||||
|
||||
use super::{USERAGENT_INTEGRITY_TAG, compute_integrity_tag};
|
||||
|
||||
#[test]
|
||||
pub fn integrity_tag_deterministic() {
|
||||
let salt = generate_salt();
|
||||
let mut integrity_key = derive_key(SafeCell::new(b"password".to_vec()), &salt);
|
||||
let key_type = 1i32.to_be_bytes();
|
||||
let t1 = compute_integrity_tag(
|
||||
&mut integrity_key,
|
||||
USERAGENT_INTEGRITY_TAG,
|
||||
[key_type.as_slice(), b"pubkey".as_ref()],
|
||||
);
|
||||
let t2 = compute_integrity_tag(
|
||||
&mut integrity_key,
|
||||
USERAGENT_INTEGRITY_TAG,
|
||||
[key_type.as_slice(), b"pubkey".as_ref()],
|
||||
);
|
||||
assert_eq!(t1, t2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn integrity_tag_changes_with_payload() {
|
||||
let salt = generate_salt();
|
||||
let mut integrity_key = derive_key(SafeCell::new(b"password".to_vec()), &salt);
|
||||
let key_type_1 = 1i32.to_be_bytes();
|
||||
let key_type_2 = 2i32.to_be_bytes();
|
||||
let t1 = compute_integrity_tag(
|
||||
&mut integrity_key,
|
||||
USERAGENT_INTEGRITY_TAG,
|
||||
[key_type_1.as_slice(), b"pubkey".as_ref()],
|
||||
);
|
||||
let t2 = compute_integrity_tag(
|
||||
&mut integrity_key,
|
||||
USERAGENT_INTEGRITY_TAG,
|
||||
[key_type_2.as_slice(), b"pubkey".as_ref()],
|
||||
);
|
||||
assert_ne!(t1, t2);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ use diesel_async::{
|
||||
sync_connection_wrapper::SyncConnectionWrapper,
|
||||
};
|
||||
use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations};
|
||||
|
||||
use miette::Diagnostic;
|
||||
use thiserror::Error;
|
||||
use tracing::info;
|
||||
|
||||
@@ -21,21 +21,26 @@ static DB_FILE: &str = "arbiter.sqlite";
|
||||
|
||||
const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations");
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
#[derive(Error, Diagnostic, Debug)]
|
||||
pub enum DatabaseSetupError {
|
||||
#[error("Failed to determine home directory")]
|
||||
#[diagnostic(code(arbiter::db::home_dir))]
|
||||
HomeDir(std::io::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
#[diagnostic(code(arbiter::db::connection))]
|
||||
Connection(diesel::ConnectionError),
|
||||
|
||||
#[error(transparent)]
|
||||
#[diagnostic(code(arbiter::db::concurrency))]
|
||||
ConcurrencySetup(diesel::result::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
#[diagnostic(code(arbiter::db::migration))]
|
||||
Migration(Box<dyn std::error::Error + Send + Sync>),
|
||||
|
||||
#[error(transparent)]
|
||||
#[diagnostic(code(arbiter::db::pool))]
|
||||
Pool(#[from] PoolInitError),
|
||||
}
|
||||
|
||||
|
||||
@@ -242,7 +242,6 @@ pub struct UseragentClient {
|
||||
pub id: i32,
|
||||
pub nonce: i32,
|
||||
pub public_key: Vec<u8>,
|
||||
pub pubkey_integrity_tag: Option<Vec<u8>>,
|
||||
pub created_at: SqliteTimestamp,
|
||||
pub updated_at: SqliteTimestamp,
|
||||
pub key_type: KeyType,
|
||||
|
||||
@@ -178,7 +178,6 @@ diesel::table! {
|
||||
id -> Integer,
|
||||
nonce -> Integer,
|
||||
public_key -> Binary,
|
||||
pubkey_integrity_tag -> Nullable<Binary>,
|
||||
key_type -> Integer,
|
||||
created_at -> Integer,
|
||||
updated_at -> Integer,
|
||||
|
||||
@@ -28,32 +28,39 @@ pub mod policies;
|
||||
mod utils;
|
||||
|
||||
/// Errors that can only occur once the transaction meaning is known (during policy evaluation)
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[derive(Debug, thiserror::Error, miette::Diagnostic)]
|
||||
pub enum PolicyError {
|
||||
#[error("Database error")]
|
||||
Database(#[from] crate::db::DatabaseError),
|
||||
#[error("Transaction violates policy: {0:?}")]
|
||||
#[diagnostic(code(arbiter_server::evm::policy_error::violation))]
|
||||
Violations(Vec<EvalViolation>),
|
||||
#[error("No matching grant found")]
|
||||
#[diagnostic(code(arbiter_server::evm::policy_error::no_matching_grant))]
|
||||
NoMatchingGrant,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[derive(Debug, thiserror::Error, miette::Diagnostic)]
|
||||
pub enum VetError {
|
||||
#[error("Contract creation transactions are not supported")]
|
||||
#[diagnostic(code(arbiter_server::evm::vet_error::contract_creation_unsupported))]
|
||||
ContractCreationNotSupported,
|
||||
#[error("Engine can't classify this transaction")]
|
||||
#[diagnostic(code(arbiter_server::evm::vet_error::unsupported))]
|
||||
UnsupportedTransactionType,
|
||||
#[error("Policy evaluation failed: {1}")]
|
||||
#[diagnostic(code(arbiter_server::evm::vet_error::evaluated))]
|
||||
Evaluated(SpecificMeaning, #[source] PolicyError),
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[derive(Debug, thiserror::Error, miette::Diagnostic)]
|
||||
pub enum AnalyzeError {
|
||||
#[error("Engine doesn't support granting permissions for contract creation")]
|
||||
#[diagnostic(code(arbiter_server::evm::analyze_error::contract_creation_not_supported))]
|
||||
ContractCreationNotSupported,
|
||||
|
||||
#[error("Unsupported transaction type")]
|
||||
#[diagnostic(code(arbiter_server::evm::analyze_error::unsupported_transaction_type))]
|
||||
UnsupportedTransactionType,
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ use diesel::{
|
||||
ExpressionMethods as _, QueryDsl, SelectableHelper, result::QueryResult, sqlite::Sqlite,
|
||||
};
|
||||
use diesel_async::{AsyncConnection, RunQueryDsl};
|
||||
|
||||
use miette::Diagnostic;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{
|
||||
@@ -33,27 +33,33 @@ pub struct EvalContext {
|
||||
pub max_priority_fee_per_gas: u128,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[derive(Debug, Error, Diagnostic)]
|
||||
pub enum EvalViolation {
|
||||
#[error("This grant doesn't allow transactions to the target address {target}")]
|
||||
#[diagnostic(code(arbiter_server::evm::eval_violation::invalid_target))]
|
||||
InvalidTarget { target: Address },
|
||||
|
||||
#[error("Gas limit exceeded for this grant")]
|
||||
#[diagnostic(code(arbiter_server::evm::eval_violation::gas_limit_exceeded))]
|
||||
GasLimitExceeded {
|
||||
max_gas_fee_per_gas: Option<U256>,
|
||||
max_priority_fee_per_gas: Option<U256>,
|
||||
},
|
||||
|
||||
#[error("Rate limit exceeded for this grant")]
|
||||
#[diagnostic(code(arbiter_server::evm::eval_violation::rate_limit_exceeded))]
|
||||
RateLimitExceeded,
|
||||
|
||||
#[error("Transaction exceeds volumetric limits of the grant")]
|
||||
#[diagnostic(code(arbiter_server::evm::eval_violation::volumetric_limit_exceeded))]
|
||||
VolumetricLimitExceeded,
|
||||
|
||||
#[error("Transaction is outside of the grant's validity period")]
|
||||
#[diagnostic(code(arbiter_server::evm::eval_violation::invalid_time))]
|
||||
InvalidTime,
|
||||
|
||||
#[error("Transaction type is not allowed by this grant")]
|
||||
#[diagnostic(code(arbiter_server::evm::eval_violation::invalid_transaction_type))]
|
||||
InvalidTransactionType,
|
||||
}
|
||||
|
||||
|
||||
@@ -140,9 +140,7 @@ impl Receiver<auth::Inbound> for AuthTransportAdapter<'_> {
|
||||
let Some(payload) = auth_request.payload else {
|
||||
let _ = self
|
||||
.bi
|
||||
.send(Err(Status::invalid_argument(
|
||||
"Missing client auth request payload",
|
||||
)))
|
||||
.send(Err(Status::invalid_argument("Missing client auth request payload")))
|
||||
.await;
|
||||
return None;
|
||||
};
|
||||
@@ -172,7 +170,9 @@ impl Receiver<auth::Inbound> for AuthTransportAdapter<'_> {
|
||||
metadata: client_metadata_from_proto(client_info),
|
||||
})
|
||||
}
|
||||
AuthRequestPayload::ChallengeSolution(ProtoAuthChallengeSolution { signature }) => {
|
||||
AuthRequestPayload::ChallengeSolution(ProtoAuthChallengeSolution {
|
||||
signature,
|
||||
}) => {
|
||||
let Ok(signature) = ed25519_dalek::Signature::try_from(signature.as_slice()) else {
|
||||
let _ = self
|
||||
.send_auth_result(ProtoAuthResult::InvalidSignature)
|
||||
|
||||
@@ -34,9 +34,7 @@ pub(super) async fn dispatch(
|
||||
req: proto_evm::Request,
|
||||
) -> Result<ClientResponsePayload, Status> {
|
||||
let Some(payload) = req.payload else {
|
||||
return Err(Status::invalid_argument(
|
||||
"Missing client EVM request payload",
|
||||
));
|
||||
return Err(Status::invalid_argument("Missing client EVM request payload"));
|
||||
};
|
||||
|
||||
match payload {
|
||||
@@ -61,13 +59,13 @@ pub(super) async fn dispatch(
|
||||
))) => EvmSignTransactionResponse {
|
||||
result: Some(vet_error.convert()),
|
||||
},
|
||||
Err(kameo::error::SendError::HandlerError(SignTransactionRpcError::Internal)) => {
|
||||
EvmSignTransactionResponse {
|
||||
result: Some(EvmSignTransactionResult::Error(
|
||||
ProtoEvmError::Internal.into(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
Err(kameo::error::SendError::HandlerError(
|
||||
SignTransactionRpcError::Internal,
|
||||
)) => EvmSignTransactionResponse {
|
||||
result: Some(EvmSignTransactionResult::Error(
|
||||
ProtoEvmError::Internal.into(),
|
||||
)),
|
||||
},
|
||||
Err(err) => {
|
||||
warn!(error = ?err, "Failed to sign EVM transaction");
|
||||
EvmSignTransactionResponse {
|
||||
@@ -80,8 +78,8 @@ pub(super) async fn dispatch(
|
||||
|
||||
Ok(wrap_response(EvmResponsePayload::SignTransaction(response)))
|
||||
}
|
||||
EvmRequestPayload::AnalyzeTransaction(_) => Err(Status::unimplemented(
|
||||
"EVM transaction analysis is not yet implemented",
|
||||
)),
|
||||
EvmRequestPayload::AnalyzeTransaction(_) => {
|
||||
Err(Status::unimplemented("EVM transaction analysis is not yet implemented"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
|
||||
@@ -12,9 +12,11 @@ use kameo::{actor::ActorRef, error::SendError};
|
||||
use tonic::Status;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::actors::{
|
||||
client::session::{ClientSession, Error, HandleQueryVaultState},
|
||||
keyholder::KeyHolderState,
|
||||
use crate::{
|
||||
actors::{
|
||||
client::session::{ClientSession, Error, HandleQueryVaultState},
|
||||
keyholder::KeyHolderState,
|
||||
},
|
||||
};
|
||||
|
||||
pub(super) async fn dispatch(
|
||||
@@ -22,9 +24,7 @@ pub(super) async fn dispatch(
|
||||
req: proto_vault::Request,
|
||||
) -> Result<ClientResponsePayload, Status> {
|
||||
let Some(payload) = req.payload else {
|
||||
return Err(Status::invalid_argument(
|
||||
"Missing client vault request payload",
|
||||
));
|
||||
return Err(Status::invalid_argument("Missing client vault request payload"));
|
||||
};
|
||||
|
||||
match payload {
|
||||
|
||||
@@ -28,8 +28,9 @@ impl TryConvert for RawEvmTransaction {
|
||||
type Error = tonic::Status;
|
||||
|
||||
fn try_convert(self) -> Result<Self::Output, Self::Error> {
|
||||
let tx = TxEip1559::decode(&mut self.0.as_slice())
|
||||
.map_err(|_| tonic::Status::invalid_argument("Invalid EVM transaction format"))?;
|
||||
let tx = TxEip1559::decode(&mut self.0.as_slice()).map_err(|_| {
|
||||
tonic::Status::invalid_argument("Invalid EVM transaction format")
|
||||
})?;
|
||||
Ok(tx)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,9 @@
|
||||
use alloy::primitives::U256;
|
||||
use arbiter_proto::proto::{
|
||||
evm::{
|
||||
EvmError as ProtoEvmError,
|
||||
evm_sign_transaction_response::Result as EvmSignTransactionResult,
|
||||
},
|
||||
evm::{EvmError as ProtoEvmError, evm_sign_transaction_response::Result as EvmSignTransactionResult},
|
||||
shared::evm::{
|
||||
EvalViolation as ProtoEvalViolation, GasLimitExceededViolation, NoMatchingGrantError,
|
||||
PolicyViolationsError, SpecificMeaning as ProtoSpecificMeaning,
|
||||
EvalViolation as ProtoEvalViolation, GasLimitExceededViolation,
|
||||
NoMatchingGrantError, PolicyViolationsError, SpecificMeaning as ProtoSpecificMeaning,
|
||||
TokenInfo as ProtoTokenInfo, TransactionEvalError as ProtoTransactionEvalError,
|
||||
eval_violation::Kind as ProtoEvalViolationKind,
|
||||
specific_meaning::Meaning as ProtoSpecificMeaningKind,
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
use arbiter_proto::{
|
||||
proto::user_agent::{
|
||||
UserAgentRequest, UserAgentResponse,
|
||||
user_agent_request::Payload as UserAgentRequestPayload,
|
||||
user_agent_response::Payload as UserAgentResponsePayload,
|
||||
proto::{
|
||||
user_agent::{
|
||||
UserAgentRequest, UserAgentResponse,
|
||||
user_agent_request::Payload as UserAgentRequestPayload,
|
||||
user_agent_response::Payload as UserAgentResponsePayload,
|
||||
},
|
||||
},
|
||||
transport::{Error as TransportError, Receiver, Sender, grpc::GrpcBi},
|
||||
};
|
||||
@@ -17,7 +19,6 @@ use crate::{
|
||||
actors::user_agent::{OutOfBand, UserAgentConnection, UserAgentSession},
|
||||
grpc::request_tracker::RequestTracker,
|
||||
};
|
||||
|
||||
mod auth;
|
||||
mod evm;
|
||||
mod inbound;
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
use arbiter_proto::{
|
||||
proto::user_agent::{
|
||||
UserAgentRequest, UserAgentResponse,
|
||||
auth::{
|
||||
UserAgentRequest, UserAgentResponse, auth::{
|
||||
self as proto_auth, AuthChallenge as ProtoAuthChallenge,
|
||||
AuthChallengeRequest as ProtoAuthChallengeRequest,
|
||||
AuthChallengeSolution as ProtoAuthChallengeSolution, AuthResult as ProtoAuthResult,
|
||||
KeyType as ProtoKeyType, request::Payload as AuthRequestPayload,
|
||||
response::Payload as AuthResponsePayload,
|
||||
},
|
||||
user_agent_request::Payload as UserAgentRequestPayload,
|
||||
}, user_agent_request::Payload as UserAgentRequestPayload,
|
||||
user_agent_response::Payload as UserAgentResponsePayload,
|
||||
},
|
||||
transport::{Bi, Error as TransportError, Receiver, Sender, grpc::GrpcBi},
|
||||
@@ -65,9 +63,7 @@ impl Sender<Result<auth::Outbound, auth::Error>> for AuthTransportAdapter<'_> {
|
||||
Ok(Outbound::AuthChallenge { nonce }) => {
|
||||
AuthResponsePayload::Challenge(ProtoAuthChallenge { nonce })
|
||||
}
|
||||
Ok(Outbound::AuthSuccess) => {
|
||||
AuthResponsePayload::Result(ProtoAuthResult::Success.into())
|
||||
}
|
||||
Ok(Outbound::AuthSuccess) => AuthResponsePayload::Result(ProtoAuthResult::Success.into()),
|
||||
Err(Error::UnregisteredPublicKey) => {
|
||||
AuthResponsePayload::Result(ProtoAuthResult::InvalidKey.into())
|
||||
}
|
||||
@@ -175,9 +171,9 @@ impl Receiver<auth::Inbound> for AuthTransportAdapter<'_> {
|
||||
bootstrap_token,
|
||||
})
|
||||
}
|
||||
AuthRequestPayload::ChallengeSolution(ProtoAuthChallengeSolution { signature }) => {
|
||||
Some(auth::Inbound::AuthChallengeSolution { signature })
|
||||
}
|
||||
AuthRequestPayload::ChallengeSolution(ProtoAuthChallengeSolution {
|
||||
signature,
|
||||
}) => Some(auth::Inbound::AuthChallengeSolution { signature }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@ use arbiter_proto::proto::{
|
||||
EvmError as ProtoEvmError, EvmGrantCreateRequest, EvmGrantCreateResponse,
|
||||
EvmGrantDeleteRequest, EvmGrantDeleteResponse, EvmGrantList, EvmGrantListResponse,
|
||||
EvmSignTransactionResponse, GrantEntry, WalletCreateResponse, WalletEntry, WalletList,
|
||||
WalletListResponse, evm_grant_create_response::Result as EvmGrantCreateResult,
|
||||
WalletListResponse,
|
||||
evm_grant_create_response::Result as EvmGrantCreateResult,
|
||||
evm_grant_delete_response::Result as EvmGrantDeleteResult,
|
||||
evm_grant_list_response::Result as EvmGrantListResult,
|
||||
evm_sign_transaction_response::Result as EvmSignTransactionResult,
|
||||
@@ -164,12 +165,7 @@ async fn handle_grant_delete(
|
||||
actor: &ActorRef<UserAgentSession>,
|
||||
req: EvmGrantDeleteRequest,
|
||||
) -> Result<Option<UserAgentResponsePayload>, Status> {
|
||||
let result = match actor
|
||||
.ask(HandleGrantDelete {
|
||||
grant_id: req.grant_id,
|
||||
})
|
||||
.await
|
||||
{
|
||||
let result = match actor.ask(HandleGrantDelete { grant_id: req.grant_id }).await {
|
||||
Ok(()) => EvmGrantDeleteResult::Ok(()),
|
||||
Err(err) => {
|
||||
warn!(error = ?err, "Failed to delete EVM grant");
|
||||
@@ -206,18 +202,18 @@ async fn handle_sign_transaction(
|
||||
signature.as_bytes().to_vec(),
|
||||
)),
|
||||
},
|
||||
Err(kameo::error::SendError::HandlerError(SessionSignTransactionError::Vet(vet_error))) => {
|
||||
EvmSignTransactionResponse {
|
||||
result: Some(vet_error.convert()),
|
||||
}
|
||||
}
|
||||
Err(kameo::error::SendError::HandlerError(SessionSignTransactionError::Internal)) => {
|
||||
EvmSignTransactionResponse {
|
||||
result: Some(EvmSignTransactionResult::Error(
|
||||
ProtoEvmError::Internal.into(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
Err(kameo::error::SendError::HandlerError(
|
||||
SessionSignTransactionError::Vet(vet_error),
|
||||
)) => EvmSignTransactionResponse {
|
||||
result: Some(vet_error.convert()),
|
||||
},
|
||||
Err(kameo::error::SendError::HandlerError(
|
||||
SessionSignTransactionError::Internal,
|
||||
)) => EvmSignTransactionResponse {
|
||||
result: Some(EvmSignTransactionResult::Error(
|
||||
ProtoEvmError::Internal.into(),
|
||||
)),
|
||||
},
|
||||
Err(err) => {
|
||||
warn!(error = ?err, "Failed to sign EVM transaction");
|
||||
EvmSignTransactionResponse {
|
||||
@@ -228,7 +224,7 @@ async fn handle_sign_transaction(
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Some(wrap_evm_response(
|
||||
EvmResponsePayload::SignTransaction(response),
|
||||
)))
|
||||
Ok(Some(wrap_evm_response(EvmResponsePayload::SignTransaction(
|
||||
response,
|
||||
))))
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@ use arbiter_proto::proto::{
|
||||
TransactionRateLimit as ProtoTransactionRateLimit, VolumeRateLimit as ProtoVolumeRateLimit,
|
||||
specific_grant::Grant as ProtoSpecificGrantType,
|
||||
},
|
||||
user_agent::sdk_client::{WalletAccess, WalletAccessEntry as ProtoSdkClientWalletAccess},
|
||||
user_agent::sdk_client::{
|
||||
WalletAccess, WalletAccessEntry as ProtoSdkClientWalletAccess,
|
||||
},
|
||||
};
|
||||
use chrono::{DateTime, Utc};
|
||||
use prost_types::Timestamp as ProtoTimestamp;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use arbiter_proto::proto::{
|
||||
shared::ClientInfo as ProtoClientMetadata,
|
||||
user_agent::{
|
||||
sdk_client::{
|
||||
self as proto_sdk_client, ConnectionCancel as ProtoSdkClientConnectionCancel,
|
||||
@@ -14,6 +13,7 @@ use arbiter_proto::proto::{
|
||||
},
|
||||
user_agent_response::Payload as UserAgentResponsePayload,
|
||||
},
|
||||
shared::ClientInfo as ProtoClientMetadata,
|
||||
};
|
||||
use kameo::actor::ActorRef;
|
||||
use tonic::Status;
|
||||
@@ -62,22 +62,18 @@ pub(super) async fn dispatch(
|
||||
req: proto_sdk_client::Request,
|
||||
) -> Result<Option<UserAgentResponsePayload>, Status> {
|
||||
let Some(payload) = req.payload else {
|
||||
return Err(Status::invalid_argument(
|
||||
"Missing SDK client request payload",
|
||||
));
|
||||
return Err(Status::invalid_argument("Missing SDK client request payload"));
|
||||
};
|
||||
|
||||
match payload {
|
||||
SdkClientRequestPayload::ConnectionResponse(resp) => {
|
||||
handle_connection_response(actor, resp).await
|
||||
}
|
||||
SdkClientRequestPayload::Revoke(_) => Err(Status::unimplemented(
|
||||
"SdkClientRevoke is not yet implemented",
|
||||
)),
|
||||
SdkClientRequestPayload::List(_) => handle_list(actor).await,
|
||||
SdkClientRequestPayload::GrantWalletAccess(req) => {
|
||||
handle_grant_wallet_access(actor, req).await
|
||||
SdkClientRequestPayload::Revoke(_) => {
|
||||
Err(Status::unimplemented("SdkClientRevoke is not yet implemented"))
|
||||
}
|
||||
SdkClientRequestPayload::List(_) => handle_list(actor).await,
|
||||
SdkClientRequestPayload::GrantWalletAccess(req) => handle_grant_wallet_access(actor, req).await,
|
||||
SdkClientRequestPayload::RevokeWalletAccess(req) => {
|
||||
handle_revoke_wallet_access(actor, req).await
|
||||
}
|
||||
@@ -132,11 +128,11 @@ async fn handle_list(
|
||||
ProtoSdkClientListResult::Error(ProtoSdkClientError::Internal.into())
|
||||
}
|
||||
};
|
||||
Ok(Some(wrap_sdk_client_response(
|
||||
SdkClientResponsePayload::List(ProtoSdkClientListResponse {
|
||||
Ok(Some(wrap_sdk_client_response(SdkClientResponsePayload::List(
|
||||
ProtoSdkClientListResponse {
|
||||
result: Some(result),
|
||||
}),
|
||||
)))
|
||||
},
|
||||
))))
|
||||
}
|
||||
|
||||
async fn handle_grant_wallet_access(
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use arbiter_proto::proto::shared::VaultState as ProtoVaultState;
|
||||
use arbiter_proto::proto::user_agent::{
|
||||
user_agent_response::Payload as UserAgentResponsePayload,
|
||||
vault::{
|
||||
@@ -12,21 +11,25 @@ use arbiter_proto::proto::user_agent::{
|
||||
unseal::{
|
||||
self as proto_unseal, UnsealEncryptedKey as ProtoUnsealEncryptedKey,
|
||||
UnsealResult as ProtoUnsealResult, UnsealStart,
|
||||
request::Payload as UnsealRequestPayload, response::Payload as UnsealResponsePayload,
|
||||
request::Payload as UnsealRequestPayload,
|
||||
response::Payload as UnsealResponsePayload,
|
||||
},
|
||||
},
|
||||
};
|
||||
use arbiter_proto::proto::shared::VaultState as ProtoVaultState;
|
||||
use kameo::{actor::ActorRef, error::SendError};
|
||||
use tonic::Status;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::actors::{
|
||||
keyholder::KeyHolderState,
|
||||
user_agent::{
|
||||
UserAgentSession,
|
||||
session::connection::{
|
||||
BootstrapError, HandleBootstrapEncryptedKey, HandleQueryVaultState,
|
||||
HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError,
|
||||
use crate::{
|
||||
actors::{
|
||||
keyholder::KeyHolderState,
|
||||
user_agent::{
|
||||
UserAgentSession,
|
||||
session::connection::{
|
||||
BootstrapError, HandleBootstrapEncryptedKey, HandleQueryVaultState,
|
||||
HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -148,9 +151,7 @@ async fn handle_bootstrap_encrypted_key(
|
||||
.await
|
||||
{
|
||||
Ok(()) => ProtoBootstrapResult::Success,
|
||||
Err(SendError::HandlerError(BootstrapError::InvalidKey)) => {
|
||||
ProtoBootstrapResult::InvalidKey
|
||||
}
|
||||
Err(SendError::HandlerError(BootstrapError::InvalidKey)) => ProtoBootstrapResult::InvalidKey,
|
||||
Err(SendError::HandlerError(BootstrapError::AlreadyBootstrapped)) => {
|
||||
ProtoBootstrapResult::AlreadyBootstrapped
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ use crate::context::ServerContext;
|
||||
|
||||
pub mod actors;
|
||||
pub mod context;
|
||||
pub mod crypto;
|
||||
pub mod db;
|
||||
pub mod evm;
|
||||
pub mod grpc;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
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 miette::miette;
|
||||
use rustls::crypto::aws_lc_rs;
|
||||
use tonic::transport::{Identity, ServerTlsConfig};
|
||||
use tracing::info;
|
||||
@@ -10,7 +10,7 @@ use tracing::info;
|
||||
const PORT: u16 = 50051;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
async fn main() -> miette::Result<()> {
|
||||
aws_lc_rs::default_provider().install_default().unwrap();
|
||||
|
||||
tracing_subscriber::fmt()
|
||||
@@ -46,11 +46,11 @@ async fn main() -> anyhow::Result<()> {
|
||||
|
||||
tonic::transport::Server::builder()
|
||||
.tls_config(tls)
|
||||
.map_err(|err| anyhow!("Failed to setup TLS: {err}"))?
|
||||
.map_err(|err| miette!("Faild to setup TLS: {err}"))?
|
||||
.add_service(ArbiterServiceServer::new(Server::new(context)))
|
||||
.serve(addr)
|
||||
.await
|
||||
.map_err(|e| anyhow!("gRPC server error: {e}"))?;
|
||||
.map_err(|e| miette::miette!("gRPC server error: {e}"))?;
|
||||
|
||||
unreachable!("gRPC server should run indefinitely");
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use arbiter_server::{
|
||||
actors::keyholder::{Error, KeyHolder},
|
||||
crypto::encryption::v1::{Nonce, ROOT_KEY_TAG},
|
||||
db::{self, models, schema},
|
||||
safe_cell::{SafeCell, SafeCellHandle as _},
|
||||
};
|
||||
@@ -26,10 +25,16 @@ async fn test_bootstrap() {
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(row.schema_version, 1);
|
||||
assert_eq!(row.tag, ROOT_KEY_TAG);
|
||||
assert_eq!(
|
||||
row.tag,
|
||||
arbiter_server::actors::keyholder::encryption::v1::ROOT_KEY_TAG
|
||||
);
|
||||
assert!(!row.ciphertext.is_empty());
|
||||
assert!(!row.salt.is_empty());
|
||||
assert_eq!(row.data_encryption_nonce, Nonce::default().to_vec());
|
||||
assert_eq!(
|
||||
row.data_encryption_nonce,
|
||||
arbiter_server::actors::keyholder::encryption::v1::Nonce::default().to_vec()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use arbiter_server::{
|
||||
actors::keyholder::Error,
|
||||
crypto::encryption::v1::Nonce,
|
||||
actors::keyholder::{Error, encryption::v1},
|
||||
db::{self, models, schema},
|
||||
safe_cell::{SafeCell, SafeCellHandle as _},
|
||||
};
|
||||
@@ -103,7 +102,7 @@ async fn test_nonce_never_reused() {
|
||||
assert_eq!(nonces.len(), unique.len(), "all nonces must be unique");
|
||||
|
||||
for (i, row) in rows.iter().enumerate() {
|
||||
let mut expected = Nonce::default();
|
||||
let mut expected = v1::Nonce::default();
|
||||
for _ in 0..=i {
|
||||
expected.increment();
|
||||
}
|
||||
|
||||
@@ -3,11 +3,9 @@ use arbiter_server::{
|
||||
actors::{
|
||||
GlobalActors,
|
||||
bootstrap::GetToken,
|
||||
keyholder::Bootstrap,
|
||||
user_agent::{AuthPublicKey, UserAgentConnection, auth},
|
||||
},
|
||||
db::{self, schema},
|
||||
safe_cell::{SafeCell, SafeCellHandle as _},
|
||||
};
|
||||
use diesel::{ExpressionMethods as _, QueryDsl, insert_into};
|
||||
use diesel_async::RunQueryDsl;
|
||||
@@ -85,6 +83,7 @@ pub async fn test_bootstrap_invalid_token_auth() {
|
||||
Err(auth::Error::InvalidBootstrapToken)
|
||||
));
|
||||
|
||||
// Verify no key was registered
|
||||
let mut conn = db.get().await.unwrap();
|
||||
let count: i64 = schema::useragent_client::table
|
||||
.count()
|
||||
@@ -103,6 +102,7 @@ pub async fn test_challenge_auth() {
|
||||
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
||||
let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec();
|
||||
|
||||
// Pre-register key with key_type
|
||||
{
|
||||
let mut conn = db.get().await.unwrap();
|
||||
insert_into(schema::useragent_client::table)
|
||||
@@ -122,6 +122,7 @@ pub async fn test_challenge_auth() {
|
||||
auth::authenticate(&mut props, server_transport).await
|
||||
});
|
||||
|
||||
// Send challenge request
|
||||
test_transport
|
||||
.send(auth::Inbound::AuthChallengeRequest {
|
||||
pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()),
|
||||
@@ -130,6 +131,7 @@ pub async fn test_challenge_auth() {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Read the challenge response
|
||||
let response = test_transport
|
||||
.recv()
|
||||
.await
|
||||
@@ -164,57 +166,6 @@ pub async fn test_challenge_auth() {
|
||||
task.await.unwrap().unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[test_log::test]
|
||||
pub 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();
|
||||
|
||||
actors
|
||||
.key_holder
|
||||
.ask(Bootstrap {
|
||||
seal_key_raw: SafeCell::new(b"test-seal-key".to_vec()),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
||||
let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec();
|
||||
|
||||
{
|
||||
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),
|
||||
schema::useragent_client::pubkey_integrity_tag.eq(Some(vec![0u8; 32])),
|
||||
))
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let (server_transport, mut test_transport) = ChannelTransport::new();
|
||||
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, server_transport).await
|
||||
});
|
||||
|
||||
test_transport
|
||||
.send(auth::Inbound::AuthChallengeRequest {
|
||||
pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()),
|
||||
bootstrap_token: None,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(matches!(
|
||||
task.await.unwrap(),
|
||||
Err(auth::Error::InvalidChallengeSolution)
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[test_log::test]
|
||||
pub async fn test_challenge_auth_rejects_invalid_signature() {
|
||||
@@ -224,6 +175,7 @@ pub async fn test_challenge_auth_rejects_invalid_signature() {
|
||||
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
||||
let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec();
|
||||
|
||||
// Pre-register key with key_type
|
||||
{
|
||||
let mut conn = db.get().await.unwrap();
|
||||
insert_into(schema::useragent_client::table)
|
||||
@@ -263,6 +215,7 @@ pub async fn test_challenge_auth_rejects_invalid_signature() {
|
||||
Err(err) => panic!("Expected Ok response, got Err({err:?})"),
|
||||
};
|
||||
|
||||
// Sign a different challenge value so signature format is valid but verification must fail.
|
||||
let wrong_challenge = arbiter_proto::format_challenge(challenge + 1, &pubkey_bytes);
|
||||
let signature = new_key.sign(&wrong_challenge);
|
||||
|
||||
@@ -273,10 +226,8 @@ pub async fn test_challenge_auth_rejects_invalid_signature() {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let expected_err = task.await.unwrap();
|
||||
println!("Received expected error: {expected_err:#?}");
|
||||
assert!(matches!(
|
||||
expected_err,
|
||||
task.await.unwrap(),
|
||||
Err(auth::Error::InvalidChallengeSolution)
|
||||
));
|
||||
}
|
||||
|
||||
@@ -2,17 +2,14 @@ use arbiter_server::{
|
||||
actors::{
|
||||
GlobalActors,
|
||||
keyholder::{Bootstrap, Seal},
|
||||
user_agent::{
|
||||
UserAgentSession,
|
||||
session::connection::{HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError},
|
||||
},
|
||||
user_agent::{UserAgentSession, session::connection::{
|
||||
HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError,
|
||||
}},
|
||||
},
|
||||
db,
|
||||
safe_cell::{SafeCell, SafeCellHandle as _},
|
||||
};
|
||||
use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit};
|
||||
use diesel::{ExpressionMethods as _, QueryDsl as _, insert_into};
|
||||
use diesel_async::RunQueryDsl;
|
||||
use kameo::actor::Spawn as _;
|
||||
use x25519_dalek::{EphemeralSecret, PublicKey};
|
||||
|
||||
@@ -152,42 +149,3 @@ pub async fn test_unseal_retry_after_invalid_key() {
|
||||
assert!(matches!(response, Ok(())));
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[test_log::test]
|
||||
pub async fn test_unseal_backfills_missing_pubkey_integrity_tags() {
|
||||
let seal_key = b"test-seal-key";
|
||||
let (db, user_agent) = setup_sealed_user_agent(seal_key).await;
|
||||
|
||||
{
|
||||
let mut conn = db.get().await.unwrap();
|
||||
insert_into(arbiter_server::db::schema::useragent_client::table)
|
||||
.values((
|
||||
arbiter_server::db::schema::useragent_client::public_key
|
||||
.eq(vec![1u8, 2u8, 3u8, 4u8]),
|
||||
arbiter_server::db::schema::useragent_client::key_type.eq(1i32),
|
||||
arbiter_server::db::schema::useragent_client::pubkey_integrity_tag
|
||||
.eq(Option::<Vec<u8>>::None),
|
||||
))
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let encrypted_key = client_dh_encrypt(&user_agent, seal_key).await;
|
||||
let response = user_agent.ask(encrypted_key).await;
|
||||
assert!(matches!(response, Ok(())));
|
||||
|
||||
{
|
||||
let mut conn = db.get().await.unwrap();
|
||||
let tags: Vec<Option<Vec<u8>>> = arbiter_server::db::schema::useragent_client::table
|
||||
.select(arbiter_server::db::schema::useragent_client::pubkey_integrity_tag)
|
||||
.load(&mut conn)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(
|
||||
tags.iter()
|
||||
.all(|tag| matches!(tag, Some(v) if v.len() == 32))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ class GrantCard extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
// Enrichment lookups — each watch scopes rebuilds to this card only
|
||||
final walletAccesses =
|
||||
ref.watch(walletAccessListProvider).asData?.value ?? const [];
|
||||
final wallets = ref.watch(evmProvider).asData?.value ?? const [];
|
||||
@@ -43,6 +44,7 @@ class GrantCard extends ConsumerWidget {
|
||||
final theme = Theme.of(context);
|
||||
final muted = Palette.ink.withValues(alpha: 0.62);
|
||||
|
||||
// Resolve wallet_access_id → wallet address + client name
|
||||
final accessById = <int, ua_sdk.WalletAccessEntry>{
|
||||
for (final a in walletAccesses) a.id: a,
|
||||
};
|
||||
@@ -92,6 +94,7 @@ class GrantCard extends ConsumerWidget {
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Accent strip
|
||||
Container(
|
||||
width: 0.8.w,
|
||||
decoration: BoxDecoration(
|
||||
@@ -101,6 +104,7 @@ class GrantCard extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
// Card body
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
@@ -110,6 +114,7 @@ class GrantCard extends ConsumerWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Row 1: type badge · chain · spacer · revoke button
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
@@ -179,6 +184,7 @@ class GrantCard extends ConsumerWidget {
|
||||
],
|
||||
),
|
||||
SizedBox(height: 0.8.h),
|
||||
// Row 2: wallet address · client name
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
|
||||
Reference in New Issue
Block a user