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 rust
|
||||||
- mise install protoc
|
- mise install protoc
|
||||||
- mise install cargo:cargo-nextest
|
- 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
|
## Cryptography
|
||||||
|
|
||||||
### Authentication
|
### Authentication
|
||||||
- **Client protocol:** ed25519
|
- **Signature scheme:** 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.
|
|
||||||
|
|
||||||
### Encryption at Rest
|
### Encryption at Rest
|
||||||
- **Scheme:** Symmetric AEAD — currently **XChaCha20-Poly1305**
|
- **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:
|
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.
|
- **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.
|
`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.
|
- **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.
|
- **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.
|
- **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.
|
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
|
- **Current:** Using the `memsafe` crate as an interim solution
|
||||||
- **Planned:** Additional backends can be introduced behind the same abstraction, including a custom implementation based on `mlock` (Unix) and `VirtualProtect` (Windows)
|
- **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"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"alloy",
|
"alloy",
|
||||||
"anyhow",
|
|
||||||
"arbiter-proto",
|
"arbiter-proto",
|
||||||
"arbiter-tokens-registry",
|
"arbiter-tokens-registry",
|
||||||
"argon2",
|
"argon2",
|
||||||
@@ -738,11 +737,11 @@ dependencies = [
|
|||||||
"ed25519-dalek",
|
"ed25519-dalek",
|
||||||
"fatality",
|
"fatality",
|
||||||
"futures",
|
"futures",
|
||||||
"hmac",
|
|
||||||
"insta",
|
"insta",
|
||||||
"k256",
|
"k256",
|
||||||
"kameo",
|
"kameo",
|
||||||
"memsafe",
|
"memsafe",
|
||||||
|
"miette",
|
||||||
"pem",
|
"pem",
|
||||||
"prost-types",
|
"prost-types",
|
||||||
"rand 0.10.0",
|
"rand 0.10.0",
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ chrono = { version = "0.4.44", features = ["serde"] }
|
|||||||
rand = "0.10.0"
|
rand = "0.10.0"
|
||||||
rustls = { version = "0.23.37", features = ["aws-lc-rs"] }
|
rustls = { version = "0.23.37", features = ["aws-lc-rs"] }
|
||||||
smlang = "0.8.0"
|
smlang = "0.8.0"
|
||||||
|
miette = { version = "7.6.0", features = ["fancy", "serde"] }
|
||||||
thiserror = "2.0.18"
|
thiserror = "2.0.18"
|
||||||
async-trait = "0.1.89"
|
async-trait = "0.1.89"
|
||||||
futures = "0.3.32"
|
futures = "0.3.32"
|
||||||
@@ -42,4 +43,3 @@ k256 = { version = "0.13.4", features = ["ecdsa", "pkcs8"] }
|
|||||||
rsa = { version = "0.9", features = ["sha2"] }
|
rsa = { version = "0.9", features = ["sha2"] }
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
spki = "0.7"
|
spki = "0.7"
|
||||||
miette = { version = "7.6.0", features = ["fancy", "serde"] }
|
|
||||||
@@ -122,7 +122,9 @@ async fn receive_auth_confirmation(
|
|||||||
.await
|
.await
|
||||||
.map_err(|_| AuthError::UnexpectedAuthResponse)?;
|
.map_err(|_| AuthError::UnexpectedAuthResponse)?;
|
||||||
|
|
||||||
let payload = response.payload.ok_or(AuthError::UnexpectedAuthResponse)?;
|
let payload = response
|
||||||
|
.payload
|
||||||
|
.ok_or(AuthError::UnexpectedAuthResponse)?;
|
||||||
match payload {
|
match payload {
|
||||||
ClientResponsePayload::Auth(response) => match response.payload {
|
ClientResponsePayload::Auth(response) => match response.payload {
|
||||||
Some(AuthResponsePayload::Result(result))
|
Some(AuthResponsePayload::Result(result))
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
use std::io::{self, Write};
|
use std::io::{self, Write};
|
||||||
|
|
||||||
use arbiter_client::ArbiterClient;
|
use arbiter_client::ArbiterClient;
|
||||||
@@ -21,6 +22,8 @@ async fn main() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
let url = match ArbiterUrl::try_from(input) {
|
let url = match ArbiterUrl::try_from(input) {
|
||||||
Ok(url) => url,
|
Ok(url) => url,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -29,7 +32,7 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
println!("{:#?}", url);
|
println!("{:#?}", url);
|
||||||
|
|
||||||
let metadata = ClientMetadata {
|
let metadata = ClientMetadata {
|
||||||
name: "arbiter-client test_connect".to_string(),
|
name: "arbiter-client test_connect".to_string(),
|
||||||
@@ -41,4 +44,4 @@ async fn main() {
|
|||||||
Ok(_) => println!("Connected and authenticated successfully."),
|
Ok(_) => println!("Connected and authenticated successfully."),
|
||||||
Err(err) => eprintln!("Failed to connect: {:#?}", err),
|
Err(err) => eprintln!("Failed to connect: {:#?}", err),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,16 +1,11 @@
|
|||||||
use arbiter_proto::{
|
use arbiter_proto::{ClientMetadata, proto::arbiter_service_client::ArbiterServiceClient, url::ArbiterUrl};
|
||||||
ClientMetadata, proto::arbiter_service_client::ArbiterServiceClient, url::ArbiterUrl,
|
|
||||||
};
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::{Mutex, mpsc};
|
use tokio::sync::{Mutex, mpsc};
|
||||||
use tokio_stream::wrappers::ReceiverStream;
|
use tokio_stream::wrappers::ReceiverStream;
|
||||||
use tonic::transport::ClientTlsConfig;
|
use tonic::transport::ClientTlsConfig;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
StorageError,
|
StorageError, auth::{AuthError, authenticate}, storage::{FileSigningKeyStorage, SigningKeyStorage}, transport::{BUFFER_LENGTH, ClientTransport}
|
||||||
auth::{AuthError, authenticate},
|
|
||||||
storage::{FileSigningKeyStorage, SigningKeyStorage},
|
|
||||||
transport::{BUFFER_LENGTH, ClientTransport},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "evm")]
|
#[cfg(feature = "evm")]
|
||||||
@@ -35,6 +30,7 @@ pub enum Error {
|
|||||||
|
|
||||||
#[error("Storage error")]
|
#[error("Storage error")]
|
||||||
Storage(#[from] StorageError),
|
Storage(#[from] StorageError),
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ArbiterClient {
|
pub struct ArbiterClient {
|
||||||
@@ -65,11 +61,10 @@ impl ArbiterClient {
|
|||||||
let anchor = webpki::anchor_from_trusted_cert(&url.ca_cert)?.to_owned();
|
let anchor = webpki::anchor_from_trusted_cert(&url.ca_cert)?.to_owned();
|
||||||
let tls = ClientTlsConfig::new().trust_anchor(anchor);
|
let tls = ClientTlsConfig::new().trust_anchor(anchor);
|
||||||
|
|
||||||
let channel =
|
let channel = tonic::transport::Channel::from_shared(format!("https://{}:{}", url.host, url.port))?
|
||||||
tonic::transport::Channel::from_shared(format!("https://{}:{}", url.host, url.port))?
|
.tls_config(tls)?
|
||||||
.tls_config(tls)?
|
.connect()
|
||||||
.connect()
|
.await?;
|
||||||
.await?;
|
|
||||||
|
|
||||||
let mut client = ArbiterServiceClient::new(channel);
|
let mut client = ArbiterServiceClient::new(channel);
|
||||||
let (tx, rx) = mpsc::channel(BUFFER_LENGTH);
|
let (tx, rx) = mpsc::channel(BUFFER_LENGTH);
|
||||||
|
|||||||
@@ -9,4 +9,4 @@ pub use client::{ArbiterClient, Error};
|
|||||||
pub use storage::{FileSigningKeyStorage, SigningKeyStorage, StorageError};
|
pub use storage::{FileSigningKeyStorage, SigningKeyStorage, StorageError};
|
||||||
|
|
||||||
#[cfg(feature = "evm")]
|
#[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::{
|
use arbiter_proto::proto::{
|
||||||
client::{
|
client::{
|
||||||
ClientRequest,
|
ClientRequest, client_request::Payload as ClientRequestPayload,
|
||||||
client_request::Payload as ClientRequestPayload,
|
|
||||||
client_response::Payload as ClientResponsePayload,
|
client_response::Payload as ClientResponsePayload,
|
||||||
evm::{
|
|
||||||
self as proto_evm, request::Payload as EvmRequestPayload,
|
|
||||||
response::Payload as EvmResponsePayload,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
evm::{
|
evm::evm_sign_transaction_response::Result as EvmSignTransactionResult,
|
||||||
EvmSignTransactionRequest,
|
|
||||||
evm_sign_transaction_response::Result as EvmSignTransactionResult,
|
|
||||||
},
|
|
||||||
shared::evm::TransactionEvalError,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::transport::{ClientTransport, next_request_id};
|
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 {
|
pub struct ArbiterEvmWallet {
|
||||||
transport: Arc<Mutex<ClientTransport>>,
|
transport: Arc<Mutex<ClientTransport>>,
|
||||||
address: Address,
|
address: Address,
|
||||||
@@ -130,14 +96,12 @@ impl TxSigner<Signature> for ArbiterEvmWallet {
|
|||||||
transport
|
transport
|
||||||
.send(ClientRequest {
|
.send(ClientRequest {
|
||||||
request_id,
|
request_id,
|
||||||
payload: Some(ClientRequestPayload::Evm(proto_evm::Request {
|
payload: Some(ClientRequestPayload::EvmSignTransaction(
|
||||||
payload: Some(EvmRequestPayload::SignTransaction(
|
arbiter_proto::proto::evm::EvmSignTransactionRequest {
|
||||||
EvmSignTransactionRequest {
|
wallet_address: self.address.to_vec(),
|
||||||
wallet_address: self.address.to_vec(),
|
rlp_transaction,
|
||||||
rlp_transaction,
|
},
|
||||||
},
|
)),
|
||||||
)),
|
|
||||||
})),
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|_| Error::other("failed to send evm sign transaction request"))?;
|
.map_err(|_| Error::other("failed to send evm sign transaction request"))?;
|
||||||
@@ -157,21 +121,12 @@ impl TxSigner<Signature> for ArbiterEvmWallet {
|
|||||||
.payload
|
.payload
|
||||||
.ok_or_else(|| Error::other("missing evm sign transaction response payload"))?;
|
.ok_or_else(|| Error::other("missing evm sign transaction response payload"))?;
|
||||||
|
|
||||||
let ClientResponsePayload::Evm(proto_evm::Response {
|
let ClientResponsePayload::EvmSignTransaction(response) = payload else {
|
||||||
payload: Some(payload),
|
|
||||||
}) = payload
|
|
||||||
else {
|
|
||||||
return Err(Error::other(
|
return Err(Error::other(
|
||||||
"unexpected response payload for evm sign transaction request",
|
"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
|
let result = response
|
||||||
.result
|
.result
|
||||||
.ok_or_else(|| Error::other("missing evm sign transaction 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())
|
Signature::try_from(signature.as_slice())
|
||||||
.map_err(|_| Error::other("invalid signature returned by server"))
|
.map_err(|_| Error::other("invalid signature returned by server"))
|
||||||
}
|
}
|
||||||
EvmSignTransactionResult::EvalError(eval_error) => Err(Error::other(
|
EvmSignTransactionResult::EvalError(eval_error) => Err(Error::other(format!(
|
||||||
ArbiterEvmSignTransactionError::PolicyEval(eval_error),
|
"transaction rejected by policy: {eval_error:?}"
|
||||||
)),
|
))),
|
||||||
EvmSignTransactionResult::Error(code) => Err(Error::other(format!(
|
EvmSignTransactionResult::Error(code) => Err(Error::other(format!(
|
||||||
"server failed to sign transaction with error code {code}"
|
"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 CERT_QUERY_KEY: &str = "cert";
|
||||||
const BOOTSTRAP_TOKEN_QUERY_KEY: &str = "bootstrap_token";
|
const BOOTSTRAP_TOKEN_QUERY_KEY: &str = "bootstrap_token";
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ArbiterUrl {
|
pub struct ArbiterUrl {
|
||||||
pub host: String,
|
pub host: String,
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ tonic.features = ["tls-aws-lc"]
|
|||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
rustls.workspace = true
|
rustls.workspace = true
|
||||||
smlang.workspace = true
|
smlang.workspace = true
|
||||||
|
miette.workspace = true
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
fatality = "0.1.1"
|
fatality = "0.1.1"
|
||||||
diesel_migrations = { version = "2.3.1", features = ["sqlite"] }
|
diesel_migrations = { version = "2.3.1", features = ["sqlite"] }
|
||||||
@@ -48,12 +49,10 @@ pem = "3.0.6"
|
|||||||
k256.workspace = true
|
k256.workspace = true
|
||||||
rsa.workspace = true
|
rsa.workspace = true
|
||||||
sha2.workspace = true
|
sha2.workspace = true
|
||||||
hmac = "0.12"
|
|
||||||
spki.workspace = true
|
spki.workspace = true
|
||||||
alloy.workspace = true
|
alloy.workspace = true
|
||||||
prost-types.workspace = true
|
prost-types.workspace = true
|
||||||
arbiter-tokens-registry.path = "../arbiter-tokens-registry"
|
arbiter-tokens-registry.path = "../arbiter-tokens-registry"
|
||||||
anyhow = "1.0.102"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
insta = "1.46.3"
|
insta = "1.46.3"
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ create table if not exists useragent_client (
|
|||||||
id integer not null primary key,
|
id integer not null primary key,
|
||||||
nonce integer not null default(1), -- used for auth challenge
|
nonce integer not null default(1), -- used for auth challenge
|
||||||
public_key blob not null,
|
public_key blob not null,
|
||||||
pubkey_integrity_tag blob,
|
|
||||||
key_type integer not null default(1), -- 1=Ed25519, 2=ECDSA(secp256k1)
|
key_type integer not null default(1), -- 1=Ed25519, 2=ECDSA(secp256k1)
|
||||||
created_at integer not null default(unixepoch ('now')),
|
created_at integer not null default(unixepoch ('now')),
|
||||||
updated_at integer not null default(unixepoch ('now'))
|
updated_at integer not null default(unixepoch ('now'))
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use arbiter_proto::{BOOTSTRAP_PATH, home_path};
|
|||||||
use diesel::QueryDsl;
|
use diesel::QueryDsl;
|
||||||
use diesel_async::RunQueryDsl;
|
use diesel_async::RunQueryDsl;
|
||||||
use kameo::{Actor, messages};
|
use kameo::{Actor, messages};
|
||||||
|
use miette::Diagnostic;
|
||||||
use rand::{RngExt, distr::Alphanumeric, make_rng, rngs::StdRng};
|
use rand::{RngExt, distr::Alphanumeric, make_rng, rngs::StdRng};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
@@ -25,15 +25,18 @@ pub async fn generate_token() -> Result<String, std::io::Error> {
|
|||||||
Ok(token)
|
Ok(token)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug, Diagnostic)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error("Database error: {0}")]
|
#[error("Database error: {0}")]
|
||||||
|
#[diagnostic(code(arbiter_server::bootstrap::database))]
|
||||||
Database(#[from] db::PoolError),
|
Database(#[from] db::PoolError),
|
||||||
|
|
||||||
#[error("Database query error: {0}")]
|
#[error("Database query error: {0}")]
|
||||||
|
#[diagnostic(code(arbiter_server::bootstrap::database_query))]
|
||||||
Query(#[from] diesel::result::Error),
|
Query(#[from] diesel::result::Error),
|
||||||
|
|
||||||
#[error("I/O error: {0}")]
|
#[error("I/O error: {0}")]
|
||||||
|
#[diagnostic(code(arbiter_server::bootstrap::io))]
|
||||||
Io(#[from] std::io::Error),
|
Io(#[from] std::io::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -287,7 +287,10 @@ where
|
|||||||
Ok(())
|
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
|
where
|
||||||
T: Bi<Inbound, Result<Outbound, Error>> + Send + ?Sized,
|
T: Bi<Inbound, Result<Outbound, Error>> + Send + ?Sized,
|
||||||
{
|
{
|
||||||
@@ -316,7 +319,7 @@ where
|
|||||||
|
|
||||||
sync_client_metadata(&props.db, info.id, &metadata).await?;
|
sync_client_metadata(&props.db, info.id, &metadata).await?;
|
||||||
challenge_client(transport, pubkey, info.current_nonce).await?;
|
challenge_client(transport, pubkey, info.current_nonce).await?;
|
||||||
|
|
||||||
transport
|
transport
|
||||||
.send(Ok(Outbound::AuthSuccess))
|
.send(Ok(Outbound::AuthSuccess))
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -20,7 +20,10 @@ pub struct ClientConnection {
|
|||||||
|
|
||||||
impl ClientConnection {
|
impl ClientConnection {
|
||||||
pub fn new(db: db::DatabasePool, actors: GlobalActors) -> Self {
|
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::{
|
use crate::{
|
||||||
actors::{
|
actors::{
|
||||||
GlobalActors,
|
GlobalActors,
|
||||||
client::ClientConnection,
|
client::ClientConnection, flow_coordinator::RegisterClient,
|
||||||
|
|
||||||
evm::{ClientSignTransaction, SignTransactionError},
|
evm::{ClientSignTransaction, SignTransactionError},
|
||||||
flow_coordinator::RegisterClient,
|
|
||||||
keyholder::KeyHolderState,
|
keyholder::KeyHolderState,
|
||||||
|
|
||||||
},
|
},
|
||||||
db,
|
db,
|
||||||
evm::VetError,
|
evm::VetError,
|
||||||
@@ -94,10 +95,7 @@ impl Actor for ClientSession {
|
|||||||
impl ClientSession {
|
impl ClientSession {
|
||||||
pub fn new_test(db: db::DatabasePool, actors: GlobalActors) -> Self {
|
pub fn new_test(db: db::DatabasePool, actors: GlobalActors) -> Self {
|
||||||
let props = ClientConnection::new(db, actors);
|
let props = ClientConnection::new(db, actors);
|
||||||
Self {
|
Self { props, client_id: 0 }
|
||||||
props,
|
|
||||||
client_id: 0,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,36 +25,45 @@ use crate::{
|
|||||||
|
|
||||||
pub use crate::evm::safe_signer;
|
pub use crate::evm::safe_signer;
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error, miette::Diagnostic)]
|
||||||
pub enum SignTransactionError {
|
pub enum SignTransactionError {
|
||||||
#[error("Wallet not found")]
|
#[error("Wallet not found")]
|
||||||
|
#[diagnostic(code(arbiter::evm::sign::wallet_not_found))]
|
||||||
WalletNotFound,
|
WalletNotFound,
|
||||||
|
|
||||||
#[error("Database error: {0}")]
|
#[error("Database error: {0}")]
|
||||||
|
#[diagnostic(code(arbiter::evm::sign::database))]
|
||||||
Database(#[from] DatabaseError),
|
Database(#[from] DatabaseError),
|
||||||
|
|
||||||
#[error("Keyholder error: {0}")]
|
#[error("Keyholder error: {0}")]
|
||||||
|
#[diagnostic(code(arbiter::evm::sign::keyholder))]
|
||||||
Keyholder(#[from] crate::actors::keyholder::Error),
|
Keyholder(#[from] crate::actors::keyholder::Error),
|
||||||
|
|
||||||
#[error("Keyholder mailbox error")]
|
#[error("Keyholder mailbox error")]
|
||||||
|
#[diagnostic(code(arbiter::evm::sign::keyholder_send))]
|
||||||
KeyholderSend,
|
KeyholderSend,
|
||||||
|
|
||||||
#[error("Signing error: {0}")]
|
#[error("Signing error: {0}")]
|
||||||
|
#[diagnostic(code(arbiter::evm::sign::signing))]
|
||||||
Signing(#[from] alloy::signers::Error),
|
Signing(#[from] alloy::signers::Error),
|
||||||
|
|
||||||
#[error("Policy error: {0}")]
|
#[error("Policy error: {0}")]
|
||||||
|
#[diagnostic(code(arbiter::evm::sign::vet))]
|
||||||
Vet(#[from] evm::VetError),
|
Vet(#[from] evm::VetError),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error, miette::Diagnostic)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error("Keyholder error: {0}")]
|
#[error("Keyholder error: {0}")]
|
||||||
|
#[diagnostic(code(arbiter::evm::keyholder))]
|
||||||
Keyholder(#[from] crate::actors::keyholder::Error),
|
Keyholder(#[from] crate::actors::keyholder::Error),
|
||||||
|
|
||||||
#[error("Keyholder mailbox error")]
|
#[error("Keyholder mailbox error")]
|
||||||
|
#[diagnostic(code(arbiter::evm::keyholder_send))]
|
||||||
KeyholderSend,
|
KeyholderSend,
|
||||||
|
|
||||||
#[error("Database error: {0}")]
|
#[error("Database error: {0}")]
|
||||||
|
#[diagnostic(code(arbiter::evm::database))]
|
||||||
Database(#[from] DatabaseError),
|
Database(#[from] DatabaseError),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ use crate::actors::{
|
|||||||
pub struct Args {
|
pub struct Args {
|
||||||
pub client: ClientProfile,
|
pub client: ClientProfile,
|
||||||
pub user_agents: Vec<ActorRef<UserAgentSession>>,
|
pub user_agents: Vec<ActorRef<UserAgentSession>>,
|
||||||
pub reply: ReplySender<Result<bool, ApprovalError>>,
|
pub reply: ReplySender<Result<bool, ApprovalError>>
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ClientApprovalController {
|
pub struct ClientApprovalController {
|
||||||
@@ -39,11 +39,7 @@ impl Actor for ClientApprovalController {
|
|||||||
type Error = ();
|
type Error = ();
|
||||||
|
|
||||||
async fn on_start(
|
async fn on_start(
|
||||||
Args {
|
Args { client, mut user_agents, reply }: Self::Args,
|
||||||
client,
|
|
||||||
mut user_agents,
|
|
||||||
reply,
|
|
||||||
}: Self::Args,
|
|
||||||
actor_ref: ActorRef<Self>,
|
actor_ref: ActorRef<Self>,
|
||||||
) -> Result<Self, Self::Error> {
|
) -> Result<Self, Self::Error> {
|
||||||
let this = Self {
|
let this = Self {
|
||||||
|
|||||||
@@ -1,21 +1,52 @@
|
|||||||
use std::ops::Deref as _;
|
use std::ops::Deref as _;
|
||||||
|
|
||||||
use argon2::{Algorithm, Argon2};
|
use argon2::{Algorithm, Argon2, password_hash::Salt as ArgonSalt};
|
||||||
use chacha20poly1305::{
|
use chacha20poly1305::{
|
||||||
AeadInPlace, Key, KeyInit as _, XChaCha20Poly1305, XNonce,
|
AeadInPlace, Key, KeyInit as _, XChaCha20Poly1305, XNonce,
|
||||||
aead::{AeadMut, Error, Payload},
|
aead::{AeadMut, Error, Payload},
|
||||||
};
|
};
|
||||||
use rand::{
|
use rand::{
|
||||||
Rng as _, SeedableRng as _,
|
Rng as _, SeedableRng,
|
||||||
rngs::{StdRng, SysRng},
|
rngs::{StdRng, SysRng},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::safe_cell::{SafeCell, SafeCellHandle as _};
|
use crate::safe_cell::{SafeCell, SafeCellHandle as _};
|
||||||
|
|
||||||
pub mod encryption;
|
pub const ROOT_KEY_TAG: &[u8] = "arbiter/seal/v1".as_bytes();
|
||||||
pub mod integrity;
|
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>);
|
pub struct KeyCell(pub SafeCell<Key>);
|
||||||
impl From<SafeCell<Key>> for KeyCell {
|
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...
|
/// 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.
|
/// 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)]
|
#[allow(clippy::unwrap_used)]
|
||||||
let params = argon2::Params::new(262_144, 3, 4, None).unwrap();
|
let params = argon2::Params::new(262_144, 3, 4, None).unwrap();
|
||||||
let hasher = Argon2::new(Algorithm::Argon2id, argon2::Version::V0x13, params);
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
use super::*;
|
||||||
derive_key,
|
use crate::safe_cell::SafeCell;
|
||||||
encryption::v1::{Nonce, generate_salt},
|
|
||||||
};
|
#[test]
|
||||||
use crate::safe_cell::{SafeCell, SafeCellHandle as _};
|
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]
|
#[test]
|
||||||
pub fn encrypt_decrypt() {
|
pub fn encrypt_decrypt() {
|
||||||
@@ -139,7 +209,7 @@ mod tests {
|
|||||||
let password = SafeCell::new(PASSWORD.to_vec());
|
let password = SafeCell::new(PASSWORD.to_vec());
|
||||||
let salt = generate_salt();
|
let salt = generate_salt();
|
||||||
|
|
||||||
let mut key = derive_key(password, &salt);
|
let mut key = derive_seal_key(password, &salt);
|
||||||
let nonce = Nonce(*b"unique nonce 123 1231233"); // 24 bytes for XChaCha20Poly1305
|
let nonce = Nonce(*b"unique nonce 123 1231233"); // 24 bytes for XChaCha20Poly1305
|
||||||
let associated_data = b"associated data";
|
let associated_data = b"associated data";
|
||||||
let mut buffer = b"secret data".to_vec();
|
let mut buffer = b"secret data".to_vec();
|
||||||
@@ -156,4 +226,18 @@ mod tests {
|
|||||||
let buffer = buffer.read();
|
let buffer = buffer.read();
|
||||||
assert_eq!(*buffer, b"secret data");
|
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 strum::{EnumDiscriminants, IntoDiscriminant};
|
||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
|
|
||||||
use crate::{
|
use crate::safe_cell::SafeCell;
|
||||||
crypto::{
|
|
||||||
KeyCell, derive_key,
|
|
||||||
encryption::v1::{self, Nonce},
|
|
||||||
integrity::v1::compute_integrity_tag,
|
|
||||||
},
|
|
||||||
safe_cell::SafeCell,
|
|
||||||
};
|
|
||||||
use crate::{
|
use crate::{
|
||||||
db::{
|
db::{
|
||||||
self,
|
self,
|
||||||
@@ -24,6 +17,9 @@ use crate::{
|
|||||||
},
|
},
|
||||||
safe_cell::SafeCellHandle as _,
|
safe_cell::SafeCellHandle as _,
|
||||||
};
|
};
|
||||||
|
use encryption::v1::{self, KeyCell, Nonce};
|
||||||
|
|
||||||
|
pub mod encryption;
|
||||||
|
|
||||||
#[derive(Default, EnumDiscriminants)]
|
#[derive(Default, EnumDiscriminants)]
|
||||||
#[strum_discriminants(derive(Reply), vis(pub), name(KeyHolderState))]
|
#[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 {
|
pub enum Error {
|
||||||
#[error("Keyholder is already bootstrapped")]
|
#[error("Keyholder is already bootstrapped")]
|
||||||
|
#[diagnostic(code(arbiter::keyholder::already_bootstrapped))]
|
||||||
AlreadyBootstrapped,
|
AlreadyBootstrapped,
|
||||||
#[error("Keyholder is not bootstrapped")]
|
#[error("Keyholder is not bootstrapped")]
|
||||||
|
#[diagnostic(code(arbiter::keyholder::not_bootstrapped))]
|
||||||
NotBootstrapped,
|
NotBootstrapped,
|
||||||
#[error("Invalid key provided")]
|
#[error("Invalid key provided")]
|
||||||
|
#[diagnostic(code(arbiter::keyholder::invalid_key))]
|
||||||
InvalidKey,
|
InvalidKey,
|
||||||
|
|
||||||
#[error("Requested aead entry not found")]
|
#[error("Requested aead entry not found")]
|
||||||
|
#[diagnostic(code(arbiter::keyholder::aead_not_found))]
|
||||||
NotFound,
|
NotFound,
|
||||||
|
|
||||||
#[error("Encryption error: {0}")]
|
#[error("Encryption error: {0}")]
|
||||||
|
#[diagnostic(code(arbiter::keyholder::encryption_error))]
|
||||||
Encryption(#[from] chacha20poly1305::aead::Error),
|
Encryption(#[from] chacha20poly1305::aead::Error),
|
||||||
|
|
||||||
#[error("Database error: {0}")]
|
#[error("Database error: {0}")]
|
||||||
|
#[diagnostic(code(arbiter::keyholder::database_error))]
|
||||||
DatabaseConnection(#[from] db::PoolError),
|
DatabaseConnection(#[from] db::PoolError),
|
||||||
|
|
||||||
#[error("Database transaction error: {0}")]
|
#[error("Database transaction error: {0}")]
|
||||||
|
#[diagnostic(code(arbiter::keyholder::database_transaction_error))]
|
||||||
DatabaseTransaction(#[from] diesel::result::Error),
|
DatabaseTransaction(#[from] diesel::result::Error),
|
||||||
|
|
||||||
#[error("Broken database")]
|
#[error("Broken database")]
|
||||||
|
#[diagnostic(code(arbiter::keyholder::broken_database))]
|
||||||
BrokenDatabase,
|
BrokenDatabase,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,13 +114,14 @@ impl KeyHolder {
|
|||||||
.first(conn)
|
.first(conn)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mut nonce = Nonce::try_from(current_nonce.as_slice()).map_err(|_| {
|
let mut nonce =
|
||||||
error!(
|
v1::Nonce::try_from(current_nonce.as_slice()).map_err(|_| {
|
||||||
"Broken database: invalid nonce for root key history id={}",
|
error!(
|
||||||
root_key_id
|
"Broken database: invalid nonce for root key history id={}",
|
||||||
);
|
root_key_id
|
||||||
Error::BrokenDatabase
|
);
|
||||||
})?;
|
Error::BrokenDatabase
|
||||||
|
})?;
|
||||||
nonce.increment();
|
nonce.increment();
|
||||||
|
|
||||||
update(schema::root_key_history::table)
|
update(schema::root_key_history::table)
|
||||||
@@ -139,12 +144,12 @@ impl KeyHolder {
|
|||||||
return Err(Error::AlreadyBootstrapped);
|
return Err(Error::AlreadyBootstrapped);
|
||||||
}
|
}
|
||||||
let salt = v1::generate_salt();
|
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();
|
let mut root_key = KeyCell::new_secure_random();
|
||||||
|
|
||||||
// Zero nonces are fine because they are one-time
|
// Zero nonces are fine because they are one-time
|
||||||
let root_key_nonce = Nonce::default();
|
let root_key_nonce = v1::Nonce::default();
|
||||||
let data_encryption_nonce = Nonce::default();
|
let data_encryption_nonce = v1::Nonce::default();
|
||||||
|
|
||||||
let root_key_ciphertext: Vec<u8> = root_key.0.read_inline(|reader| {
|
let root_key_ciphertext: Vec<u8> = root_key.0.read_inline(|reader| {
|
||||||
let root_key_reader = reader.as_slice();
|
let root_key_reader = reader.as_slice();
|
||||||
@@ -219,7 +224,7 @@ impl KeyHolder {
|
|||||||
error!("Broken database: invalid salt for root key");
|
error!("Broken database: invalid salt for root key");
|
||||||
Error::BrokenDatabase
|
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());
|
let mut root_key = SafeCell::new(current_key.ciphertext.clone());
|
||||||
|
|
||||||
@@ -239,7 +244,7 @@ impl KeyHolder {
|
|||||||
|
|
||||||
self.state = State::Unsealed {
|
self.state = State::Unsealed {
|
||||||
root_key_history_id: current_key.id,
|
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!(?err, "Broken database: invalid encryption key size");
|
||||||
Error::BrokenDatabase
|
Error::BrokenDatabase
|
||||||
})?,
|
})?,
|
||||||
@@ -250,22 +255,7 @@ impl KeyHolder {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Signs a generic integrity payload using the vault-derived integrity key
|
// Decrypts the `aead_encrypted` entry with the given ID and returns the plaintext
|
||||||
#[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())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[message]
|
#[message]
|
||||||
pub async fn decrypt(&mut self, aead_id: i32) -> Result<SafeCell<Vec<u8>>, Error> {
|
pub async fn decrypt(&mut self, aead_id: i32) -> Result<SafeCell<Vec<u8>>, Error> {
|
||||||
let State::Unsealed { root_key, .. } = &mut self.state else {
|
let State::Unsealed { root_key, .. } = &mut self.state else {
|
||||||
@@ -301,7 +291,6 @@ impl KeyHolder {
|
|||||||
let State::Unsealed {
|
let State::Unsealed {
|
||||||
root_key,
|
root_key,
|
||||||
root_key_history_id,
|
root_key_history_id,
|
||||||
..
|
|
||||||
} = &mut self.state
|
} = &mut self.state
|
||||||
else {
|
else {
|
||||||
return Err(Error::NotBootstrapped);
|
return Err(Error::NotBootstrapped);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use kameo::actor::{ActorRef, Spawn};
|
use kameo::actor::{ActorRef, Spawn};
|
||||||
|
use miette::Diagnostic;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -16,12 +17,14 @@ pub mod flow_coordinator;
|
|||||||
pub mod keyholder;
|
pub mod keyholder;
|
||||||
pub mod user_agent;
|
pub mod user_agent;
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug, Diagnostic)]
|
||||||
pub enum SpawnError {
|
pub enum SpawnError {
|
||||||
#[error("Failed to spawn Bootstrapper actor")]
|
#[error("Failed to spawn Bootstrapper actor")]
|
||||||
|
#[diagnostic(code(SpawnError::Bootstrapper))]
|
||||||
Bootstrapper(#[from] bootstrap::Error),
|
Bootstrapper(#[from] bootstrap::Error),
|
||||||
|
|
||||||
#[error("Failed to spawn KeyHolder actor")]
|
#[error("Failed to spawn KeyHolder actor")]
|
||||||
|
#[diagnostic(code(SpawnError::KeyHolder))]
|
||||||
KeyHolder(#[from] keyholder::Error),
|
KeyHolder(#[from] keyholder::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,17 @@
|
|||||||
use arbiter_proto::transport::Bi;
|
use arbiter_proto::transport::Bi;
|
||||||
use diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl, update};
|
use diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl, update};
|
||||||
use diesel_async::RunQueryDsl;
|
use diesel_async::RunQueryDsl;
|
||||||
use kameo::error::SendError;
|
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
use super::Error;
|
use super::Error;
|
||||||
use crate::{
|
use crate::{
|
||||||
actors::{
|
actors::{
|
||||||
bootstrap::ConsumeToken,
|
bootstrap::ConsumeToken,
|
||||||
keyholder::{self, SignIntegrityTag},
|
|
||||||
user_agent::{AuthPublicKey, UserAgentConnection, auth::Outbound},
|
user_agent::{AuthPublicKey, UserAgentConnection, auth::Outbound},
|
||||||
},
|
},
|
||||||
crypto::integrity::v1::USERAGENT_INTEGRITY_TAG,
|
|
||||||
db::schema,
|
db::schema,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum AttestationStatus {
|
|
||||||
Attested,
|
|
||||||
NotAttested,
|
|
||||||
Unavailable,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ChallengeRequest {
|
pub struct ChallengeRequest {
|
||||||
pub pubkey: AuthPublicKey,
|
pub pubkey: AuthPublicKey,
|
||||||
}
|
}
|
||||||
@@ -50,11 +40,7 @@ smlang::statemachine!(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
async fn create_nonce(
|
async fn create_nonce(db: &crate::db::DatabasePool, pubkey_bytes: &[u8]) -> Result<i32, Error> {
|
||||||
db: &crate::db::DatabasePool,
|
|
||||||
pubkey_bytes: &[u8],
|
|
||||||
key_type: crate::db::models::KeyType,
|
|
||||||
) -> Result<i32, Error> {
|
|
||||||
let mut db_conn = db.get().await.map_err(|e| {
|
let mut db_conn = db.get().await.map_err(|e| {
|
||||||
error!(error = ?e, "Database pool error");
|
error!(error = ?e, "Database pool error");
|
||||||
Error::internal("Database unavailable")
|
Error::internal("Database unavailable")
|
||||||
@@ -64,14 +50,12 @@ async fn create_nonce(
|
|||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let current_nonce = schema::useragent_client::table
|
let current_nonce = schema::useragent_client::table
|
||||||
.filter(schema::useragent_client::public_key.eq(pubkey_bytes.to_vec()))
|
.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)
|
.select(schema::useragent_client::nonce)
|
||||||
.first::<i32>(conn)
|
.first::<i32>(conn)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
update(schema::useragent_client::table)
|
update(schema::useragent_client::table)
|
||||||
.filter(schema::useragent_client::public_key.eq(pubkey_bytes.to_vec()))
|
.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))
|
.set(schema::useragent_client::nonce.eq(current_nonce + 1))
|
||||||
.execute(conn)
|
.execute(conn)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -91,11 +75,7 @@ async fn create_nonce(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn register_key(
|
async fn register_key(db: &crate::db::DatabasePool, pubkey: &AuthPublicKey) -> Result<(), Error> {
|
||||||
db: &crate::db::DatabasePool,
|
|
||||||
pubkey: &AuthPublicKey,
|
|
||||||
integrity_tag: Option<Vec<u8>>,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
let pubkey_bytes = pubkey.to_stored_bytes();
|
let pubkey_bytes = pubkey.to_stored_bytes();
|
||||||
let key_type = pubkey.key_type();
|
let key_type = pubkey.key_type();
|
||||||
let mut conn = db.get().await.map_err(|e| {
|
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::public_key.eq(pubkey_bytes),
|
||||||
schema::useragent_client::nonce.eq(1),
|
schema::useragent_client::nonce.eq(1),
|
||||||
schema::useragent_client::key_type.eq(key_type),
|
schema::useragent_client::key_type.eq(key_type),
|
||||||
schema::useragent_client::pubkey_integrity_tag.eq(integrity_tag),
|
|
||||||
))
|
))
|
||||||
.execute(&mut conn)
|
.execute(&mut conn)
|
||||||
.await
|
.await
|
||||||
@@ -141,15 +120,8 @@ where
|
|||||||
&mut self,
|
&mut self,
|
||||||
ChallengeRequest { pubkey }: ChallengeRequest,
|
ChallengeRequest { pubkey }: ChallengeRequest,
|
||||||
) -> Result<ChallengeContext, Self::Error> {
|
) -> 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 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
|
self.transport
|
||||||
.send(Ok(Outbound::AuthChallenge { nonce }))
|
.send(Ok(Outbound::AuthChallenge { nonce }))
|
||||||
@@ -189,15 +161,7 @@ where
|
|||||||
return Err(Error::InvalidBootstrapToken);
|
return Err(Error::InvalidBootstrapToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
let integrity_tag = self
|
register_key(&self.conn.db, &pubkey).await?;
|
||||||
.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?;
|
|
||||||
|
|
||||||
self.transport
|
self.transport
|
||||||
.send(Ok(Outbound::AuthSuccess))
|
.send(Ok(Outbound::AuthSuccess))
|
||||||
@@ -246,111 +210,16 @@ where
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
match valid {
|
if !valid {
|
||||||
true => {
|
error!("Invalid challenge solution signature");
|
||||||
self.transport
|
return Err(Error::InvalidChallengeSolution);
|
||||||
.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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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::{ExpressionMethods as _, QueryDsl as _, SelectableHelper};
|
||||||
use diesel_async::{AsyncConnection, RunQueryDsl};
|
use diesel_async::{AsyncConnection, RunQueryDsl};
|
||||||
use kameo::error::SendError;
|
use kameo::error::SendError;
|
||||||
use kameo::messages;
|
|
||||||
use kameo::prelude::Context;
|
use kameo::prelude::Context;
|
||||||
|
use kameo::messages;
|
||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
use x25519_dalek::{EphemeralSecret, PublicKey};
|
use x25519_dalek::{EphemeralSecret, PublicKey};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use miette::Diagnostic;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -10,24 +11,30 @@ use crate::{
|
|||||||
|
|
||||||
pub mod tls;
|
pub mod tls;
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug, Diagnostic)]
|
||||||
pub enum InitError {
|
pub enum InitError {
|
||||||
#[error("Database setup failed: {0}")]
|
#[error("Database setup failed: {0}")]
|
||||||
|
#[diagnostic(code(arbiter_server::init::database_setup))]
|
||||||
DatabaseSetup(#[from] db::DatabaseSetupError),
|
DatabaseSetup(#[from] db::DatabaseSetupError),
|
||||||
|
|
||||||
#[error("Connection acquire failed: {0}")]
|
#[error("Connection acquire failed: {0}")]
|
||||||
|
#[diagnostic(code(arbiter_server::init::database_pool))]
|
||||||
DatabasePool(#[from] db::PoolError),
|
DatabasePool(#[from] db::PoolError),
|
||||||
|
|
||||||
#[error("Database query error: {0}")]
|
#[error("Database query error: {0}")]
|
||||||
|
#[diagnostic(code(arbiter_server::init::database_query))]
|
||||||
DatabaseQuery(#[from] diesel::result::Error),
|
DatabaseQuery(#[from] diesel::result::Error),
|
||||||
|
|
||||||
#[error("TLS initialization failed: {0}")]
|
#[error("TLS initialization failed: {0}")]
|
||||||
|
#[diagnostic(code(arbiter_server::init::tls_init))]
|
||||||
Tls(#[from] tls::InitError),
|
Tls(#[from] tls::InitError),
|
||||||
|
|
||||||
#[error("Actor spawn failed: {0}")]
|
#[error("Actor spawn failed: {0}")]
|
||||||
|
#[diagnostic(code(arbiter_server::init::actor_spawn))]
|
||||||
ActorSpawn(#[from] crate::actors::SpawnError),
|
ActorSpawn(#[from] crate::actors::SpawnError),
|
||||||
|
|
||||||
#[error("I/O Error: {0}")]
|
#[error("I/O Error: {0}")]
|
||||||
|
#[diagnostic(code(arbiter_server::init::io))]
|
||||||
Io(#[from] std::io::Error),
|
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::{ExpressionMethods as _, QueryDsl, SelectableHelper as _};
|
||||||
use diesel_async::{AsyncConnection, RunQueryDsl};
|
use diesel_async::{AsyncConnection, RunQueryDsl};
|
||||||
|
use miette::Diagnostic;
|
||||||
use pem::Pem;
|
use pem::Pem;
|
||||||
use rcgen::{
|
use rcgen::{
|
||||||
BasicConstraints, Certificate, CertificateParams, CertifiedIssuer, DistinguishedName, DnType,
|
BasicConstraints, Certificate, CertificateParams, CertifiedIssuer, DistinguishedName, DnType,
|
||||||
@@ -29,24 +29,30 @@ const ENCODE_CONFIG: pem::EncodeConfig = {
|
|||||||
pem::EncodeConfig::new().set_line_ending(line_ending)
|
pem::EncodeConfig::new().set_line_ending(line_ending)
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug, Diagnostic)]
|
||||||
pub enum InitError {
|
pub enum InitError {
|
||||||
#[error("Key generation error during TLS initialization: {0}")]
|
#[error("Key generation error during TLS initialization: {0}")]
|
||||||
|
#[diagnostic(code(arbiter_server::tls_init::key_generation))]
|
||||||
KeyGeneration(#[from] rcgen::Error),
|
KeyGeneration(#[from] rcgen::Error),
|
||||||
|
|
||||||
#[error("Key invalid format: {0}")]
|
#[error("Key invalid format: {0}")]
|
||||||
|
#[diagnostic(code(arbiter_server::tls_init::key_invalid_format))]
|
||||||
KeyInvalidFormat(#[from] FromUtf8Error),
|
KeyInvalidFormat(#[from] FromUtf8Error),
|
||||||
|
|
||||||
#[error("Key deserialization error: {0}")]
|
#[error("Key deserialization error: {0}")]
|
||||||
|
#[diagnostic(code(arbiter_server::tls_init::key_deserialization))]
|
||||||
KeyDeserializationError(rcgen::Error),
|
KeyDeserializationError(rcgen::Error),
|
||||||
|
|
||||||
#[error("Database error during TLS initialization: {0}")]
|
#[error("Database error during TLS initialization: {0}")]
|
||||||
|
#[diagnostic(code(arbiter_server::tls_init::database_error))]
|
||||||
DatabaseError(#[from] diesel::result::Error),
|
DatabaseError(#[from] diesel::result::Error),
|
||||||
|
|
||||||
#[error("Pem deserialization error during TLS initialization: {0}")]
|
#[error("Pem deserialization error during TLS initialization: {0}")]
|
||||||
|
#[diagnostic(code(arbiter_server::tls_init::pem_deserialization))]
|
||||||
PemDeserializationError(#[from] rustls::pki_types::pem::Error),
|
PemDeserializationError(#[from] rustls::pki_types::pem::Error),
|
||||||
|
|
||||||
#[error("Database pool acquire error during TLS initialization: {0}")]
|
#[error("Database pool acquire error during TLS initialization: {0}")]
|
||||||
|
#[diagnostic(code(arbiter_server::tls_init::database_pool_acquire))]
|
||||||
DatabasePoolAcquire(#[from] db::PoolError),
|
DatabasePoolAcquire(#[from] db::PoolError),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,7 +116,9 @@ impl TlsCa {
|
|||||||
];
|
];
|
||||||
params
|
params
|
||||||
.subject_alt_names
|
.subject_alt_names
|
||||||
.push(SanType::IpAddress(Ipv4Addr::LOCALHOST.into()));
|
.push(SanType::IpAddress(IpAddr::from([
|
||||||
|
127, 0, 0, 1,
|
||||||
|
])));
|
||||||
|
|
||||||
let mut dn = DistinguishedName::new();
|
let mut dn = DistinguishedName::new();
|
||||||
dn.push(DnType::CommonName, "Arbiter Instance Leaf");
|
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,
|
sync_connection_wrapper::SyncConnectionWrapper,
|
||||||
};
|
};
|
||||||
use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations};
|
use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations};
|
||||||
|
use miette::Diagnostic;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
@@ -21,21 +21,26 @@ static DB_FILE: &str = "arbiter.sqlite";
|
|||||||
|
|
||||||
const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations");
|
const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations");
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Diagnostic, Debug)]
|
||||||
pub enum DatabaseSetupError {
|
pub enum DatabaseSetupError {
|
||||||
#[error("Failed to determine home directory")]
|
#[error("Failed to determine home directory")]
|
||||||
|
#[diagnostic(code(arbiter::db::home_dir))]
|
||||||
HomeDir(std::io::Error),
|
HomeDir(std::io::Error),
|
||||||
|
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
|
#[diagnostic(code(arbiter::db::connection))]
|
||||||
Connection(diesel::ConnectionError),
|
Connection(diesel::ConnectionError),
|
||||||
|
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
|
#[diagnostic(code(arbiter::db::concurrency))]
|
||||||
ConcurrencySetup(diesel::result::Error),
|
ConcurrencySetup(diesel::result::Error),
|
||||||
|
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
|
#[diagnostic(code(arbiter::db::migration))]
|
||||||
Migration(Box<dyn std::error::Error + Send + Sync>),
|
Migration(Box<dyn std::error::Error + Send + Sync>),
|
||||||
|
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
|
#[diagnostic(code(arbiter::db::pool))]
|
||||||
Pool(#[from] PoolInitError),
|
Pool(#[from] PoolInitError),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -242,7 +242,6 @@ pub struct UseragentClient {
|
|||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub nonce: i32,
|
pub nonce: i32,
|
||||||
pub public_key: Vec<u8>,
|
pub public_key: Vec<u8>,
|
||||||
pub pubkey_integrity_tag: Option<Vec<u8>>,
|
|
||||||
pub created_at: SqliteTimestamp,
|
pub created_at: SqliteTimestamp,
|
||||||
pub updated_at: SqliteTimestamp,
|
pub updated_at: SqliteTimestamp,
|
||||||
pub key_type: KeyType,
|
pub key_type: KeyType,
|
||||||
|
|||||||
@@ -178,7 +178,6 @@ diesel::table! {
|
|||||||
id -> Integer,
|
id -> Integer,
|
||||||
nonce -> Integer,
|
nonce -> Integer,
|
||||||
public_key -> Binary,
|
public_key -> Binary,
|
||||||
pubkey_integrity_tag -> Nullable<Binary>,
|
|
||||||
key_type -> Integer,
|
key_type -> Integer,
|
||||||
created_at -> Integer,
|
created_at -> Integer,
|
||||||
updated_at -> Integer,
|
updated_at -> Integer,
|
||||||
|
|||||||
@@ -28,32 +28,39 @@ pub mod policies;
|
|||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
/// Errors that can only occur once the transaction meaning is known (during policy evaluation)
|
/// 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 {
|
pub enum PolicyError {
|
||||||
#[error("Database error")]
|
#[error("Database error")]
|
||||||
Database(#[from] crate::db::DatabaseError),
|
Database(#[from] crate::db::DatabaseError),
|
||||||
#[error("Transaction violates policy: {0:?}")]
|
#[error("Transaction violates policy: {0:?}")]
|
||||||
|
#[diagnostic(code(arbiter_server::evm::policy_error::violation))]
|
||||||
Violations(Vec<EvalViolation>),
|
Violations(Vec<EvalViolation>),
|
||||||
#[error("No matching grant found")]
|
#[error("No matching grant found")]
|
||||||
|
#[diagnostic(code(arbiter_server::evm::policy_error::no_matching_grant))]
|
||||||
NoMatchingGrant,
|
NoMatchingGrant,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error, miette::Diagnostic)]
|
||||||
pub enum VetError {
|
pub enum VetError {
|
||||||
#[error("Contract creation transactions are not supported")]
|
#[error("Contract creation transactions are not supported")]
|
||||||
|
#[diagnostic(code(arbiter_server::evm::vet_error::contract_creation_unsupported))]
|
||||||
ContractCreationNotSupported,
|
ContractCreationNotSupported,
|
||||||
#[error("Engine can't classify this transaction")]
|
#[error("Engine can't classify this transaction")]
|
||||||
|
#[diagnostic(code(arbiter_server::evm::vet_error::unsupported))]
|
||||||
UnsupportedTransactionType,
|
UnsupportedTransactionType,
|
||||||
#[error("Policy evaluation failed: {1}")]
|
#[error("Policy evaluation failed: {1}")]
|
||||||
|
#[diagnostic(code(arbiter_server::evm::vet_error::evaluated))]
|
||||||
Evaluated(SpecificMeaning, #[source] PolicyError),
|
Evaluated(SpecificMeaning, #[source] PolicyError),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error, miette::Diagnostic)]
|
||||||
pub enum AnalyzeError {
|
pub enum AnalyzeError {
|
||||||
#[error("Engine doesn't support granting permissions for contract creation")]
|
#[error("Engine doesn't support granting permissions for contract creation")]
|
||||||
|
#[diagnostic(code(arbiter_server::evm::analyze_error::contract_creation_not_supported))]
|
||||||
ContractCreationNotSupported,
|
ContractCreationNotSupported,
|
||||||
|
|
||||||
#[error("Unsupported transaction type")]
|
#[error("Unsupported transaction type")]
|
||||||
|
#[diagnostic(code(arbiter_server::evm::analyze_error::unsupported_transaction_type))]
|
||||||
UnsupportedTransactionType,
|
UnsupportedTransactionType,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use diesel::{
|
|||||||
ExpressionMethods as _, QueryDsl, SelectableHelper, result::QueryResult, sqlite::Sqlite,
|
ExpressionMethods as _, QueryDsl, SelectableHelper, result::QueryResult, sqlite::Sqlite,
|
||||||
};
|
};
|
||||||
use diesel_async::{AsyncConnection, RunQueryDsl};
|
use diesel_async::{AsyncConnection, RunQueryDsl};
|
||||||
|
use miette::Diagnostic;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -33,27 +33,33 @@ pub struct EvalContext {
|
|||||||
pub max_priority_fee_per_gas: u128,
|
pub max_priority_fee_per_gas: u128,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error, Diagnostic)]
|
||||||
pub enum EvalViolation {
|
pub enum EvalViolation {
|
||||||
#[error("This grant doesn't allow transactions to the target address {target}")]
|
#[error("This grant doesn't allow transactions to the target address {target}")]
|
||||||
|
#[diagnostic(code(arbiter_server::evm::eval_violation::invalid_target))]
|
||||||
InvalidTarget { target: Address },
|
InvalidTarget { target: Address },
|
||||||
|
|
||||||
#[error("Gas limit exceeded for this grant")]
|
#[error("Gas limit exceeded for this grant")]
|
||||||
|
#[diagnostic(code(arbiter_server::evm::eval_violation::gas_limit_exceeded))]
|
||||||
GasLimitExceeded {
|
GasLimitExceeded {
|
||||||
max_gas_fee_per_gas: Option<U256>,
|
max_gas_fee_per_gas: Option<U256>,
|
||||||
max_priority_fee_per_gas: Option<U256>,
|
max_priority_fee_per_gas: Option<U256>,
|
||||||
},
|
},
|
||||||
|
|
||||||
#[error("Rate limit exceeded for this grant")]
|
#[error("Rate limit exceeded for this grant")]
|
||||||
|
#[diagnostic(code(arbiter_server::evm::eval_violation::rate_limit_exceeded))]
|
||||||
RateLimitExceeded,
|
RateLimitExceeded,
|
||||||
|
|
||||||
#[error("Transaction exceeds volumetric limits of the grant")]
|
#[error("Transaction exceeds volumetric limits of the grant")]
|
||||||
|
#[diagnostic(code(arbiter_server::evm::eval_violation::volumetric_limit_exceeded))]
|
||||||
VolumetricLimitExceeded,
|
VolumetricLimitExceeded,
|
||||||
|
|
||||||
#[error("Transaction is outside of the grant's validity period")]
|
#[error("Transaction is outside of the grant's validity period")]
|
||||||
|
#[diagnostic(code(arbiter_server::evm::eval_violation::invalid_time))]
|
||||||
InvalidTime,
|
InvalidTime,
|
||||||
|
|
||||||
#[error("Transaction type is not allowed by this grant")]
|
#[error("Transaction type is not allowed by this grant")]
|
||||||
|
#[diagnostic(code(arbiter_server::evm::eval_violation::invalid_transaction_type))]
|
||||||
InvalidTransactionType,
|
InvalidTransactionType,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -140,9 +140,7 @@ impl Receiver<auth::Inbound> for AuthTransportAdapter<'_> {
|
|||||||
let Some(payload) = auth_request.payload else {
|
let Some(payload) = auth_request.payload else {
|
||||||
let _ = self
|
let _ = self
|
||||||
.bi
|
.bi
|
||||||
.send(Err(Status::invalid_argument(
|
.send(Err(Status::invalid_argument("Missing client auth request payload")))
|
||||||
"Missing client auth request payload",
|
|
||||||
)))
|
|
||||||
.await;
|
.await;
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
@@ -172,7 +170,9 @@ impl Receiver<auth::Inbound> for AuthTransportAdapter<'_> {
|
|||||||
metadata: client_metadata_from_proto(client_info),
|
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 Ok(signature) = ed25519_dalek::Signature::try_from(signature.as_slice()) else {
|
||||||
let _ = self
|
let _ = self
|
||||||
.send_auth_result(ProtoAuthResult::InvalidSignature)
|
.send_auth_result(ProtoAuthResult::InvalidSignature)
|
||||||
|
|||||||
@@ -34,9 +34,7 @@ pub(super) async fn dispatch(
|
|||||||
req: proto_evm::Request,
|
req: proto_evm::Request,
|
||||||
) -> Result<ClientResponsePayload, Status> {
|
) -> Result<ClientResponsePayload, Status> {
|
||||||
let Some(payload) = req.payload else {
|
let Some(payload) = req.payload else {
|
||||||
return Err(Status::invalid_argument(
|
return Err(Status::invalid_argument("Missing client EVM request payload"));
|
||||||
"Missing client EVM request payload",
|
|
||||||
));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
match payload {
|
match payload {
|
||||||
@@ -61,13 +59,13 @@ pub(super) async fn dispatch(
|
|||||||
))) => EvmSignTransactionResponse {
|
))) => EvmSignTransactionResponse {
|
||||||
result: Some(vet_error.convert()),
|
result: Some(vet_error.convert()),
|
||||||
},
|
},
|
||||||
Err(kameo::error::SendError::HandlerError(SignTransactionRpcError::Internal)) => {
|
Err(kameo::error::SendError::HandlerError(
|
||||||
EvmSignTransactionResponse {
|
SignTransactionRpcError::Internal,
|
||||||
result: Some(EvmSignTransactionResult::Error(
|
)) => EvmSignTransactionResponse {
|
||||||
ProtoEvmError::Internal.into(),
|
result: Some(EvmSignTransactionResult::Error(
|
||||||
)),
|
ProtoEvmError::Internal.into(),
|
||||||
}
|
)),
|
||||||
}
|
},
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
warn!(error = ?err, "Failed to sign EVM transaction");
|
warn!(error = ?err, "Failed to sign EVM transaction");
|
||||||
EvmSignTransactionResponse {
|
EvmSignTransactionResponse {
|
||||||
@@ -80,8 +78,8 @@ pub(super) async fn dispatch(
|
|||||||
|
|
||||||
Ok(wrap_response(EvmResponsePayload::SignTransaction(response)))
|
Ok(wrap_response(EvmResponsePayload::SignTransaction(response)))
|
||||||
}
|
}
|
||||||
EvmRequestPayload::AnalyzeTransaction(_) => Err(Status::unimplemented(
|
EvmRequestPayload::AnalyzeTransaction(_) => {
|
||||||
"EVM transaction analysis is not yet implemented",
|
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 tonic::Status;
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
|
|
||||||
use crate::actors::{
|
use crate::{
|
||||||
client::session::{ClientSession, Error, HandleQueryVaultState},
|
actors::{
|
||||||
keyholder::KeyHolderState,
|
client::session::{ClientSession, Error, HandleQueryVaultState},
|
||||||
|
keyholder::KeyHolderState,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(super) async fn dispatch(
|
pub(super) async fn dispatch(
|
||||||
@@ -22,9 +24,7 @@ pub(super) async fn dispatch(
|
|||||||
req: proto_vault::Request,
|
req: proto_vault::Request,
|
||||||
) -> Result<ClientResponsePayload, Status> {
|
) -> Result<ClientResponsePayload, Status> {
|
||||||
let Some(payload) = req.payload else {
|
let Some(payload) = req.payload else {
|
||||||
return Err(Status::invalid_argument(
|
return Err(Status::invalid_argument("Missing client vault request payload"));
|
||||||
"Missing client vault request payload",
|
|
||||||
));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
match payload {
|
match payload {
|
||||||
|
|||||||
@@ -28,8 +28,9 @@ impl TryConvert for RawEvmTransaction {
|
|||||||
type Error = tonic::Status;
|
type Error = tonic::Status;
|
||||||
|
|
||||||
fn try_convert(self) -> Result<Self::Output, Self::Error> {
|
fn try_convert(self) -> Result<Self::Output, Self::Error> {
|
||||||
let tx = TxEip1559::decode(&mut self.0.as_slice())
|
let tx = TxEip1559::decode(&mut self.0.as_slice()).map_err(|_| {
|
||||||
.map_err(|_| tonic::Status::invalid_argument("Invalid EVM transaction format"))?;
|
tonic::Status::invalid_argument("Invalid EVM transaction format")
|
||||||
|
})?;
|
||||||
Ok(tx)
|
Ok(tx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,9 @@
|
|||||||
use alloy::primitives::U256;
|
use alloy::primitives::U256;
|
||||||
use arbiter_proto::proto::{
|
use arbiter_proto::proto::{
|
||||||
evm::{
|
evm::{EvmError as ProtoEvmError, evm_sign_transaction_response::Result as EvmSignTransactionResult},
|
||||||
EvmError as ProtoEvmError,
|
|
||||||
evm_sign_transaction_response::Result as EvmSignTransactionResult,
|
|
||||||
},
|
|
||||||
shared::evm::{
|
shared::evm::{
|
||||||
EvalViolation as ProtoEvalViolation, GasLimitExceededViolation, NoMatchingGrantError,
|
EvalViolation as ProtoEvalViolation, GasLimitExceededViolation,
|
||||||
PolicyViolationsError, SpecificMeaning as ProtoSpecificMeaning,
|
NoMatchingGrantError, PolicyViolationsError, SpecificMeaning as ProtoSpecificMeaning,
|
||||||
TokenInfo as ProtoTokenInfo, TransactionEvalError as ProtoTransactionEvalError,
|
TokenInfo as ProtoTokenInfo, TransactionEvalError as ProtoTransactionEvalError,
|
||||||
eval_violation::Kind as ProtoEvalViolationKind,
|
eval_violation::Kind as ProtoEvalViolationKind,
|
||||||
specific_meaning::Meaning as ProtoSpecificMeaningKind,
|
specific_meaning::Meaning as ProtoSpecificMeaningKind,
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
use arbiter_proto::{
|
use arbiter_proto::{
|
||||||
proto::user_agent::{
|
proto::{
|
||||||
UserAgentRequest, UserAgentResponse,
|
user_agent::{
|
||||||
user_agent_request::Payload as UserAgentRequestPayload,
|
UserAgentRequest, UserAgentResponse,
|
||||||
user_agent_response::Payload as UserAgentResponsePayload,
|
user_agent_request::Payload as UserAgentRequestPayload,
|
||||||
|
user_agent_response::Payload as UserAgentResponsePayload,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
transport::{Error as TransportError, Receiver, Sender, grpc::GrpcBi},
|
transport::{Error as TransportError, Receiver, Sender, grpc::GrpcBi},
|
||||||
};
|
};
|
||||||
@@ -17,7 +19,6 @@ use crate::{
|
|||||||
actors::user_agent::{OutOfBand, UserAgentConnection, UserAgentSession},
|
actors::user_agent::{OutOfBand, UserAgentConnection, UserAgentSession},
|
||||||
grpc::request_tracker::RequestTracker,
|
grpc::request_tracker::RequestTracker,
|
||||||
};
|
};
|
||||||
|
|
||||||
mod auth;
|
mod auth;
|
||||||
mod evm;
|
mod evm;
|
||||||
mod inbound;
|
mod inbound;
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
use arbiter_proto::{
|
use arbiter_proto::{
|
||||||
proto::user_agent::{
|
proto::user_agent::{
|
||||||
UserAgentRequest, UserAgentResponse,
|
UserAgentRequest, UserAgentResponse, auth::{
|
||||||
auth::{
|
|
||||||
self as proto_auth, AuthChallenge as ProtoAuthChallenge,
|
self as proto_auth, AuthChallenge as ProtoAuthChallenge,
|
||||||
AuthChallengeRequest as ProtoAuthChallengeRequest,
|
AuthChallengeRequest as ProtoAuthChallengeRequest,
|
||||||
AuthChallengeSolution as ProtoAuthChallengeSolution, AuthResult as ProtoAuthResult,
|
AuthChallengeSolution as ProtoAuthChallengeSolution, AuthResult as ProtoAuthResult,
|
||||||
KeyType as ProtoKeyType, request::Payload as AuthRequestPayload,
|
KeyType as ProtoKeyType, request::Payload as AuthRequestPayload,
|
||||||
response::Payload as AuthResponsePayload,
|
response::Payload as AuthResponsePayload,
|
||||||
},
|
}, user_agent_request::Payload as UserAgentRequestPayload,
|
||||||
user_agent_request::Payload as UserAgentRequestPayload,
|
|
||||||
user_agent_response::Payload as UserAgentResponsePayload,
|
user_agent_response::Payload as UserAgentResponsePayload,
|
||||||
},
|
},
|
||||||
transport::{Bi, Error as TransportError, Receiver, Sender, grpc::GrpcBi},
|
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 }) => {
|
Ok(Outbound::AuthChallenge { nonce }) => {
|
||||||
AuthResponsePayload::Challenge(ProtoAuthChallenge { nonce })
|
AuthResponsePayload::Challenge(ProtoAuthChallenge { nonce })
|
||||||
}
|
}
|
||||||
Ok(Outbound::AuthSuccess) => {
|
Ok(Outbound::AuthSuccess) => AuthResponsePayload::Result(ProtoAuthResult::Success.into()),
|
||||||
AuthResponsePayload::Result(ProtoAuthResult::Success.into())
|
|
||||||
}
|
|
||||||
Err(Error::UnregisteredPublicKey) => {
|
Err(Error::UnregisteredPublicKey) => {
|
||||||
AuthResponsePayload::Result(ProtoAuthResult::InvalidKey.into())
|
AuthResponsePayload::Result(ProtoAuthResult::InvalidKey.into())
|
||||||
}
|
}
|
||||||
@@ -175,9 +171,9 @@ impl Receiver<auth::Inbound> for AuthTransportAdapter<'_> {
|
|||||||
bootstrap_token,
|
bootstrap_token,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
AuthRequestPayload::ChallengeSolution(ProtoAuthChallengeSolution { signature }) => {
|
AuthRequestPayload::ChallengeSolution(ProtoAuthChallengeSolution {
|
||||||
Some(auth::Inbound::AuthChallengeSolution { signature })
|
signature,
|
||||||
}
|
}) => Some(auth::Inbound::AuthChallengeSolution { signature }),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ use arbiter_proto::proto::{
|
|||||||
EvmError as ProtoEvmError, EvmGrantCreateRequest, EvmGrantCreateResponse,
|
EvmError as ProtoEvmError, EvmGrantCreateRequest, EvmGrantCreateResponse,
|
||||||
EvmGrantDeleteRequest, EvmGrantDeleteResponse, EvmGrantList, EvmGrantListResponse,
|
EvmGrantDeleteRequest, EvmGrantDeleteResponse, EvmGrantList, EvmGrantListResponse,
|
||||||
EvmSignTransactionResponse, GrantEntry, WalletCreateResponse, WalletEntry, WalletList,
|
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_delete_response::Result as EvmGrantDeleteResult,
|
||||||
evm_grant_list_response::Result as EvmGrantListResult,
|
evm_grant_list_response::Result as EvmGrantListResult,
|
||||||
evm_sign_transaction_response::Result as EvmSignTransactionResult,
|
evm_sign_transaction_response::Result as EvmSignTransactionResult,
|
||||||
@@ -164,12 +165,7 @@ async fn handle_grant_delete(
|
|||||||
actor: &ActorRef<UserAgentSession>,
|
actor: &ActorRef<UserAgentSession>,
|
||||||
req: EvmGrantDeleteRequest,
|
req: EvmGrantDeleteRequest,
|
||||||
) -> Result<Option<UserAgentResponsePayload>, Status> {
|
) -> Result<Option<UserAgentResponsePayload>, Status> {
|
||||||
let result = match actor
|
let result = match actor.ask(HandleGrantDelete { grant_id: req.grant_id }).await {
|
||||||
.ask(HandleGrantDelete {
|
|
||||||
grant_id: req.grant_id,
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(()) => EvmGrantDeleteResult::Ok(()),
|
Ok(()) => EvmGrantDeleteResult::Ok(()),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
warn!(error = ?err, "Failed to delete EVM grant");
|
warn!(error = ?err, "Failed to delete EVM grant");
|
||||||
@@ -206,18 +202,18 @@ async fn handle_sign_transaction(
|
|||||||
signature.as_bytes().to_vec(),
|
signature.as_bytes().to_vec(),
|
||||||
)),
|
)),
|
||||||
},
|
},
|
||||||
Err(kameo::error::SendError::HandlerError(SessionSignTransactionError::Vet(vet_error))) => {
|
Err(kameo::error::SendError::HandlerError(
|
||||||
EvmSignTransactionResponse {
|
SessionSignTransactionError::Vet(vet_error),
|
||||||
result: Some(vet_error.convert()),
|
)) => EvmSignTransactionResponse {
|
||||||
}
|
result: Some(vet_error.convert()),
|
||||||
}
|
},
|
||||||
Err(kameo::error::SendError::HandlerError(SessionSignTransactionError::Internal)) => {
|
Err(kameo::error::SendError::HandlerError(
|
||||||
EvmSignTransactionResponse {
|
SessionSignTransactionError::Internal,
|
||||||
result: Some(EvmSignTransactionResult::Error(
|
)) => EvmSignTransactionResponse {
|
||||||
ProtoEvmError::Internal.into(),
|
result: Some(EvmSignTransactionResult::Error(
|
||||||
)),
|
ProtoEvmError::Internal.into(),
|
||||||
}
|
)),
|
||||||
}
|
},
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
warn!(error = ?err, "Failed to sign EVM transaction");
|
warn!(error = ?err, "Failed to sign EVM transaction");
|
||||||
EvmSignTransactionResponse {
|
EvmSignTransactionResponse {
|
||||||
@@ -228,7 +224,7 @@ async fn handle_sign_transaction(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Some(wrap_evm_response(
|
Ok(Some(wrap_evm_response(EvmResponsePayload::SignTransaction(
|
||||||
EvmResponsePayload::SignTransaction(response),
|
response,
|
||||||
)))
|
))))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ use arbiter_proto::proto::{
|
|||||||
TransactionRateLimit as ProtoTransactionRateLimit, VolumeRateLimit as ProtoVolumeRateLimit,
|
TransactionRateLimit as ProtoTransactionRateLimit, VolumeRateLimit as ProtoVolumeRateLimit,
|
||||||
specific_grant::Grant as ProtoSpecificGrantType,
|
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 chrono::{DateTime, Utc};
|
||||||
use prost_types::Timestamp as ProtoTimestamp;
|
use prost_types::Timestamp as ProtoTimestamp;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use arbiter_proto::proto::{
|
use arbiter_proto::proto::{
|
||||||
shared::ClientInfo as ProtoClientMetadata,
|
|
||||||
user_agent::{
|
user_agent::{
|
||||||
sdk_client::{
|
sdk_client::{
|
||||||
self as proto_sdk_client, ConnectionCancel as ProtoSdkClientConnectionCancel,
|
self as proto_sdk_client, ConnectionCancel as ProtoSdkClientConnectionCancel,
|
||||||
@@ -14,6 +13,7 @@ use arbiter_proto::proto::{
|
|||||||
},
|
},
|
||||||
user_agent_response::Payload as UserAgentResponsePayload,
|
user_agent_response::Payload as UserAgentResponsePayload,
|
||||||
},
|
},
|
||||||
|
shared::ClientInfo as ProtoClientMetadata,
|
||||||
};
|
};
|
||||||
use kameo::actor::ActorRef;
|
use kameo::actor::ActorRef;
|
||||||
use tonic::Status;
|
use tonic::Status;
|
||||||
@@ -62,22 +62,18 @@ pub(super) async fn dispatch(
|
|||||||
req: proto_sdk_client::Request,
|
req: proto_sdk_client::Request,
|
||||||
) -> Result<Option<UserAgentResponsePayload>, Status> {
|
) -> Result<Option<UserAgentResponsePayload>, Status> {
|
||||||
let Some(payload) = req.payload else {
|
let Some(payload) = req.payload else {
|
||||||
return Err(Status::invalid_argument(
|
return Err(Status::invalid_argument("Missing SDK client request payload"));
|
||||||
"Missing SDK client request payload",
|
|
||||||
));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
match payload {
|
match payload {
|
||||||
SdkClientRequestPayload::ConnectionResponse(resp) => {
|
SdkClientRequestPayload::ConnectionResponse(resp) => {
|
||||||
handle_connection_response(actor, resp).await
|
handle_connection_response(actor, resp).await
|
||||||
}
|
}
|
||||||
SdkClientRequestPayload::Revoke(_) => Err(Status::unimplemented(
|
SdkClientRequestPayload::Revoke(_) => {
|
||||||
"SdkClientRevoke is not yet implemented",
|
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::List(_) => handle_list(actor).await,
|
||||||
|
SdkClientRequestPayload::GrantWalletAccess(req) => handle_grant_wallet_access(actor, req).await,
|
||||||
SdkClientRequestPayload::RevokeWalletAccess(req) => {
|
SdkClientRequestPayload::RevokeWalletAccess(req) => {
|
||||||
handle_revoke_wallet_access(actor, req).await
|
handle_revoke_wallet_access(actor, req).await
|
||||||
}
|
}
|
||||||
@@ -132,11 +128,11 @@ async fn handle_list(
|
|||||||
ProtoSdkClientListResult::Error(ProtoSdkClientError::Internal.into())
|
ProtoSdkClientListResult::Error(ProtoSdkClientError::Internal.into())
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
Ok(Some(wrap_sdk_client_response(
|
Ok(Some(wrap_sdk_client_response(SdkClientResponsePayload::List(
|
||||||
SdkClientResponsePayload::List(ProtoSdkClientListResponse {
|
ProtoSdkClientListResponse {
|
||||||
result: Some(result),
|
result: Some(result),
|
||||||
}),
|
},
|
||||||
)))
|
))))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_grant_wallet_access(
|
async fn handle_grant_wallet_access(
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
use arbiter_proto::proto::shared::VaultState as ProtoVaultState;
|
|
||||||
use arbiter_proto::proto::user_agent::{
|
use arbiter_proto::proto::user_agent::{
|
||||||
user_agent_response::Payload as UserAgentResponsePayload,
|
user_agent_response::Payload as UserAgentResponsePayload,
|
||||||
vault::{
|
vault::{
|
||||||
@@ -12,21 +11,25 @@ use arbiter_proto::proto::user_agent::{
|
|||||||
unseal::{
|
unseal::{
|
||||||
self as proto_unseal, UnsealEncryptedKey as ProtoUnsealEncryptedKey,
|
self as proto_unseal, UnsealEncryptedKey as ProtoUnsealEncryptedKey,
|
||||||
UnsealResult as ProtoUnsealResult, UnsealStart,
|
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 kameo::{actor::ActorRef, error::SendError};
|
||||||
use tonic::Status;
|
use tonic::Status;
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
|
|
||||||
use crate::actors::{
|
use crate::{
|
||||||
keyholder::KeyHolderState,
|
actors::{
|
||||||
user_agent::{
|
keyholder::KeyHolderState,
|
||||||
UserAgentSession,
|
user_agent::{
|
||||||
session::connection::{
|
UserAgentSession,
|
||||||
BootstrapError, HandleBootstrapEncryptedKey, HandleQueryVaultState,
|
session::connection::{
|
||||||
HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError,
|
BootstrapError, HandleBootstrapEncryptedKey, HandleQueryVaultState,
|
||||||
|
HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -148,9 +151,7 @@ async fn handle_bootstrap_encrypted_key(
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(()) => ProtoBootstrapResult::Success,
|
Ok(()) => ProtoBootstrapResult::Success,
|
||||||
Err(SendError::HandlerError(BootstrapError::InvalidKey)) => {
|
Err(SendError::HandlerError(BootstrapError::InvalidKey)) => ProtoBootstrapResult::InvalidKey,
|
||||||
ProtoBootstrapResult::InvalidKey
|
|
||||||
}
|
|
||||||
Err(SendError::HandlerError(BootstrapError::AlreadyBootstrapped)) => {
|
Err(SendError::HandlerError(BootstrapError::AlreadyBootstrapped)) => {
|
||||||
ProtoBootstrapResult::AlreadyBootstrapped
|
ProtoBootstrapResult::AlreadyBootstrapped
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ use crate::context::ServerContext;
|
|||||||
|
|
||||||
pub mod actors;
|
pub mod actors;
|
||||||
pub mod context;
|
pub mod context;
|
||||||
pub mod crypto;
|
|
||||||
pub mod db;
|
pub mod db;
|
||||||
pub mod evm;
|
pub mod evm;
|
||||||
pub mod grpc;
|
pub mod grpc;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
|
|
||||||
use anyhow::anyhow;
|
|
||||||
use arbiter_proto::{proto::arbiter_service_server::ArbiterServiceServer, url::ArbiterUrl};
|
use arbiter_proto::{proto::arbiter_service_server::ArbiterServiceServer, url::ArbiterUrl};
|
||||||
use arbiter_server::{Server, actors::bootstrap::GetToken, context::ServerContext, db};
|
use arbiter_server::{Server, actors::bootstrap::GetToken, context::ServerContext, db};
|
||||||
|
use miette::miette;
|
||||||
use rustls::crypto::aws_lc_rs;
|
use rustls::crypto::aws_lc_rs;
|
||||||
use tonic::transport::{Identity, ServerTlsConfig};
|
use tonic::transport::{Identity, ServerTlsConfig};
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
@@ -10,7 +10,7 @@ use tracing::info;
|
|||||||
const PORT: u16 = 50051;
|
const PORT: u16 = 50051;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> anyhow::Result<()> {
|
async fn main() -> miette::Result<()> {
|
||||||
aws_lc_rs::default_provider().install_default().unwrap();
|
aws_lc_rs::default_provider().install_default().unwrap();
|
||||||
|
|
||||||
tracing_subscriber::fmt()
|
tracing_subscriber::fmt()
|
||||||
@@ -46,11 +46,11 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
tonic::transport::Server::builder()
|
tonic::transport::Server::builder()
|
||||||
.tls_config(tls)
|
.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)))
|
.add_service(ArbiterServiceServer::new(Server::new(context)))
|
||||||
.serve(addr)
|
.serve(addr)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| anyhow!("gRPC server error: {e}"))?;
|
.map_err(|e| miette::miette!("gRPC server error: {e}"))?;
|
||||||
|
|
||||||
unreachable!("gRPC server should run indefinitely");
|
unreachable!("gRPC server should run indefinitely");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use arbiter_server::{
|
use arbiter_server::{
|
||||||
actors::keyholder::{Error, KeyHolder},
|
actors::keyholder::{Error, KeyHolder},
|
||||||
crypto::encryption::v1::{Nonce, ROOT_KEY_TAG},
|
|
||||||
db::{self, models, schema},
|
db::{self, models, schema},
|
||||||
safe_cell::{SafeCell, SafeCellHandle as _},
|
safe_cell::{SafeCell, SafeCellHandle as _},
|
||||||
};
|
};
|
||||||
@@ -26,10 +25,16 @@ async fn test_bootstrap() {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(row.schema_version, 1);
|
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.ciphertext.is_empty());
|
||||||
assert!(!row.salt.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]
|
#[tokio::test]
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use arbiter_server::{
|
use arbiter_server::{
|
||||||
actors::keyholder::Error,
|
actors::keyholder::{Error, encryption::v1},
|
||||||
crypto::encryption::v1::Nonce,
|
|
||||||
db::{self, models, schema},
|
db::{self, models, schema},
|
||||||
safe_cell::{SafeCell, SafeCellHandle as _},
|
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");
|
assert_eq!(nonces.len(), unique.len(), "all nonces must be unique");
|
||||||
|
|
||||||
for (i, row) in rows.iter().enumerate() {
|
for (i, row) in rows.iter().enumerate() {
|
||||||
let mut expected = Nonce::default();
|
let mut expected = v1::Nonce::default();
|
||||||
for _ in 0..=i {
|
for _ in 0..=i {
|
||||||
expected.increment();
|
expected.increment();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,9 @@ use arbiter_server::{
|
|||||||
actors::{
|
actors::{
|
||||||
GlobalActors,
|
GlobalActors,
|
||||||
bootstrap::GetToken,
|
bootstrap::GetToken,
|
||||||
keyholder::Bootstrap,
|
|
||||||
user_agent::{AuthPublicKey, UserAgentConnection, auth},
|
user_agent::{AuthPublicKey, UserAgentConnection, auth},
|
||||||
},
|
},
|
||||||
db::{self, schema},
|
db::{self, schema},
|
||||||
safe_cell::{SafeCell, SafeCellHandle as _},
|
|
||||||
};
|
};
|
||||||
use diesel::{ExpressionMethods as _, QueryDsl, insert_into};
|
use diesel::{ExpressionMethods as _, QueryDsl, insert_into};
|
||||||
use diesel_async::RunQueryDsl;
|
use diesel_async::RunQueryDsl;
|
||||||
@@ -85,6 +83,7 @@ pub async fn test_bootstrap_invalid_token_auth() {
|
|||||||
Err(auth::Error::InvalidBootstrapToken)
|
Err(auth::Error::InvalidBootstrapToken)
|
||||||
));
|
));
|
||||||
|
|
||||||
|
// Verify no key was registered
|
||||||
let mut conn = db.get().await.unwrap();
|
let mut conn = db.get().await.unwrap();
|
||||||
let count: i64 = schema::useragent_client::table
|
let count: i64 = schema::useragent_client::table
|
||||||
.count()
|
.count()
|
||||||
@@ -103,6 +102,7 @@ pub async fn test_challenge_auth() {
|
|||||||
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
||||||
let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec();
|
let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec();
|
||||||
|
|
||||||
|
// Pre-register key with key_type
|
||||||
{
|
{
|
||||||
let mut conn = db.get().await.unwrap();
|
let mut conn = db.get().await.unwrap();
|
||||||
insert_into(schema::useragent_client::table)
|
insert_into(schema::useragent_client::table)
|
||||||
@@ -122,6 +122,7 @@ pub async fn test_challenge_auth() {
|
|||||||
auth::authenticate(&mut props, server_transport).await
|
auth::authenticate(&mut props, server_transport).await
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Send challenge request
|
||||||
test_transport
|
test_transport
|
||||||
.send(auth::Inbound::AuthChallengeRequest {
|
.send(auth::Inbound::AuthChallengeRequest {
|
||||||
pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()),
|
pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()),
|
||||||
@@ -130,6 +131,7 @@ pub async fn test_challenge_auth() {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
// Read the challenge response
|
||||||
let response = test_transport
|
let response = test_transport
|
||||||
.recv()
|
.recv()
|
||||||
.await
|
.await
|
||||||
@@ -164,57 +166,6 @@ pub async fn test_challenge_auth() {
|
|||||||
task.await.unwrap().unwrap();
|
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]
|
#[tokio::test]
|
||||||
#[test_log::test]
|
#[test_log::test]
|
||||||
pub async fn test_challenge_auth_rejects_invalid_signature() {
|
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 new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
||||||
let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec();
|
let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec();
|
||||||
|
|
||||||
|
// Pre-register key with key_type
|
||||||
{
|
{
|
||||||
let mut conn = db.get().await.unwrap();
|
let mut conn = db.get().await.unwrap();
|
||||||
insert_into(schema::useragent_client::table)
|
insert_into(schema::useragent_client::table)
|
||||||
@@ -263,6 +215,7 @@ pub async fn test_challenge_auth_rejects_invalid_signature() {
|
|||||||
Err(err) => panic!("Expected Ok response, got Err({err:?})"),
|
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 wrong_challenge = arbiter_proto::format_challenge(challenge + 1, &pubkey_bytes);
|
||||||
let signature = new_key.sign(&wrong_challenge);
|
let signature = new_key.sign(&wrong_challenge);
|
||||||
|
|
||||||
@@ -273,10 +226,8 @@ pub async fn test_challenge_auth_rejects_invalid_signature() {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let expected_err = task.await.unwrap();
|
|
||||||
println!("Received expected error: {expected_err:#?}");
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
expected_err,
|
task.await.unwrap(),
|
||||||
Err(auth::Error::InvalidChallengeSolution)
|
Err(auth::Error::InvalidChallengeSolution)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,17 +2,14 @@ use arbiter_server::{
|
|||||||
actors::{
|
actors::{
|
||||||
GlobalActors,
|
GlobalActors,
|
||||||
keyholder::{Bootstrap, Seal},
|
keyholder::{Bootstrap, Seal},
|
||||||
user_agent::{
|
user_agent::{UserAgentSession, session::connection::{
|
||||||
UserAgentSession,
|
HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError,
|
||||||
session::connection::{HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError},
|
}},
|
||||||
},
|
|
||||||
},
|
},
|
||||||
db,
|
db,
|
||||||
safe_cell::{SafeCell, SafeCellHandle as _},
|
safe_cell::{SafeCell, SafeCellHandle as _},
|
||||||
};
|
};
|
||||||
use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit};
|
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 kameo::actor::Spawn as _;
|
||||||
use x25519_dalek::{EphemeralSecret, PublicKey};
|
use x25519_dalek::{EphemeralSecret, PublicKey};
|
||||||
|
|
||||||
@@ -152,42 +149,3 @@ pub async fn test_unseal_retry_after_invalid_key() {
|
|||||||
assert!(matches!(response, Ok(())));
|
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
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
// Enrichment lookups — each watch scopes rebuilds to this card only
|
||||||
final walletAccesses =
|
final walletAccesses =
|
||||||
ref.watch(walletAccessListProvider).asData?.value ?? const [];
|
ref.watch(walletAccessListProvider).asData?.value ?? const [];
|
||||||
final wallets = ref.watch(evmProvider).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 theme = Theme.of(context);
|
||||||
final muted = Palette.ink.withValues(alpha: 0.62);
|
final muted = Palette.ink.withValues(alpha: 0.62);
|
||||||
|
|
||||||
|
// Resolve wallet_access_id → wallet address + client name
|
||||||
final accessById = <int, ua_sdk.WalletAccessEntry>{
|
final accessById = <int, ua_sdk.WalletAccessEntry>{
|
||||||
for (final a in walletAccesses) a.id: a,
|
for (final a in walletAccesses) a.id: a,
|
||||||
};
|
};
|
||||||
@@ -92,6 +94,7 @@ class GrantCard extends ConsumerWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
|
// Accent strip
|
||||||
Container(
|
Container(
|
||||||
width: 0.8.w,
|
width: 0.8.w,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -101,6 +104,7 @@ class GrantCard extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// Card body
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.symmetric(
|
padding: EdgeInsets.symmetric(
|
||||||
@@ -110,6 +114,7 @@ class GrantCard extends ConsumerWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
// Row 1: type badge · chain · spacer · revoke button
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
@@ -179,6 +184,7 @@ class GrantCard extends ConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
SizedBox(height: 0.8.h),
|
SizedBox(height: 0.8.h),
|
||||||
|
// Row 2: wallet address · client name
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
Reference in New Issue
Block a user