feat(server): two-operator vault requires at least one recovery share
Some checks failed
ci/woodpecker/pr/server-audit Pipeline was successful
ci/woodpecker/pr/server-lint Pipeline failed
ci/woodpecker/pr/server-vet Pipeline failed
ci/woodpecker/pr/server-test Pipeline was successful

This commit is contained in:
CleverWild
2026-06-13 22:13:07 +02:00
parent 3b090cd3ce
commit f8c621b20e
5 changed files with 44 additions and 2 deletions

View File

@@ -39,6 +39,8 @@ pub enum Error {
Encryption,
#[error("Vault error")]
VaultError,
#[error("Two-operator vaults require at least one recovery share")]
TwoOperatorsRequireRecovery,
#[error("Broken database")]
BrokenDatabase,
}
@@ -200,11 +202,15 @@ impl VaultCoordinator {
&mut self,
operator_id: i32,
declared_count: usize,
recovery_count: usize,
) -> Result<(), Error> {
let _ = operator_id; // fixme!: any authenticated operator may announce the committee size. the first call wins
if !matches!(self.state, CoordinatorState::Idle) {
return Err(Error::AlreadyBootstrapping);
}
if declared_count == 2 && recovery_count == 0 {
return Err(Error::TwoOperatorsRequireRecovery);
}
self.state = CoordinatorState::Bootstrapping {
declared_count,
passphrases: HashMap::new(),
@@ -223,6 +229,7 @@ impl VaultCoordinator {
let CoordinatorState::Bootstrapping {
declared_count,
passphrases,
..
} = &mut self.state
else {
return Err(Error::NotBootstrapping);

View File

@@ -132,6 +132,7 @@ impl TryConvert for BootstrapRequestPayload {
Self::DeclareCommittee(dc) => Ok(
vault_gate::Inbound::HandleDeclareCommittee(HandleDeclareCommittee {
count: dc.count as usize,
recovery_count: dc.recovery_count as usize,
}),
),
Self::ContributePassphrase(cp) => Ok(

View File

@@ -234,12 +234,17 @@ impl VaultGate {
}
#[message]
pub async fn handle_declare_committee(&mut self, count: usize) -> Result<(), Error> {
pub async fn handle_declare_committee(
&mut self,
count: usize,
recovery_count: usize,
) -> Result<(), Error> {
self.actors
.vault_coordinator
.ask(StartBootstrap {
operator_id: self.auth_creds.id,
declared_count: count,
recovery_count,
})
.await
.map_err(|_| Error::internal("VaultCoordinator unavailable"))

View File

@@ -4,6 +4,7 @@ use arbiter_server::{
actors::{
GlobalActors,
vault::{Error, Vault},
vault_coordinator::{Error as CoordinatorError, StartBootstrap, VaultCoordinator},
},
crypto::{KeyCell, encryption::v1::{Nonce, ROOT_KEY_TAG}},
db::{self, models, schema},
@@ -11,6 +12,7 @@ use arbiter_server::{
use diesel::{QueryDsl, SelectableHelper};
use diesel_async::RunQueryDsl;
use kameo::actor::Spawn as _;
#[tokio::test]
#[test_log::test]
@@ -139,3 +141,29 @@ async fn test_unseal_wrong_then_correct_password() {
let mut decrypted = actor.decrypt(aead_id).await.unwrap();
assert_eq!(*decrypted.read(), plaintext);
}
#[tokio::test]
#[test_log::test]
async fn two_operator_vault_requires_recovery_share() {
let db = db::create_test_pool().await;
let bus = GlobalActors::spawn_message_bus();
let vault_ref = Vault::spawn(Vault::new(db.clone(), bus).await.unwrap());
let coordinator = VaultCoordinator::spawn(VaultCoordinator::new(db, vault_ref));
let err = coordinator
.ask(StartBootstrap {
operator_id: 1,
declared_count: 2,
recovery_count: 0,
})
.await
.unwrap_err();
assert!(
matches!(
err,
kameo::error::SendError::HandlerError(CoordinatorError::TwoOperatorsRequireRecovery)
),
"expected TwoOperatorsRequireRecovery, got {err:?}"
);
}