feat(server::user-agent): Unseal implemented

This commit is contained in:
hdbg
2026-02-16 21:07:09 +01:00
parent dd716da4cd
commit eb9cbc88e9
5 changed files with 96 additions and 51 deletions

View File

@@ -1,25 +0,0 @@
syntax = "proto3";
package arbiter.unseal;
import "google/protobuf/empty.proto";
message UnsealStart {
bytes client_pubkey = 1;
}
message UnsealStartResponse {
bytes server_pubkey = 1;
}
message UnsealEncryptedKey {
bytes nonce = 1;
bytes ciphertext = 2;
bytes associated_data = 3;
}
enum UnsealResult {
UNSEAL_RESULT_UNSPECIFIED = 0;
UNSEAL_RESULT_SUCCESS = 1;
UNSEAL_RESULT_INVALID_KEY = 2;
UNSEAL_RESULT_UNBOOTSTRAPPED = 3;
}

View File

@@ -3,19 +3,49 @@ syntax = "proto3";
package arbiter; package arbiter;
import "auth.proto"; import "auth.proto";
import "unseal.proto"; import "google/protobuf/empty.proto";
message UnsealStart {
bytes client_pubkey = 1;
}
message UnsealStartResponse {
bytes server_pubkey = 1;
}
message UnsealEncryptedKey {
bytes nonce = 1;
bytes ciphertext = 2;
bytes associated_data = 3;
}
enum UnsealResult {
UNSEAL_RESULT_UNSPECIFIED = 0;
UNSEAL_RESULT_SUCCESS = 1;
UNSEAL_RESULT_INVALID_KEY = 2;
UNSEAL_RESULT_UNBOOTSTRAPPED = 3;
}
enum VaultState {
VAULT_STATE_UNSPECIFIED = 0;
VAULT_STATE_UNBOOTSTRAPPED = 1;
VAULT_STATE_SEALED = 2;
VAULT_STATE_UNSEALED = 3;
VAULT_STATE_ERROR = 4;
}
message UserAgentRequest { message UserAgentRequest {
oneof payload { oneof payload {
arbiter.auth.ClientMessage auth_message = 1; arbiter.auth.ClientMessage auth_message = 1;
arbiter.unseal.UnsealStart unseal_start = 2; UnsealStart unseal_start = 2;
arbiter.unseal.UnsealEncryptedKey unseal_encrypted_key = 3; UnsealEncryptedKey unseal_encrypted_key = 3;
google.protobuf.Empty query_vault_state = 4;
} }
} }
message UserAgentResponse { message UserAgentResponse {
oneof payload { oneof payload {
arbiter.auth.ServerMessage auth_message = 1; arbiter.auth.ServerMessage auth_message = 1;
arbiter.unseal.UnsealStartResponse unseal_start_response = 2; UnsealStartResponse unseal_start_response = 2;
arbiter.unseal.UnsealResult unseal_result = 3; UnsealResult unseal_result = 3;
VaultState vault_state = 4;
} }
} }

View File

@@ -6,9 +6,6 @@ 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,25 +1,18 @@
use std::{ use std::{ops::DerefMut, sync::Mutex};
ops::DerefMut,
sync::Mutex,
};
use arbiter_proto::proto::{ use arbiter_proto::proto::{
UserAgentResponse, UnsealEncryptedKey, UnsealResult, UnsealStart, UnsealStartResponse, UserAgentResponse,
auth::{ auth::{
self, AuthChallengeRequest, AuthOk, ServerMessage as AuthServerMessage, self, AuthChallengeRequest, AuthOk, ServerMessage as AuthServerMessage,
server_message::Payload as ServerAuthPayload, server_message::Payload as ServerAuthPayload,
}, },
unseal::{UnsealEncryptedKey, UnsealResult, UnsealStart, UnsealStartResponse},
user_agent_response::Payload as UserAgentResponsePayload, user_agent_response::Payload as UserAgentResponsePayload,
}; };
use chacha20poly1305::{ use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit};
AeadInPlace, XChaCha20Poly1305, XNonce,
aead::KeyInit,
};
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 kameo::{Actor, actor::ActorRef, messages}; use kameo::{Actor, actor::ActorRef, error::SendError, messages};
use memsafe::MemSafe; use memsafe::MemSafe;
use tokio::sync::mpsc::Sender; use tokio::sync::mpsc::Sender;
use tonic::Status; use tonic::Status;
@@ -30,6 +23,7 @@ use crate::{
ServerContext, ServerContext,
actors::{ actors::{
bootstrap::{Bootstrapper, ConsumeToken}, bootstrap::{Bootstrapper, ConsumeToken},
keyholder::{self, KeyHolder, TryUnseal},
user_agent::state::{ user_agent::state::{
AuthRequestContext, ChallengeContext, DummyContext, UnsealContext, UserAgentEvents, AuthRequestContext, ChallengeContext, DummyContext, UnsealContext, UserAgentEvents,
UserAgentStateMachine, UserAgentStates, UserAgentStateMachine, UserAgentStates,
@@ -50,6 +44,7 @@ pub(crate) use transport::handle_user_agent;
pub struct UserAgentActor { pub struct UserAgentActor {
db: db::DatabasePool, db: db::DatabasePool,
bootstapper: ActorRef<Bootstrapper>, bootstapper: ActorRef<Bootstrapper>,
keyholder: ActorRef<KeyHolder>,
state: UserAgentStateMachine<DummyContext>, state: UserAgentStateMachine<DummyContext>,
// will be used in future // will be used in future
_tx: Sender<Result<UserAgentResponse, Status>>, _tx: Sender<Result<UserAgentResponse, Status>>,
@@ -63,6 +58,7 @@ impl UserAgentActor {
Self { Self {
db: context.db.clone(), db: context.db.clone(),
bootstapper: context.bootstrapper.clone(), bootstapper: context.bootstrapper.clone(),
keyholder: context.keyholder.clone(),
state: UserAgentStateMachine::new(DummyContext), state: UserAgentStateMachine::new(DummyContext),
_tx: tx, _tx: tx,
} }
@@ -72,11 +68,13 @@ impl UserAgentActor {
pub(crate) fn new_manual( pub(crate) fn new_manual(
db: db::DatabasePool, db: db::DatabasePool,
bootstapper: ActorRef<Bootstrapper>, bootstapper: ActorRef<Bootstrapper>,
keyholder: ActorRef<KeyHolder>,
tx: Sender<Result<UserAgentResponse, Status>>, tx: Sender<Result<UserAgentResponse, Status>>,
) -> Self { ) -> Self {
Self { Self {
db, db,
bootstapper, bootstapper,
keyholder,
state: UserAgentStateMachine::new(DummyContext), state: UserAgentStateMachine::new(DummyContext),
_tx: tx, _tx: tx,
} }
@@ -280,22 +278,57 @@ impl UserAgentActor {
let shared_secret = ephemeral_secret.diffie_hellman(&unseal_context.client_public_key); let shared_secret = ephemeral_secret.diffie_hellman(&unseal_context.client_public_key);
let cipher = XChaCha20Poly1305::new(shared_secret.as_bytes().into()); let cipher = XChaCha20Poly1305::new(shared_secret.as_bytes().into());
let mut root_key_buffer = MemSafe::new(req.ciphertext.clone()).unwrap(); let mut seal_key_buffer = MemSafe::new(req.ciphertext.clone()).unwrap();
let mut write_handle = root_key_buffer.write().unwrap();
let write_handle = write_handle.deref_mut();
let decryption_result = cipher let decryption_result = {
.decrypt_in_place(nonce, &req.associated_data, write_handle); let mut write_handle = seal_key_buffer.write().unwrap();
let write_handle = write_handle.deref_mut();
cipher.decrypt_in_place(nonce, &req.associated_data, write_handle)
};
match decryption_result { match decryption_result {
Ok(_) => todo!("Send key to the keyguarding"), Ok(_) => {
match self
.keyholder
.ask(TryUnseal {
seal_key_raw: seal_key_buffer,
})
.await
{
Ok(_) => {
info!("Successfully unsealed key with client-provided key");
self.transition(UserAgentEvents::ReceivedValidKey)?;
Ok(unseal_response(UserAgentResponsePayload::UnsealResult(
UnsealResult::Success.into(),
)))
}
Err(SendError::HandlerError(keyholder::Error::InvalidKey)) => {
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
Ok(unseal_response(UserAgentResponsePayload::UnsealResult(
UnsealResult::InvalidKey.into(),
)))
}
Err(SendError::HandlerError(err)) => {
error!(?err, "Keyholder failed to unseal key");
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
Ok(unseal_response(UserAgentResponsePayload::UnsealResult(
UnsealResult::InvalidKey.into(),
)))
}
Err(err) => {
error!(?err, "Failed to send unseal request to keyholder");
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
Err(Status::internal("Vault is not available"))
}
}
}
Err(err) => { Err(err) => {
error!(?err, "Failed to decrypt unseal key"); error!(?err, "Failed to decrypt unseal key");
self.transition(UserAgentEvents::ReceivedInvalidKey)?; self.transition(UserAgentEvents::ReceivedInvalidKey)?;
return Ok(unseal_response(UserAgentResponsePayload::UnsealResult( return Ok(unseal_response(UserAgentResponsePayload::UnsealResult(
UnsealResult::InvalidKey.into(), UnsealResult::InvalidKey.into(),
))); )));
}, }
} }
} }

View File

@@ -11,6 +11,7 @@ use kameo::actor::Spawn;
use crate::{ use crate::{
actors::{ actors::{
bootstrap::Bootstrapper, bootstrap::Bootstrapper,
keyholder::{self, KeyHolder},
user_agent::{HandleAuthChallengeRequest, HandleAuthChallengeSolution}, user_agent::{HandleAuthChallengeRequest, HandleAuthChallengeSolution},
}, },
db::{self, schema}, db::{self, schema},
@@ -24,12 +25,15 @@ pub async fn test_bootstrap_token_auth() {
let db = db::create_test_pool().await; let db = db::create_test_pool().await;
// explicitly not installing any user_agent pubkeys // explicitly not installing any user_agent pubkeys
let bootstrapper = Bootstrapper::new(&db).await.unwrap(); // this will create bootstrap token let bootstrapper = Bootstrapper::new(&db).await.unwrap(); // this will create bootstrap token
let keyholder = KeyHolder::new(db.clone()).await.unwrap();
let token = bootstrapper.get_token().unwrap(); let token = bootstrapper.get_token().unwrap();
let bootstrapper_ref = Bootstrapper::spawn(bootstrapper); let bootstrapper_ref = Bootstrapper::spawn(bootstrapper);
let keyholder_ref = KeyHolder::spawn(keyholder);
let user_agent = UserAgentActor::new_manual( let user_agent = UserAgentActor::new_manual(
db.clone(), db.clone(),
bootstrapper_ref, bootstrapper_ref,
keyholder_ref,
tokio::sync::mpsc::channel(1).0, // dummy channel, we won't actually send responses in this test tokio::sync::mpsc::channel(1).0, // dummy channel, we won't actually send responses in this test
); );
let user_agent_ref = UserAgentActor::spawn(user_agent); let user_agent_ref = UserAgentActor::spawn(user_agent);
@@ -78,11 +82,15 @@ pub async fn test_bootstrap_invalid_token_auth() {
let db = db::create_test_pool().await; let db = db::create_test_pool().await;
// explicitly not installing any user_agent pubkeys // explicitly not installing any user_agent pubkeys
let bootstrapper = Bootstrapper::new(&db).await.unwrap(); // this will create bootstrap token let bootstrapper = Bootstrapper::new(&db).await.unwrap(); // this will create bootstrap token
let keyholder = KeyHolder::new(db.clone()).await.unwrap();
let bootstrapper_ref = Bootstrapper::spawn(bootstrapper); let bootstrapper_ref = Bootstrapper::spawn(bootstrapper);
let keyholder_ref = KeyHolder::spawn(keyholder);
let user_agent = UserAgentActor::new_manual( let user_agent = UserAgentActor::new_manual(
db.clone(), db.clone(),
bootstrapper_ref, bootstrapper_ref,
keyholder_ref,
tokio::sync::mpsc::channel(1).0, // dummy channel, we won't actually send responses in this test tokio::sync::mpsc::channel(1).0, // dummy channel, we won't actually send responses in this test
); );
let user_agent_ref = UserAgentActor::spawn(user_agent); let user_agent_ref = UserAgentActor::spawn(user_agent);
@@ -126,9 +134,11 @@ pub async fn test_challenge_auth() {
let db = db::create_test_pool().await; let db = db::create_test_pool().await;
let bootstrapper_ref = Bootstrapper::spawn(Bootstrapper::new(&db).await.unwrap()); let bootstrapper_ref = Bootstrapper::spawn(Bootstrapper::new(&db).await.unwrap());
let keyholder_ref = KeyHolder::spawn(KeyHolder::new(db.clone()).await.unwrap());
let user_agent = UserAgentActor::new_manual( let user_agent = UserAgentActor::new_manual(
db.clone(), db.clone(),
bootstrapper_ref, bootstrapper_ref,
keyholder_ref,
tokio::sync::mpsc::channel(1).0, // dummy channel, we won't actually send responses in this test tokio::sync::mpsc::channel(1).0, // dummy channel, we won't actually send responses in this test
); );
let user_agent_ref = UserAgentActor::spawn(user_agent); let user_agent_ref = UserAgentActor::spawn(user_agent);