feat(vault)!: add multi-operator Shamir custody
This commit is contained in:
@@ -1,11 +1,10 @@
|
||||
use crate::{
|
||||
grpc::request_tracker::RequestTracker,
|
||||
peers::operator::{OutOfBand, OperatorConnection, OperatorSession},
|
||||
peers::operator::{OperatorConnection, OperatorSession, OutOfBand},
|
||||
};
|
||||
use arbiter_proto::{
|
||||
proto::operator::{
|
||||
OperatorRequest, OperatorResponse,
|
||||
operator_request::Payload as OperatorRequestPayload,
|
||||
OperatorRequest, OperatorResponse, operator_request::Payload as OperatorRequestPayload,
|
||||
operator_response::Payload as OperatorResponsePayload,
|
||||
},
|
||||
transport::{Error as TransportError, Receiver, Sender, grpc::GrpcBi},
|
||||
@@ -111,6 +110,9 @@ async fn dispatch_inner(
|
||||
OperatorRequestPayload::Vault(req) => vault::dispatch(actor, req).await,
|
||||
OperatorRequestPayload::Evm(req) => evm::dispatch(actor, req).await,
|
||||
OperatorRequestPayload::SdkClient(req) => sdk_client::dispatch(actor, req).await,
|
||||
OperatorRequestPayload::Governance(_) => {
|
||||
Err(Status::permission_denied(stringify!(Governance)))
|
||||
}
|
||||
OperatorRequestPayload::Auth(..) => {
|
||||
warn!("Unsupported post-auth operator auth request");
|
||||
Err(Status::invalid_argument("Unsupported operator request"))
|
||||
|
||||
@@ -3,7 +3,6 @@ use crate::{
|
||||
peers::operator::{OperatorSession, session::handlers::HandleQueryVaultState},
|
||||
};
|
||||
use arbiter_proto::{
|
||||
proto::shared::VaultState as ProtoVaultState,
|
||||
proto::operator::{
|
||||
operator_response::Payload as OperatorResponsePayload,
|
||||
vault::{
|
||||
@@ -11,6 +10,7 @@ use arbiter_proto::{
|
||||
response::Payload as VaultResponsePayload,
|
||||
},
|
||||
},
|
||||
proto::shared::VaultState as ProtoVaultState,
|
||||
};
|
||||
|
||||
use kameo::actor::ActorRef;
|
||||
@@ -33,11 +33,11 @@ pub(super) async fn dispatch(
|
||||
|
||||
match payload {
|
||||
VaultRequestPayload::QueryState(()) => handle_query_vault_state(actor).await,
|
||||
VaultRequestPayload::Unseal(_) | VaultRequestPayload::Bootstrap(_) => {
|
||||
Err(Status::permission_denied(
|
||||
"Vault is already unsealed; unseal/bootstrap not permitted in session",
|
||||
))
|
||||
}
|
||||
VaultRequestPayload::Unseal(_)
|
||||
| VaultRequestPayload::Bootstrap(_)
|
||||
| VaultRequestPayload::Rekey(_) => Err(Status::permission_denied(
|
||||
"Vault is already unsealed; unseal/bootstrap not permitted in session",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
use crate::{
|
||||
crypto::shamir,
|
||||
grpc::{Convert, TryConvert},
|
||||
peers::operator::vault_gate::{
|
||||
self as vault_gate, HandleBootstrapEncryptedKey, HandleHandshake, HandleUnsealEncryptedKey,
|
||||
self as vault_gate, HandleBootstrapEncryptedKey, HandleContributeBootstrapPassphrase,
|
||||
HandleContributeUnsealPassphrase, HandleDeclareCommittee, HandleHandshake,
|
||||
HandleUnsealEncryptedKey,
|
||||
},
|
||||
};
|
||||
use arbiter_proto::proto::operator::{
|
||||
operator_request::Payload as OperatorRequestPayload,
|
||||
vault::{
|
||||
self as proto_vault,
|
||||
bootstrap::{self as proto_bootstrap},
|
||||
bootstrap::{self as proto_bootstrap, request::Payload as BootstrapRequestPayload},
|
||||
request::Payload as VaultRequestPayload,
|
||||
unseal::{self as proto_unseal, request::Payload as UnsealRequestPayload},
|
||||
},
|
||||
@@ -50,6 +53,7 @@ impl TryConvert for VaultRequestPayload {
|
||||
Self::QueryState(()) => Ok(vault_gate::Inbound::HandleVaultState),
|
||||
Self::Unseal(req) => req.try_convert(),
|
||||
Self::Bootstrap(req) => req.try_convert(),
|
||||
Self::Rekey(_) => Err(Status::unimplemented("Vault re-key is not available")),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,6 +77,16 @@ impl TryConvert for UnsealRequestPayload {
|
||||
match self {
|
||||
Self::Start(start) => start.try_convert(),
|
||||
Self::EncryptedKey(key) => Ok(key.convert()),
|
||||
Self::ContributePassphrase(passphrase) => {
|
||||
Ok(vault_gate::Inbound::HandleContributeUnsealPassphrase(
|
||||
HandleContributeUnsealPassphrase {
|
||||
passphrase: passphrase.passphrase,
|
||||
},
|
||||
))
|
||||
}
|
||||
Self::ContributeRecoveryPassphrase(_) => Err(Status::unimplemented(
|
||||
"Recovery operator contributions are not available",
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -107,12 +121,52 @@ impl TryConvert for proto_bootstrap::Request {
|
||||
type Error = Status;
|
||||
|
||||
fn try_convert(self) -> Result<vault_gate::Inbound, Status> {
|
||||
self.encrypted_key
|
||||
.ok_or_else(|| Status::invalid_argument("Missing bootstrap encrypted key"))?
|
||||
self.payload
|
||||
.ok_or_else(|| Status::invalid_argument("Missing bootstrap payload"))?
|
||||
.try_convert()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryConvert for BootstrapRequestPayload {
|
||||
type Output = vault_gate::Inbound;
|
||||
type Error = Status;
|
||||
|
||||
fn try_convert(self) -> Result<vault_gate::Inbound, Status> {
|
||||
match self {
|
||||
Self::EncryptedKey(key) => key.try_convert(),
|
||||
Self::DeclareCommittee(dc) => {
|
||||
if dc.recovery_count != 0 {
|
||||
return Err(Status::unimplemented(
|
||||
"Recovery operator contributions are not available",
|
||||
));
|
||||
}
|
||||
let count = usize::try_from(dc.count)
|
||||
.ok()
|
||||
.filter(|count| *count <= shamir::MAX_COMMITTEE_SIZE)
|
||||
.ok_or_else(|| {
|
||||
Status::invalid_argument(format!(
|
||||
"Committee count must not exceed {}",
|
||||
shamir::MAX_COMMITTEE_SIZE
|
||||
))
|
||||
})?;
|
||||
Ok(vault_gate::Inbound::HandleDeclareCommittee(
|
||||
HandleDeclareCommittee { count },
|
||||
))
|
||||
}
|
||||
Self::ContributePassphrase(cp) => {
|
||||
Ok(vault_gate::Inbound::HandleContributeBootstrapPassphrase(
|
||||
HandleContributeBootstrapPassphrase {
|
||||
passphrase: cp.passphrase,
|
||||
},
|
||||
))
|
||||
}
|
||||
Self::ContributeRecoveryPassphrase(_) => Err(Status::unimplemented(
|
||||
"Recovery operator contributions are not available",
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryConvert for proto_bootstrap::BootstrapEncryptedKey {
|
||||
type Output = vault_gate::Inbound;
|
||||
type Error = Status;
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use crate::{
|
||||
actors::vault::VaultState,
|
||||
actors::{vault::VaultState, vault_coordinator},
|
||||
grpc::{Convert, TryConvert},
|
||||
peers::operator::vault_gate::{self as vault_gate},
|
||||
};
|
||||
use arbiter_proto::proto::{
|
||||
shared::VaultState as ProtoVaultState,
|
||||
operator::{
|
||||
operator_response::Payload as OperatorResponsePayload,
|
||||
vault::{
|
||||
@@ -17,6 +16,7 @@ use arbiter_proto::proto::{
|
||||
},
|
||||
},
|
||||
},
|
||||
shared::VaultState as ProtoVaultState,
|
||||
};
|
||||
|
||||
use tonic::Status;
|
||||
@@ -34,6 +34,26 @@ const fn wrap_unseal_response(payload: UnsealResponsePayload) -> OperatorRespons
|
||||
}))
|
||||
}
|
||||
|
||||
/// Ceremony errors are the operator's own doing far more often than ours, so
|
||||
/// they travel back as a specific status instead of a blanket internal error.
|
||||
fn ceremony_status(error: &vault_coordinator::Error) -> Status {
|
||||
match error {
|
||||
vault_coordinator::Error::AlreadyBootstrapping
|
||||
| vault_coordinator::Error::AlreadyUnsealing
|
||||
| vault_coordinator::Error::NotBootstrapping
|
||||
| vault_coordinator::Error::DuplicateContribution => {
|
||||
Status::failed_precondition(error.to_string())
|
||||
}
|
||||
vault_coordinator::Error::EmptyCommittee
|
||||
| vault_coordinator::Error::UnsupportedCommittee
|
||||
| vault_coordinator::Error::CommitteeTooLarge => {
|
||||
Status::invalid_argument(error.to_string())
|
||||
}
|
||||
vault_coordinator::Error::InvalidPassphrase => Status::unauthenticated(error.to_string()),
|
||||
_ => Status::internal("Vault ceremony failed"),
|
||||
}
|
||||
}
|
||||
|
||||
fn wrap_bootstrap_response(result: ProtoBootstrapResult) -> OperatorResponsePayload {
|
||||
wrap_vault_response(VaultResponsePayload::Bootstrap(proto_bootstrap::Response {
|
||||
result: result.into(),
|
||||
@@ -87,7 +107,6 @@ impl TryConvert for vault_gate::Outbound {
|
||||
let proto_result = match result {
|
||||
Ok(()) => ProtoUnsealResult::Success,
|
||||
Err(vault_gate::Error::InvalidKey) => ProtoUnsealResult::InvalidKey,
|
||||
Err(vault_gate::Error::LockedOut) => ProtoUnsealResult::LockedOut,
|
||||
Err(err) => {
|
||||
warn!(?err, "unseal failed");
|
||||
return Err(Status::internal("Failed to unseal vault"));
|
||||
@@ -111,6 +130,56 @@ impl TryConvert for vault_gate::Outbound {
|
||||
};
|
||||
Ok(wrap_bootstrap_response(proto_result))
|
||||
}
|
||||
Self::HandleDeclareCommittee(result) => {
|
||||
let proto_result = match result {
|
||||
Ok(()) => ProtoBootstrapResult::AwaitingContributions,
|
||||
Err(vault_gate::Error::Ceremony(
|
||||
vault_coordinator::Error::AlreadyBootstrapped,
|
||||
)) => ProtoBootstrapResult::AlreadyBootstrapped,
|
||||
Err(vault_gate::Error::Ceremony(err)) => {
|
||||
warn!(?err, "declare committee failed");
|
||||
return Err(ceremony_status(&err));
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(?err, "declare committee failed");
|
||||
return Err(Status::internal("Failed to declare committee"));
|
||||
}
|
||||
};
|
||||
Ok(wrap_bootstrap_response(proto_result))
|
||||
}
|
||||
Self::HandleContributeBootstrapPassphrase(result) => {
|
||||
let proto_result = match result {
|
||||
Ok(true) => ProtoBootstrapResult::Success,
|
||||
Ok(false) => ProtoBootstrapResult::AwaitingContributions,
|
||||
Err(vault_gate::Error::Ceremony(
|
||||
vault_coordinator::Error::AlreadyBootstrapped,
|
||||
)) => ProtoBootstrapResult::AlreadyBootstrapped,
|
||||
Err(vault_gate::Error::Ceremony(err)) => {
|
||||
warn!(?err, "contribute bootstrap passphrase failed");
|
||||
return Err(ceremony_status(&err));
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(?err, "contribute bootstrap passphrase failed");
|
||||
return Err(Status::internal(
|
||||
"Failed to contribute bootstrap passphrase",
|
||||
));
|
||||
}
|
||||
};
|
||||
Ok(wrap_bootstrap_response(proto_result))
|
||||
}
|
||||
Self::HandleContributeUnsealPassphrase(result) => {
|
||||
let proto_result = match result {
|
||||
Ok(true) => ProtoUnsealResult::Success,
|
||||
Ok(false) => ProtoUnsealResult::AwaitingContributions,
|
||||
Err(err) => {
|
||||
warn!(?err, "contribute unseal passphrase failed");
|
||||
return Err(Status::internal("Failed to contribute unseal passphrase"));
|
||||
}
|
||||
};
|
||||
Ok(wrap_unseal_response(UnsealResponsePayload::Result(
|
||||
proto_result.into(),
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user