feat(server): two-operator vault requires at least one recovery share
This commit is contained in:
@@ -9,7 +9,8 @@ message BootstrapEncryptedKey {
|
|||||||
}
|
}
|
||||||
|
|
||||||
message DeclareCommittee {
|
message DeclareCommittee {
|
||||||
uint32 count = 1;
|
uint32 count = 1;
|
||||||
|
uint32 recovery_count = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ContributePassphrase {
|
message ContributePassphrase {
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ pub enum Error {
|
|||||||
Encryption,
|
Encryption,
|
||||||
#[error("Vault error")]
|
#[error("Vault error")]
|
||||||
VaultError,
|
VaultError,
|
||||||
|
#[error("Two-operator vaults require at least one recovery share")]
|
||||||
|
TwoOperatorsRequireRecovery,
|
||||||
#[error("Broken database")]
|
#[error("Broken database")]
|
||||||
BrokenDatabase,
|
BrokenDatabase,
|
||||||
}
|
}
|
||||||
@@ -200,11 +202,15 @@ impl VaultCoordinator {
|
|||||||
&mut self,
|
&mut self,
|
||||||
operator_id: i32,
|
operator_id: i32,
|
||||||
declared_count: usize,
|
declared_count: usize,
|
||||||
|
recovery_count: usize,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let _ = operator_id; // fixme!: any authenticated operator may announce the committee size. the first call wins
|
let _ = operator_id; // fixme!: any authenticated operator may announce the committee size. the first call wins
|
||||||
if !matches!(self.state, CoordinatorState::Idle) {
|
if !matches!(self.state, CoordinatorState::Idle) {
|
||||||
return Err(Error::AlreadyBootstrapping);
|
return Err(Error::AlreadyBootstrapping);
|
||||||
}
|
}
|
||||||
|
if declared_count == 2 && recovery_count == 0 {
|
||||||
|
return Err(Error::TwoOperatorsRequireRecovery);
|
||||||
|
}
|
||||||
self.state = CoordinatorState::Bootstrapping {
|
self.state = CoordinatorState::Bootstrapping {
|
||||||
declared_count,
|
declared_count,
|
||||||
passphrases: HashMap::new(),
|
passphrases: HashMap::new(),
|
||||||
@@ -223,6 +229,7 @@ impl VaultCoordinator {
|
|||||||
let CoordinatorState::Bootstrapping {
|
let CoordinatorState::Bootstrapping {
|
||||||
declared_count,
|
declared_count,
|
||||||
passphrases,
|
passphrases,
|
||||||
|
..
|
||||||
} = &mut self.state
|
} = &mut self.state
|
||||||
else {
|
else {
|
||||||
return Err(Error::NotBootstrapping);
|
return Err(Error::NotBootstrapping);
|
||||||
|
|||||||
@@ -132,6 +132,7 @@ impl TryConvert for BootstrapRequestPayload {
|
|||||||
Self::DeclareCommittee(dc) => Ok(
|
Self::DeclareCommittee(dc) => Ok(
|
||||||
vault_gate::Inbound::HandleDeclareCommittee(HandleDeclareCommittee {
|
vault_gate::Inbound::HandleDeclareCommittee(HandleDeclareCommittee {
|
||||||
count: dc.count as usize,
|
count: dc.count as usize,
|
||||||
|
recovery_count: dc.recovery_count as usize,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
Self::ContributePassphrase(cp) => Ok(
|
Self::ContributePassphrase(cp) => Ok(
|
||||||
|
|||||||
@@ -234,12 +234,17 @@ impl VaultGate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[message]
|
#[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
|
self.actors
|
||||||
.vault_coordinator
|
.vault_coordinator
|
||||||
.ask(StartBootstrap {
|
.ask(StartBootstrap {
|
||||||
operator_id: self.auth_creds.id,
|
operator_id: self.auth_creds.id,
|
||||||
declared_count: count,
|
declared_count: count,
|
||||||
|
recovery_count,
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|_| Error::internal("VaultCoordinator unavailable"))
|
.map_err(|_| Error::internal("VaultCoordinator unavailable"))
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use arbiter_server::{
|
|||||||
actors::{
|
actors::{
|
||||||
GlobalActors,
|
GlobalActors,
|
||||||
vault::{Error, Vault},
|
vault::{Error, Vault},
|
||||||
|
vault_coordinator::{Error as CoordinatorError, StartBootstrap, VaultCoordinator},
|
||||||
},
|
},
|
||||||
crypto::{KeyCell, encryption::v1::{Nonce, ROOT_KEY_TAG}},
|
crypto::{KeyCell, encryption::v1::{Nonce, ROOT_KEY_TAG}},
|
||||||
db::{self, models, schema},
|
db::{self, models, schema},
|
||||||
@@ -11,6 +12,7 @@ use arbiter_server::{
|
|||||||
|
|
||||||
use diesel::{QueryDsl, SelectableHelper};
|
use diesel::{QueryDsl, SelectableHelper};
|
||||||
use diesel_async::RunQueryDsl;
|
use diesel_async::RunQueryDsl;
|
||||||
|
use kameo::actor::Spawn as _;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[test_log::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();
|
let mut decrypted = actor.decrypt(aead_id).await.unwrap();
|
||||||
assert_eq!(*decrypted.read(), plaintext);
|
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:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user