feat(user-agent): add VaultGate for sealed vault authentication
This commit is contained in:
@@ -5,7 +5,7 @@ use tracing::error;
|
||||
mod state;
|
||||
use state::*;
|
||||
|
||||
use super::UserAgentConnection;
|
||||
use super::{AuthCredentials, UserAgentConnection};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Inbound {
|
||||
@@ -69,7 +69,7 @@ fn parse_auth_event(payload: Inbound) -> AuthEvents {
|
||||
pub async fn authenticate<T>(
|
||||
props: &mut UserAgentConnection,
|
||||
transport: T,
|
||||
) -> Result<(i32, authn::PublicKey), Error>
|
||||
) -> Result<AuthCredentials, Error>
|
||||
where
|
||||
T: Bi<Inbound, Result<Outbound, Error>> + Send,
|
||||
{
|
||||
@@ -82,7 +82,7 @@ where
|
||||
};
|
||||
|
||||
match state.process_event(parse_auth_event(payload)).await {
|
||||
Ok(AuthStates::AuthOk(result)) => return Ok((result.id, result.pubkey.clone())),
|
||||
Ok(AuthStates::AuthOk(result)) => return Ok(result.clone()),
|
||||
Err(AuthError::ActionFailed(err)) => {
|
||||
error!(?err, "State machine action failed");
|
||||
return Err(err);
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::super::{UserAgentConnection, UserAgentCredentials};
|
||||
use super::super::{AuthCredentials, Credentials, UserAgentConnection};
|
||||
use arbiter_crypto::authn::{self, USERAGENT_CONTEXT};
|
||||
use arbiter_proto::transport::Bi;
|
||||
use diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl, update};
|
||||
use diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl, sqlite::Sqlite, update};
|
||||
use diesel_async::{AsyncConnection, RunQueryDsl};
|
||||
use kameo::actor::ActorRef;
|
||||
use tracing::error;
|
||||
@@ -33,21 +33,18 @@ pub struct ChallengeSolution {
|
||||
pub solution: Vec<u8>,
|
||||
}
|
||||
|
||||
pub struct AuthOk {
|
||||
pub id: i32,
|
||||
pub pubkey: authn::PublicKey,
|
||||
}
|
||||
|
||||
smlang::statemachine!(
|
||||
name: Auth,
|
||||
custom_error: true,
|
||||
transitions: {
|
||||
*Init + AuthRequest(ChallengeRequest) / async prepare_challenge = SentChallenge(ChallengeContext),
|
||||
Init + BootstrapAuthRequest(BootstrapAuthRequest) / async verify_bootstrap_token = AuthOk(AuthOk),
|
||||
SentChallenge(ChallengeContext) + ReceivedSolution(ChallengeSolution) / async verify_solution = AuthOk(AuthOk),
|
||||
Init + BootstrapAuthRequest(BootstrapAuthRequest) / async verify_bootstrap_token = AuthOk(AuthCredentials),
|
||||
SentChallenge(ChallengeContext) + ReceivedSolution(ChallengeSolution) / async verify_solution = AuthOk(AuthCredentials),
|
||||
}
|
||||
);
|
||||
|
||||
const NONCE_START: i32 = 1;
|
||||
|
||||
/// Returns the current nonce, ready to use for the challenge nonce.
|
||||
async fn get_current_nonce_and_id(
|
||||
db: &DatabasePool,
|
||||
@@ -94,9 +91,12 @@ async fn verify_integrity(
|
||||
let _result = integrity::verify_entity(
|
||||
&mut db_conn,
|
||||
vault,
|
||||
&UserAgentCredentials {
|
||||
pubkey: pubkey.clone(),
|
||||
nonce,
|
||||
&AuthCredentials {
|
||||
creds: Credentials {
|
||||
id,
|
||||
pubkey: pubkey.clone(),
|
||||
},
|
||||
new_nonce: nonce,
|
||||
},
|
||||
id,
|
||||
)
|
||||
@@ -109,49 +109,46 @@ async fn verify_integrity(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_nonce(
|
||||
db: &DatabasePool,
|
||||
vault: &ActorRef<Vault>,
|
||||
async fn compute_current_nonce(
|
||||
conn: &mut impl AsyncConnection<Backend = Sqlite>,
|
||||
pubkey: &authn::PublicKey,
|
||||
) -> Result<(i32, i32), Error> {
|
||||
let mut db_conn = db.get().await.map_err(|e| {
|
||||
error!(error = ?e, "Database pool error");
|
||||
Error::internal("Database unavailable")
|
||||
})?;
|
||||
let (id, new_nonce) = db_conn
|
||||
.exclusive_transaction(|conn| {
|
||||
Box::pin(async move {
|
||||
let (id, new_nonce): (i32, i32) = update(useragent_client::table)
|
||||
.filter(useragent_client::public_key.eq(pubkey.to_bytes()))
|
||||
.set(useragent_client::nonce.eq(useragent_client::nonce + 1))
|
||||
.returning((useragent_client::id, useragent_client::nonce))
|
||||
.get_result(conn)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!(error = ?e, "Database error");
|
||||
Error::internal("Database operation failed")
|
||||
})?;
|
||||
|
||||
integrity::sign_entity(
|
||||
conn,
|
||||
vault,
|
||||
&UserAgentCredentials {
|
||||
pubkey: pubkey.clone(),
|
||||
nonce: new_nonce,
|
||||
},
|
||||
id,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!(?e, "Integrity signature update failed");
|
||||
Error::internal("Database error")
|
||||
})?;
|
||||
|
||||
Result::<_, Error>::Ok((id, new_nonce))
|
||||
})
|
||||
update(useragent_client::table)
|
||||
.filter(useragent_client::public_key.eq(pubkey.to_bytes()))
|
||||
.set(useragent_client::nonce.eq(useragent_client::nonce + 1))
|
||||
.returning((useragent_client::id, useragent_client::nonce))
|
||||
.get_result(conn)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!(error = ?e, "Database error incrementing nonce");
|
||||
Error::internal("Database operation failed")
|
||||
})
|
||||
.await?;
|
||||
Ok((id, new_nonce))
|
||||
}
|
||||
|
||||
async fn resign_credentials(
|
||||
conn: &mut impl AsyncConnection<Backend = Sqlite>,
|
||||
vault: &ActorRef<Vault>,
|
||||
id: i32,
|
||||
pubkey: &authn::PublicKey,
|
||||
new_nonce: i32,
|
||||
) -> Result<(), Error> {
|
||||
integrity::sign_entity(
|
||||
conn,
|
||||
vault,
|
||||
&AuthCredentials {
|
||||
creds: Credentials {
|
||||
id,
|
||||
pubkey: pubkey.clone(),
|
||||
},
|
||||
new_nonce,
|
||||
},
|
||||
id,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!(?e, "Integrity signature update failed");
|
||||
Error::internal("Database error")
|
||||
})
|
||||
}
|
||||
|
||||
async fn register_key(db: &DatabasePool, pubkey: &authn::PublicKey) -> Result<i32, Error> {
|
||||
@@ -161,8 +158,6 @@ async fn register_key(db: &DatabasePool, pubkey: &authn::PublicKey) -> Result<i3
|
||||
Error::internal("Database unavailable")
|
||||
})?;
|
||||
|
||||
const NONCE_START: i32 = 1;
|
||||
|
||||
let id: i32 = diesel::insert_into(useragent_client::table)
|
||||
.values((
|
||||
useragent_client::public_key.eq(pubkey_bytes),
|
||||
@@ -200,9 +195,33 @@ where
|
||||
&mut self,
|
||||
ChallengeRequest { pubkey }: ChallengeRequest,
|
||||
) -> Result<ChallengeContext, Self::Error> {
|
||||
verify_integrity(&self.conn.db, &self.conn.actors.vault, &pubkey).await?;
|
||||
let is_signing = integrity::is_signing_available(&self.conn.actors.vault)
|
||||
.await
|
||||
.unwrap_or(false);
|
||||
|
||||
let (id, nonce) = create_nonce(&self.conn.db, &self.conn.actors.vault, &pubkey).await?;
|
||||
if is_signing {
|
||||
verify_integrity(&self.conn.db, &self.conn.actors.vault, &pubkey).await?;
|
||||
}
|
||||
|
||||
let vault = self.conn.actors.vault.clone();
|
||||
let mut conn = self.conn.db.get().await.map_err(|e| {
|
||||
error!(error = ?e, "Database pool error");
|
||||
Error::internal("Database unavailable")
|
||||
})?;
|
||||
|
||||
let (id, nonce) = conn
|
||||
.exclusive_transaction(|conn| {
|
||||
let pubkey = pubkey.clone();
|
||||
let vault = vault.clone();
|
||||
Box::pin(async move {
|
||||
let (id, new_nonce) = compute_current_nonce(conn, &pubkey).await?;
|
||||
if is_signing {
|
||||
resign_credentials(conn, &vault, id, &pubkey, new_nonce).await?;
|
||||
}
|
||||
Result::<_, Error>::Ok((id, new_nonce))
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
|
||||
self.transport
|
||||
.send(Ok(Outbound::AuthChallenge { nonce }))
|
||||
@@ -224,7 +243,7 @@ where
|
||||
async fn verify_bootstrap_token(
|
||||
&mut self,
|
||||
BootstrapAuthRequest { pubkey, token }: BootstrapAuthRequest,
|
||||
) -> Result<AuthOk, Self::Error> {
|
||||
) -> Result<AuthCredentials, Self::Error> {
|
||||
let token_ok: bool = self
|
||||
.conn
|
||||
.actors
|
||||
@@ -245,12 +264,15 @@ where
|
||||
|
||||
match token_ok {
|
||||
true => {
|
||||
let id = register_key(&self.conn.db, &pubkey).await?;
|
||||
let id = register_key(&self.conn.db, &pubkey).await?;
|
||||
self.transport
|
||||
.send(Ok(Outbound::AuthSuccess))
|
||||
.await
|
||||
.map_err(|_| Error::Transport)?;
|
||||
Ok(AuthOk { id, pubkey })
|
||||
Ok(AuthCredentials {
|
||||
creds: Credentials { id, pubkey },
|
||||
new_nonce: NONCE_START,
|
||||
})
|
||||
}
|
||||
false => {
|
||||
error!("Invalid bootstrap token provided");
|
||||
@@ -273,7 +295,7 @@ where
|
||||
key,
|
||||
}: &ChallengeContext,
|
||||
ChallengeSolution { solution }: ChallengeSolution,
|
||||
) -> Result<AuthOk, Self::Error> {
|
||||
) -> Result<AuthCredentials, Self::Error> {
|
||||
let signature = authn::Signature::try_from(solution.as_slice()).map_err(|_| {
|
||||
error!("Failed to decode signature in challenge solution");
|
||||
Error::InvalidChallengeSolution
|
||||
@@ -287,7 +309,13 @@ where
|
||||
.send(Ok(Outbound::AuthSuccess))
|
||||
.await
|
||||
.map_err(|_| Error::Transport)?;
|
||||
Ok(AuthOk { id: *id, pubkey: key.clone() })
|
||||
Ok(AuthCredentials {
|
||||
creds: Credentials {
|
||||
id: *id,
|
||||
pubkey: key.clone(),
|
||||
},
|
||||
new_nonce: *challenge_nonce,
|
||||
})
|
||||
}
|
||||
false => {
|
||||
self.transport
|
||||
|
||||
@@ -3,13 +3,45 @@ use crate::{
|
||||
};
|
||||
use arbiter_crypto::authn;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct UserAgentCredentials {
|
||||
pub mod auth;
|
||||
pub mod session;
|
||||
pub mod vault_gate;
|
||||
|
||||
|
||||
#[derive(Debug, Clone, Hash)]
|
||||
pub struct Credentials {
|
||||
pub id: i32,
|
||||
pub pubkey: authn::PublicKey,
|
||||
pub nonce: i32,
|
||||
}
|
||||
impl Hashable for Credentials {
|
||||
fn hash<H: sha2::Digest>(&self, hasher: &mut H) {
|
||||
self.id.hash(hasher);
|
||||
self.pubkey.hash(hasher);
|
||||
}
|
||||
}
|
||||
|
||||
impl Integrable for UserAgentCredentials {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AuthCredentials {
|
||||
pub creds: Credentials,
|
||||
// denotes new nonce, not current
|
||||
pub new_nonce: i32,
|
||||
}
|
||||
|
||||
impl Hashable for authn::PublicKey {
|
||||
fn hash<H: sha2::Digest>(&self, hasher: &mut H) {
|
||||
hasher.update(self.to_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
impl Hashable for AuthCredentials {
|
||||
fn hash<H: sha2::Digest>(&self, hasher: &mut H) {
|
||||
self.creds.hash(hasher);
|
||||
self.new_nonce.hash(hasher);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl Integrable for AuthCredentials {
|
||||
const KIND: &'static str = "useragent_credentials";
|
||||
}
|
||||
|
||||
@@ -31,23 +63,9 @@ impl UserAgentConnection {
|
||||
}
|
||||
}
|
||||
|
||||
pub mod auth;
|
||||
pub mod session;
|
||||
|
||||
|
||||
pub use auth::authenticate;
|
||||
pub use session::UserAgentSession;
|
||||
|
||||
use crate::crypto::integrity::hashing::Hashable;
|
||||
|
||||
impl Hashable for authn::PublicKey {
|
||||
fn hash<H: sha2::Digest>(&self, hasher: &mut H) {
|
||||
hasher.update(self.to_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
impl Hashable for UserAgentCredentials {
|
||||
fn hash<H: sha2::Digest>(&self, hasher: &mut H) {
|
||||
self.pubkey.hash(hasher);
|
||||
self.nonce.hash(hasher);
|
||||
}
|
||||
}
|
||||
use crate::crypto::integrity::hashing::Hashable;
|
||||
@@ -28,88 +28,10 @@ use crate::db::models::{
|
||||
use crate::evm::policies::{Grant, SpecificGrant};
|
||||
use crate::{
|
||||
actors::vault::VaultState,
|
||||
peers::user_agent::session::state::{UnsealContext, UserAgentEvents},
|
||||
};
|
||||
|
||||
use super::{Error, UserAgentSession, state};
|
||||
use super::{Error, UserAgentSession};
|
||||
|
||||
impl UserAgentSession {
|
||||
fn take_unseal_secret(&mut self) -> Result<(EphemeralSecret, PublicKey), Error> {
|
||||
let state::UserAgentStates::WaitingForUnsealKey(unseal_context) = self.state.state() else {
|
||||
error!("Received encrypted key in invalid state");
|
||||
return Err(Error::internal("Invalid state for unseal encrypted key"));
|
||||
};
|
||||
|
||||
let ephemeral_secret = {
|
||||
#[allow(
|
||||
clippy::unwrap_used,
|
||||
reason = "Mutex poison is unrecoverable and should panic"
|
||||
)]
|
||||
let mut secret_lock = unseal_context.secret.lock().unwrap();
|
||||
let secret = secret_lock.take();
|
||||
match secret {
|
||||
Some(secret) => secret,
|
||||
None => {
|
||||
drop(secret_lock);
|
||||
error!("Ephemeral secret already taken");
|
||||
return Err(Error::internal("Ephemeral secret already taken"));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok((ephemeral_secret, unseal_context.client_public_key))
|
||||
}
|
||||
|
||||
fn decrypt_client_key_material(
|
||||
ephemeral_secret: EphemeralSecret,
|
||||
client_public_key: PublicKey,
|
||||
nonce: &[u8],
|
||||
ciphertext: &[u8],
|
||||
associated_data: &[u8],
|
||||
) -> Result<SafeCell<Vec<u8>>, ()> {
|
||||
let nonce = XNonce::from_slice(nonce);
|
||||
|
||||
let shared_secret = ephemeral_secret.diffie_hellman(&client_public_key);
|
||||
let cipher = XChaCha20Poly1305::new(shared_secret.as_bytes().into());
|
||||
|
||||
let mut key_buffer = SafeCell::new(ciphertext.to_vec());
|
||||
|
||||
let decryption_result = key_buffer.write_inline(|write_handle| {
|
||||
cipher.decrypt_in_place(nonce, associated_data, write_handle)
|
||||
});
|
||||
|
||||
match decryption_result {
|
||||
Ok(_) => Ok(key_buffer),
|
||||
Err(err) => {
|
||||
error!(?err, "Failed to decrypt encrypted key material");
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UnsealStartResponse {
|
||||
pub server_pubkey: PublicKey,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum UnsealError {
|
||||
#[error("Invalid key provided for unsealing")]
|
||||
InvalidKey,
|
||||
#[error("Internal error during unsealing process")]
|
||||
General(#[from] super::Error),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum BootstrapError {
|
||||
#[error("Invalid key provided for bootstrapping")]
|
||||
InvalidKey,
|
||||
#[error("Vault is already bootstrapped")]
|
||||
AlreadyBootstrapped,
|
||||
|
||||
#[error("Internal error during bootstrapping process")]
|
||||
General(#[from] super::Error),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum SignTransactionError {
|
||||
@@ -129,153 +51,6 @@ pub enum GrantMutationError {
|
||||
Internal,
|
||||
}
|
||||
|
||||
#[messages]
|
||||
impl UserAgentSession {
|
||||
#[message]
|
||||
pub async fn handle_unseal_request(
|
||||
&mut self,
|
||||
client_pubkey: x25519_dalek::PublicKey,
|
||||
) -> Result<UnsealStartResponse, Error> {
|
||||
let secret = EphemeralSecret::random();
|
||||
let public_key = PublicKey::from(&secret);
|
||||
|
||||
self.transition(UserAgentEvents::UnsealRequest(UnsealContext {
|
||||
secret: Mutex::new(Some(secret)),
|
||||
client_public_key: client_pubkey,
|
||||
}))?;
|
||||
|
||||
Ok(UnsealStartResponse {
|
||||
server_pubkey: public_key,
|
||||
})
|
||||
}
|
||||
|
||||
#[message]
|
||||
pub async fn handle_unseal_encrypted_key(
|
||||
&mut self,
|
||||
nonce: Vec<u8>,
|
||||
ciphertext: Vec<u8>,
|
||||
associated_data: Vec<u8>,
|
||||
) -> Result<(), UnsealError> {
|
||||
let (ephemeral_secret, client_public_key) = match self.take_unseal_secret() {
|
||||
Ok(values) => values,
|
||||
Err(Error::State) => {
|
||||
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
||||
return Err(UnsealError::InvalidKey);
|
||||
}
|
||||
Err(_err) => {
|
||||
return Err(Error::internal("Failed to take unseal secret").into());
|
||||
}
|
||||
};
|
||||
|
||||
let seal_key_buffer = match Self::decrypt_client_key_material(
|
||||
ephemeral_secret,
|
||||
client_public_key,
|
||||
&nonce,
|
||||
&ciphertext,
|
||||
&associated_data,
|
||||
) {
|
||||
Ok(buffer) => buffer,
|
||||
Err(()) => {
|
||||
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
||||
return Err(UnsealError::InvalidKey);
|
||||
}
|
||||
};
|
||||
|
||||
match self
|
||||
.props
|
||||
.actors
|
||||
.vault
|
||||
.ask(TryUnseal {
|
||||
seal_key_raw: seal_key_buffer,
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
info!("Successfully unsealed key with client-provided key");
|
||||
self.transition(UserAgentEvents::ReceivedValidKey)?;
|
||||
Ok(())
|
||||
}
|
||||
Err(SendError::HandlerError(vault::Error::InvalidKey)) => {
|
||||
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
||||
Err(UnsealError::InvalidKey)
|
||||
}
|
||||
Err(SendError::HandlerError(err)) => {
|
||||
error!(?err, "Vault failed to unseal key");
|
||||
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
||||
Err(UnsealError::InvalidKey)
|
||||
}
|
||||
Err(err) => {
|
||||
error!(?err, "Failed to send unseal request to vault");
|
||||
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
||||
Err(Error::internal("Vault actor error").into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[message]
|
||||
pub(crate) async fn handle_bootstrap_encrypted_key(
|
||||
&mut self,
|
||||
nonce: Vec<u8>,
|
||||
ciphertext: Vec<u8>,
|
||||
associated_data: Vec<u8>,
|
||||
) -> Result<(), BootstrapError> {
|
||||
let (ephemeral_secret, client_public_key) = match self.take_unseal_secret() {
|
||||
Ok(values) => values,
|
||||
Err(Error::State) => {
|
||||
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
||||
return Err(BootstrapError::InvalidKey);
|
||||
}
|
||||
Err(err) => return Err(err.into()),
|
||||
};
|
||||
|
||||
let seal_key_buffer = match Self::decrypt_client_key_material(
|
||||
ephemeral_secret,
|
||||
client_public_key,
|
||||
&nonce,
|
||||
&ciphertext,
|
||||
&associated_data,
|
||||
) {
|
||||
Ok(buffer) => buffer,
|
||||
Err(()) => {
|
||||
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
||||
return Err(BootstrapError::InvalidKey);
|
||||
}
|
||||
};
|
||||
|
||||
match self
|
||||
.props
|
||||
.actors
|
||||
.vault
|
||||
.ask(Bootstrap {
|
||||
seal_key_raw: seal_key_buffer,
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
info!("Successfully bootstrapped vault with client-provided key");
|
||||
self.transition(UserAgentEvents::ReceivedValidKey)?;
|
||||
Ok(())
|
||||
}
|
||||
Err(SendError::HandlerError(vault::Error::AlreadyBootstrapped)) => {
|
||||
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
||||
Err(BootstrapError::AlreadyBootstrapped)
|
||||
}
|
||||
Err(SendError::HandlerError(err)) => {
|
||||
error!(?err, "Vault failed to bootstrap vault");
|
||||
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
||||
Err(BootstrapError::InvalidKey)
|
||||
}
|
||||
Err(err) => {
|
||||
error!(?err, "Failed to send bootstrap request to vault");
|
||||
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
||||
Err(BootstrapError::General(Error::internal(
|
||||
"Vault actor error",
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[messages]
|
||||
impl UserAgentSession {
|
||||
#[message]
|
||||
@@ -1,12 +1,11 @@
|
||||
use arbiter_crypto::authn;
|
||||
use diesel::{ExpressionMethods, QueryDsl};
|
||||
use diesel_async::{AsyncConnection, RunQueryDsl};
|
||||
use diesel_async::{RunQueryDsl};
|
||||
use kameo_actors::message_bus::Register;
|
||||
|
||||
use std::{borrow::Cow, collections::HashMap};
|
||||
|
||||
use arbiter_proto::transport::Sender;
|
||||
use async_trait::async_trait;
|
||||
use kameo::{Actor, actor::ActorRef, messages, prelude::Message};
|
||||
use thiserror::Error;
|
||||
use tracing::error;
|
||||
@@ -15,10 +14,8 @@ use crate::{
|
||||
actors::{
|
||||
flow_coordinator::{RegisterUserAgent, client_connect_approval::ClientApprovalController},
|
||||
vault::events,
|
||||
}, crypto::integrity, db::schema::useragent_client, peers::{client::ClientProfile, user_agent::UserAgentCredentials}
|
||||
}, crypto::integrity, db::schema::useragent_client, peers::{client::ClientProfile, user_agent::{AuthCredentials, Credentials}}
|
||||
};
|
||||
mod state;
|
||||
use state::{DummyContext, UserAgentEvents, UserAgentStateMachine};
|
||||
|
||||
use super::{OutOfBand, UserAgentConnection};
|
||||
|
||||
@@ -58,41 +55,28 @@ pub struct PendingClientApproval {
|
||||
}
|
||||
|
||||
pub struct UserAgentSession {
|
||||
id: i32,
|
||||
pubkey: authn::PublicKey,
|
||||
creds: Credentials,
|
||||
props: UserAgentConnection,
|
||||
state: UserAgentStateMachine<DummyContext>,
|
||||
sender: Box<dyn Sender<OutOfBand>>,
|
||||
|
||||
pending_client_approvals: HashMap<Vec<u8>, PendingClientApproval>,
|
||||
}
|
||||
|
||||
pub mod connection;
|
||||
pub mod handlers;
|
||||
|
||||
impl UserAgentSession {
|
||||
pub(crate) fn new(
|
||||
props: UserAgentConnection,
|
||||
id: i32,
|
||||
pubkey: authn::PublicKey,
|
||||
creds: Credentials,
|
||||
sender: Box<dyn Sender<OutOfBand>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
creds,
|
||||
props,
|
||||
pubkey,
|
||||
state: UserAgentStateMachine::new(DummyContext),
|
||||
sender,
|
||||
pending_client_approvals: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn transition(&mut self, event: UserAgentEvents) -> Result<(), Error> {
|
||||
self.state.process_event(event).map_err(|e| {
|
||||
error!(?e, "State transition failed");
|
||||
Error::State
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[messages]
|
||||
@@ -128,61 +112,6 @@ impl UserAgentSession {
|
||||
}
|
||||
}
|
||||
|
||||
impl Message<events::VaultBootstrapped> for UserAgentSession {
|
||||
type Reply = Result<(), Error>;
|
||||
|
||||
async fn handle(
|
||||
&mut self,
|
||||
_: events::VaultBootstrapped,
|
||||
ctx: &mut kameo::prelude::Context<Self, Self::Reply>,
|
||||
) -> Self::Reply {
|
||||
let Ok(mut conn) = self.props.db.get().await else {
|
||||
error!("Failed to get database connection for vault bootstrapped event");
|
||||
ctx.stop();
|
||||
return Err(Error::internal("Failed to get database connection"));
|
||||
};
|
||||
|
||||
|
||||
let result = conn.exclusive_transaction(|conn| {
|
||||
Box::pin(async {
|
||||
let nonce: i32 = useragent_client::table
|
||||
.filter(useragent_client::id.eq(self.id))
|
||||
.select(useragent_client::nonce)
|
||||
.first::<i32>(conn)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!(?e, "Failed to get nonce for useragent bootstrapping");
|
||||
Error::internal("Failed to sign user agent credentials")
|
||||
})?;
|
||||
|
||||
let entity = UserAgentCredentials {
|
||||
pubkey: self.pubkey.clone(),
|
||||
nonce,
|
||||
};
|
||||
|
||||
integrity::sign_entity(conn, &self.props.actors.vault, &entity, self.id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!(?e, "Failed to sign user agent credentials during vault bootstrapping");
|
||||
Error::internal("Failed to sign user agent credentials")
|
||||
})?;
|
||||
|
||||
Result::<_, Error>::Ok(())
|
||||
})
|
||||
}).await;
|
||||
|
||||
match result {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => {
|
||||
error!(?err, "Error during vault bootstrapping");
|
||||
ctx.stop();
|
||||
Err(err)
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
impl Actor for UserAgentSession {
|
||||
type Args = Self;
|
||||
|
||||
@@ -192,21 +121,6 @@ impl Actor for UserAgentSession {
|
||||
args: Self::Args,
|
||||
this: kameo::prelude::ActorRef<Self>,
|
||||
) -> Result<Self, Self::Error> {
|
||||
args.props
|
||||
.actors
|
||||
.events
|
||||
.tell(Register(
|
||||
this.clone().recipient::<events::VaultBootstrapped>(),
|
||||
))
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!(
|
||||
?err,
|
||||
"Failed to register user agent connection with event bus"
|
||||
);
|
||||
Error::internal("Failed to register user agent connection with event bus")
|
||||
})?;
|
||||
|
||||
args.props
|
||||
.actors
|
||||
.flow_coordinator
|
||||
@@ -1,27 +0,0 @@
|
||||
use std::sync::Mutex;
|
||||
|
||||
use x25519_dalek::{EphemeralSecret, PublicKey};
|
||||
|
||||
pub struct UnsealContext {
|
||||
pub client_public_key: PublicKey,
|
||||
pub secret: Mutex<Option<EphemeralSecret>>,
|
||||
}
|
||||
|
||||
smlang::statemachine!(
|
||||
name: UserAgent,
|
||||
custom_error: false,
|
||||
transitions: {
|
||||
*Idle + UnsealRequest(UnsealContext) / generate_temp_keypair = WaitingForUnsealKey(UnsealContext),
|
||||
WaitingForUnsealKey(UnsealContext) + ReceivedValidKey = Unsealed,
|
||||
WaitingForUnsealKey(UnsealContext) + ReceivedInvalidKey = Idle,
|
||||
}
|
||||
);
|
||||
|
||||
pub struct DummyContext;
|
||||
impl UserAgentStateMachineContext for DummyContext {
|
||||
#[allow(missing_docs)]
|
||||
#[allow(clippy::unused_unit)]
|
||||
fn generate_temp_keypair(&mut self, event_data: UnsealContext) -> Result<UnsealContext, ()> {
|
||||
Ok(event_data)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
|
||||
use chacha20poly1305::{AeadInPlace, KeyInit as _, XChaCha20Poly1305, XNonce};
|
||||
use kameo::{Actor, error::SendError, messages, prelude::Message};
|
||||
use kameo_actors::message_bus::Register;
|
||||
use tokio::sync::oneshot;
|
||||
use tracing::{error, info};
|
||||
use x25519_dalek::{EphemeralSecret, PublicKey, SharedSecret};
|
||||
|
||||
pub mod state;
|
||||
use state::*;
|
||||
|
||||
use super::{AuthCredentials, Credentials};
|
||||
use crate::{
|
||||
actors::{
|
||||
GlobalActors,
|
||||
vault::{self, Bootstrap, TryUnseal, events},
|
||||
},
|
||||
crypto::integrity::{self, AttestationStatus},
|
||||
db::DatabasePool,
|
||||
};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("Vault is already bootstrapped")]
|
||||
AlreadyBootstrapped,
|
||||
#[error("Invalid key provided")]
|
||||
InvalidKey,
|
||||
|
||||
#[error("State transition failed")]
|
||||
State,
|
||||
|
||||
#[error("Internal error: {0}")]
|
||||
Internal(String),
|
||||
}
|
||||
impl Error {
|
||||
fn internal(message: impl Into<String>) -> Self {
|
||||
Self::Internal(message.into())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct HandshakeResponse {
|
||||
pub server_pubkey: PublicKey,
|
||||
}
|
||||
|
||||
pub struct VaultGate {
|
||||
pub auth_creds: AuthCredentials,
|
||||
pub promotion_tx: Option<oneshot::Sender<Result<Credentials, Error>>>,
|
||||
pub state: State,
|
||||
pub actors: GlobalActors,
|
||||
pub db: DatabasePool,
|
||||
}
|
||||
|
||||
impl VaultGate {
|
||||
pub fn new(
|
||||
auth_creds: AuthCredentials,
|
||||
actors: GlobalActors,
|
||||
db: DatabasePool,
|
||||
promotion_tx: oneshot::Sender<Result<Credentials, Error>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
auth_creds,
|
||||
state: State::default(),
|
||||
actors,
|
||||
db,
|
||||
promotion_tx: Some(promotion_tx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Actor for VaultGate {
|
||||
type Args = Self;
|
||||
|
||||
type Error = ();
|
||||
|
||||
async fn on_start(
|
||||
args: Self::Args,
|
||||
actor_ref: kameo::prelude::ActorRef<Self>,
|
||||
) -> Result<Self, Self::Error> {
|
||||
let _ = args
|
||||
.actors
|
||||
.events
|
||||
.tell(Register(
|
||||
actor_ref.clone().recipient::<events::Bootstrapped>(),
|
||||
))
|
||||
.await;
|
||||
let _ = args
|
||||
.actors
|
||||
.events
|
||||
.tell(Register(actor_ref.recipient::<events::Unsealed>()))
|
||||
.await;
|
||||
Ok(args)
|
||||
}
|
||||
}
|
||||
|
||||
impl VaultGate {
|
||||
fn decrypt_key(
|
||||
secret: &SharedSecret,
|
||||
nonce: &[u8],
|
||||
ciphertext: &[u8],
|
||||
associated_data: &[u8],
|
||||
) -> Result<SafeCell<Vec<u8>>, ()> {
|
||||
let nonce = XNonce::from_slice(nonce);
|
||||
|
||||
let cipher = XChaCha20Poly1305::new(secret.as_bytes().into());
|
||||
|
||||
let mut key_buffer = SafeCell::new(ciphertext.to_vec());
|
||||
|
||||
let decryption_result = key_buffer.write_inline(|write_handle| {
|
||||
cipher.decrypt_in_place(nonce, associated_data, write_handle)
|
||||
});
|
||||
|
||||
match decryption_result {
|
||||
Ok(_) => Ok(key_buffer),
|
||||
Err(err) => {
|
||||
error!(?err, "Failed to decrypt encrypted key material");
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[messages]
|
||||
impl VaultGate {
|
||||
#[message]
|
||||
pub async fn handle_handshake(
|
||||
&mut self,
|
||||
client_pubkey: x25519_dalek::PublicKey,
|
||||
) -> Result<HandshakeResponse, Error> {
|
||||
let ephemeral_secret = EphemeralSecret::random();
|
||||
let public_key = PublicKey::from(&ephemeral_secret);
|
||||
|
||||
let secret = ephemeral_secret.diffie_hellman(&client_pubkey);
|
||||
|
||||
self.state = State::ReadyForExchange {
|
||||
server_key: public_key.clone(),
|
||||
secret,
|
||||
};
|
||||
|
||||
Ok(HandshakeResponse {
|
||||
server_pubkey: public_key,
|
||||
})
|
||||
}
|
||||
|
||||
#[message]
|
||||
pub async fn handle_unseal_encrypted_key(
|
||||
&mut self,
|
||||
nonce: Vec<u8>,
|
||||
ciphertext: Vec<u8>,
|
||||
associated_data: Vec<u8>,
|
||||
) -> Result<(), Error> {
|
||||
let State::ReadyForExchange { secret, .. } = &self.state else {
|
||||
return Err(Error::State);
|
||||
};
|
||||
|
||||
let seal_key_buffer = match Self::decrypt_key(secret, &nonce, &ciphertext, &associated_data)
|
||||
{
|
||||
Ok(buffer) => buffer,
|
||||
Err(()) => {
|
||||
return Err(Error::InvalidKey);
|
||||
}
|
||||
};
|
||||
|
||||
match self
|
||||
.actors
|
||||
.vault
|
||||
.ask(TryUnseal {
|
||||
seal_key_raw: seal_key_buffer,
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
info!("Successfully unsealed key with client-provided key");
|
||||
Ok(())
|
||||
}
|
||||
Err(SendError::HandlerError(vault::Error::InvalidKey)) => Err(Error::InvalidKey),
|
||||
Err(SendError::HandlerError(err)) => {
|
||||
error!(?err, "Vault failed to unseal key");
|
||||
Err(Error::InvalidKey)
|
||||
}
|
||||
Err(err) => {
|
||||
error!(?err, "Failed to send unseal request to vault");
|
||||
Err(Error::internal("Vault actor error").into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[message]
|
||||
pub(crate) async fn handle_bootstrap_encrypted_key(
|
||||
&mut self,
|
||||
nonce: Vec<u8>,
|
||||
ciphertext: Vec<u8>,
|
||||
associated_data: Vec<u8>,
|
||||
) -> Result<(), Error> {
|
||||
let State::ReadyForExchange { secret, .. } = &self.state else {
|
||||
return Err(Error::State);
|
||||
};
|
||||
|
||||
let seal_key_buffer = match Self::decrypt_key(secret, &nonce, &ciphertext, &associated_data)
|
||||
{
|
||||
Ok(buffer) => buffer,
|
||||
Err(()) => {
|
||||
return Err(Error::InvalidKey);
|
||||
}
|
||||
};
|
||||
|
||||
match self
|
||||
.actors
|
||||
.vault
|
||||
.ask(Bootstrap {
|
||||
seal_key_raw: seal_key_buffer,
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
info!("Successfully bootstrapped vault with client-provided key");
|
||||
Ok(())
|
||||
}
|
||||
Err(SendError::HandlerError(vault::Error::AlreadyBootstrapped)) => {
|
||||
Err(Error::AlreadyBootstrapped)
|
||||
}
|
||||
Err(SendError::HandlerError(err)) => {
|
||||
error!(?err, "Vault failed to bootstrap vault");
|
||||
Err(Error::InvalidKey)
|
||||
}
|
||||
Err(err) => {
|
||||
error!(?err, "Failed to send bootstrap request to vault");
|
||||
Err(Error::internal("Vault error"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Message<events::Bootstrapped> for VaultGate {
|
||||
type Reply = ();
|
||||
|
||||
async fn handle(
|
||||
&mut self,
|
||||
_: events::Bootstrapped,
|
||||
ctx: &mut kameo::prelude::Context<Self, Self::Reply>,
|
||||
) -> Self::Reply {
|
||||
let result = async {
|
||||
let mut conn = self.db.get().await.map_err(|_| Error::internal("DB unavailable"))?;
|
||||
integrity::sign_entity(&mut conn, &self.actors.vault, &self.auth_creds, self.auth_creds.creds.id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!(?e, "Failed to sign integrity envelope on bootstrap");
|
||||
Error::internal("Integrity sign failed")
|
||||
})?;
|
||||
Ok(self.auth_creds.creds.clone())
|
||||
}
|
||||
.await;
|
||||
|
||||
if let Some(tx) = self.promotion_tx.take() {
|
||||
let _ = tx.send(result);
|
||||
}
|
||||
ctx.stop();
|
||||
}
|
||||
}
|
||||
|
||||
impl Message<events::Unsealed> for VaultGate {
|
||||
type Reply = ();
|
||||
|
||||
async fn handle(
|
||||
&mut self,
|
||||
_: events::Unsealed,
|
||||
ctx: &mut kameo::prelude::Context<Self, Self::Reply>,
|
||||
) -> Self::Reply {
|
||||
let result = async {
|
||||
let mut conn = self.db.get().await.map_err(|_| Error::internal("DB unavailable"))?;
|
||||
match integrity::verify_entity(
|
||||
&mut conn,
|
||||
&self.actors.vault,
|
||||
&self.auth_creds,
|
||||
self.auth_creds.creds.id,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(AttestationStatus::Attested) => Ok(self.auth_creds.creds.clone()),
|
||||
Ok(AttestationStatus::Unavailable) => {
|
||||
Err(Error::internal("Vault sealed during promotion"))
|
||||
}
|
||||
Err(e) => {
|
||||
error!(?e, "Integrity verification failed during unseal promotion");
|
||||
Err(Error::InvalidKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
.await;
|
||||
|
||||
if let Some(tx) = self.promotion_tx.take() {
|
||||
let _ = tx.send(result);
|
||||
}
|
||||
ctx.stop();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
use std::sync::Mutex;
|
||||
|
||||
use x25519_dalek::{EphemeralSecret, PublicKey, SharedSecret};
|
||||
|
||||
|
||||
|
||||
pub struct Handshake {
|
||||
client_pubkey: PublicKey,
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[derive(Default)]
|
||||
pub enum State {
|
||||
#[default]
|
||||
Idle,
|
||||
ReadyForExchange { server_key: PublicKey, secret: SharedSecret },
|
||||
}
|
||||
Reference in New Issue
Block a user