feat(unseal): add unseal protocol support for user agents

This commit is contained in:
hdbg
2026-02-14 23:44:37 +01:00
parent 45acb45a05
commit a55221573b
9 changed files with 90 additions and 84 deletions

View File

@@ -3,29 +3,8 @@ syntax = "proto3";
package arbiter; package arbiter;
import "auth.proto"; import "auth.proto";
import "client.proto";
message ClientRequest { import "user_agent.proto";
oneof payload {
arbiter.auth.ClientMessage auth_message = 1;
}
}
message ClientResponse {
oneof payload {
arbiter.auth.ServerMessage auth_message = 1;
}
}
message UserAgentRequest {
oneof payload {
arbiter.auth.ClientMessage auth_message = 1;
}
}
message UserAgentResponse {
oneof payload {
arbiter.auth.ServerMessage auth_message = 1;
}
}
message ServerInfo { message ServerInfo {
string version = 1; string version = 1;

17
protobufs/client.proto Normal file
View File

@@ -0,0 +1,17 @@
syntax = "proto3";
package arbiter;
import "auth.proto";
message ClientRequest {
oneof payload {
arbiter.auth.ClientMessage auth_message = 1;
}
}
message ClientResponse {
oneof payload {
arbiter.auth.ServerMessage auth_message = 1;
}
}

View File

@@ -2,13 +2,20 @@ syntax = "proto3";
package arbiter.unseal; package arbiter.unseal;
message UserAgentKeyRequest {} import "google/protobuf/empty.proto";
message ServerKeyResponse { message UnsealStart {}
message UnsealStartResponse {
bytes pubkey = 1; bytes pubkey = 1;
} }
message UserAgentSealedKey { message UnsealEncryptedKey {
bytes sealed_key = 1; bytes key = 1;
bytes pubkey = 2; }
bytes nonce = 3;
enum UnsealResult {
UNSEAL_RESULT_UNSPECIFIED = 0;
UNSEAL_RESULT_SUCCESS = 1;
UNSEAL_RESULT_INVALID_KEY = 2;
UNSEAL_RESULT_UNBOOTSTRAPPED = 3;
} }

View File

@@ -0,0 +1,21 @@
syntax = "proto3";
package arbiter;
import "auth.proto";
import "unseal.proto";
message UserAgentRequest {
oneof payload {
arbiter.auth.ClientMessage auth_message = 1;
arbiter.unseal.UnsealStart unseal_start = 2;
arbiter.unseal.UnsealEncryptedKey unseal_encrypted_key = 3;
}
}
message UserAgentResponse {
oneof payload {
arbiter.auth.ServerMessage auth_message = 1;
arbiter.unseal.UnsealStartResponse unseal_start_response = 2;
arbiter.unseal.UnsealResult unseal_result = 3;
}
}

View File

@@ -6,6 +6,9 @@ pub mod proto {
pub mod auth { pub mod auth {
tonic::include_proto!("arbiter.auth"); tonic::include_proto!("arbiter.auth");
} }
pub mod unseal {
tonic::include_proto!("arbiter.unseal");
}
} }
pub mod transport; pub mod transport;

View File

@@ -1,2 +1,3 @@
pub mod user_agent; pub mod user_agent;
pub mod client; pub mod client;
pub(crate) mod bootstrap;

View File

@@ -1,5 +1,5 @@
use arbiter_proto::proto::{ use arbiter_proto::proto::{
UserAgentRequest, UserAgentResponse, UserAgentResponse,
auth::{ auth::{
self, AuthChallenge, AuthChallengeRequest, AuthOk, ClientMessage, self, AuthChallenge, AuthChallengeRequest, AuthOk, ClientMessage,
ServerMessage as AuthServerMessage, client_message::Payload as ClientAuthPayload, ServerMessage as AuthServerMessage, client_message::Payload as ClientAuthPayload,
@@ -11,22 +11,19 @@ use arbiter_proto::proto::{
use diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl, dsl::update}; use diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl, dsl::update};
use diesel_async::{AsyncConnection, RunQueryDsl}; use diesel_async::{AsyncConnection, RunQueryDsl};
use ed25519_dalek::VerifyingKey; use ed25519_dalek::VerifyingKey;
use futures::StreamExt;
use kameo::{ use kameo::{
Actor, Actor,
actor::{ActorRef, Spawn}, actor::{ActorRef, Spawn},
error::SendError,
messages, messages,
prelude::Context, prelude::Context,
}; };
use tokio::sync::mpsc;
use tokio::sync::mpsc::Sender; use tokio::sync::mpsc::Sender;
use tonic::Status; use tonic::Status;
use tracing::{error, info}; use tracing::{error, info};
use crate::{ use crate::{
ServerContext, ServerContext,
context::bootstrap::{BootstrapActor, ConsumeToken}, actors::bootstrap::{BootstrapActor, ConsumeToken},
db::{self, schema}, db::{self, schema},
errors::GrpcStatusExt, errors::GrpcStatusExt,
}; };
@@ -54,12 +51,15 @@ smlang::statemachine!(
custom_error: false, custom_error: false,
transitions: { transitions: {
*Init + AuthRequest(AuthRequestContext) / auth_request_context = ReceivedAuthRequest(AuthRequestContext), *Init + AuthRequest(AuthRequestContext) / auth_request_context = ReceivedAuthRequest(AuthRequestContext),
ReceivedAuthRequest(AuthRequestContext) + ReceivedBootstrapToken = Authenticated, ReceivedAuthRequest(AuthRequestContext) + ReceivedBootstrapToken = Idle,
ReceivedAuthRequest(AuthRequestContext) + SentChallenge(ChallengeContext) / move_challenge = WaitingForChallengeSolution(ChallengeContext), ReceivedAuthRequest(AuthRequestContext) + SentChallenge(ChallengeContext) / move_challenge = WaitingForChallengeSolution(ChallengeContext),
WaitingForChallengeSolution(ChallengeContext) + ReceivedGoodSolution = Authenticated, WaitingForChallengeSolution(ChallengeContext) + ReceivedGoodSolution = Idle,
WaitingForChallengeSolution(ChallengeContext) + ReceivedBadSolution = AuthError, // block further transitions, but connection should close anyway WaitingForChallengeSolution(ChallengeContext) + ReceivedBadSolution = AuthError, // block further transitions, but connection should close anyway
Idle + UnsealRequest / generate_temp_keypair = UnsealStarted(ed25519_dalek::SigningKey),
UnsealStarted(ed25519_dalek::SigningKey) + SentTempKeypair / move_keypair = WaitingForUnsealKey(ed25519_dalek::SigningKey),
} }
); );
@@ -69,7 +69,7 @@ impl UserAgentStateMachineContext for DummyContext {
#[allow(clippy::unused_unit)] #[allow(clippy::unused_unit)]
fn move_challenge( fn move_challenge(
&mut self, &mut self,
state_data: &AuthRequestContext, _state_data: &AuthRequestContext,
event_data: ChallengeContext, event_data: ChallengeContext,
) -> Result<ChallengeContext, ()> { ) -> Result<ChallengeContext, ()> {
Ok(event_data) Ok(event_data)
@@ -83,6 +83,21 @@ impl UserAgentStateMachineContext for DummyContext {
) -> Result<AuthRequestContext, ()> { ) -> Result<AuthRequestContext, ()> {
Ok(event_data) Ok(event_data)
} }
#[allow(missing_docs)]
#[allow(clippy::unused_unit)]
fn move_keypair(
&mut self,
state_data: &ed25519_dalek::SigningKey,
) -> Result<ed25519_dalek::SigningKey, ()> {
Ok(state_data.clone())
}
#[allow(missing_docs)]
#[allow(clippy::unused_unit)]
fn generate_temp_keypair(&mut self) -> Result<ed25519_dalek::SigningKey, ()> {
Ok(ed25519_dalek::SigningKey::generate(&mut rand::rng()))
}
} }
#[derive(Actor)] #[derive(Actor)]
@@ -90,7 +105,8 @@ pub struct UserAgentActor {
db: db::DatabasePool, db: db::DatabasePool,
bootstapper: ActorRef<BootstrapActor>, bootstapper: ActorRef<BootstrapActor>,
state: UserAgentStateMachine<DummyContext>, state: UserAgentStateMachine<DummyContext>,
tx: Sender<Result<UserAgentResponse, Status>>, // will be used in future
_tx: Sender<Result<UserAgentResponse, Status>>,
} }
impl UserAgentActor { impl UserAgentActor {
@@ -102,10 +118,11 @@ impl UserAgentActor {
db: context.db.clone(), db: context.db.clone(),
bootstapper: context.bootstrapper.clone(), bootstapper: context.bootstrapper.clone(),
state: UserAgentStateMachine::new(DummyContext), state: UserAgentStateMachine::new(DummyContext),
tx, _tx: tx,
} }
} }
#[cfg(test)]
pub(crate) fn new_manual( pub(crate) fn new_manual(
db: db::DatabasePool, db: db::DatabasePool,
bootstapper: ActorRef<BootstrapActor>, bootstapper: ActorRef<BootstrapActor>,
@@ -115,7 +132,7 @@ impl UserAgentActor {
db, db,
bootstapper, bootstapper,
state: UserAgentStateMachine::new(DummyContext), state: UserAgentStateMachine::new(DummyContext),
tx, _tx: tx,
} }
} }
@@ -319,8 +336,10 @@ mod tests {
use kameo::actor::Spawn; use kameo::actor::Spawn;
use crate::{ use crate::{
actors::user_agent::{HandleAuthChallengeRequest, HandleAuthChallengeSolution}, actors::{
context::bootstrap::BootstrapActor, bootstrap::BootstrapActor,
user_agent::{HandleAuthChallengeRequest, HandleAuthChallengeSolution},
},
db::{self, schema}, db::{self, schema},
}; };
@@ -449,7 +468,7 @@ mod tests {
{ {
let mut conn = db.get().await.unwrap(); let mut conn = db.get().await.unwrap();
insert_into(schema::useragent_client::table) insert_into(schema::useragent_client::table)
.values((schema::useragent_client::public_key.eq(pubkey_bytes.clone()))) .values(schema::useragent_client::public_key.eq(pubkey_bytes.clone()))
.execute(&mut conn) .execute(&mut conn)
.await .await
.unwrap(); .unwrap();

View File

@@ -1,41 +0,0 @@
use std::sync::Arc;
use dashmap::DashSet;
#[derive(Clone, Default)]
struct LeaseStorage<T: Eq + std::hash::Hash>(Arc<DashSet<T>>);
// A lease that automatically releases the item when dropped
pub struct Lease<T: Clone + std::hash::Hash + Eq> {
item: T,
storage: LeaseStorage<T>,
}
impl<T: Clone + std::hash::Hash + Eq> Drop for Lease<T> {
fn drop(&mut self) {
self.storage.0.remove(&self.item);
}
}
#[derive(Clone, Default)]
pub struct LeaseHandler<T: Clone + std::hash::Hash + Eq> {
storage: LeaseStorage<T>,
}
impl<T: Clone + std::hash::Hash + Eq> LeaseHandler<T> {
pub fn new() -> Self {
Self {
storage: LeaseStorage(Arc::new(DashSet::new())),
}
}
pub fn acquire(&self, item: T) -> Result<Lease<T>, ()> {
if self.storage.0.insert(item.clone()) {
Ok(Lease {
item,
storage: self.storage.clone(),
})
} else {
Err(())
}
}
}