From 9f9b6820c2d4c3b6d7424718544c9dd94a5a0493 Mon Sep 17 00:00:00 2001 From: CleverWild Date: Sat, 13 Jun 2026 23:09:49 +0200 Subject: [PATCH] feat(vault): add recovery passphrase handling for bootstrap and unseal processes --- protobufs/operator/vault/bootstrap.proto | 12 ++++-- protobufs/operator/vault/unseal.proto | 12 ++++-- .../2026-02-14-171124-0000_init/up.sql | 12 +++++- server/crates/arbiter-server/src/db/schema.rs | 13 +++++++ .../src/grpc/operator/vault_gate/inbound.rs | 17 ++++++++ .../src/grpc/operator/vault_gate/outbound.rs | 28 +++++++++++++ .../src/peers/operator/vault_gate/mod.rs | 39 ++++++++++++++++++- 7 files changed, 125 insertions(+), 8 deletions(-) diff --git a/protobufs/operator/vault/bootstrap.proto b/protobufs/operator/vault/bootstrap.proto index 5fab6a4..fc0edf8 100644 --- a/protobufs/operator/vault/bootstrap.proto +++ b/protobufs/operator/vault/bootstrap.proto @@ -17,6 +17,11 @@ message ContributePassphrase { bytes passphrase = 1; } +message ContributeRecoveryPassphrase { + int32 recovery_operator_id = 1; + bytes passphrase = 2; +} + enum BootstrapResult { BOOTSTRAP_RESULT_UNSPECIFIED = 0; BOOTSTRAP_RESULT_SUCCESS = 1; @@ -27,9 +32,10 @@ enum BootstrapResult { message Request { oneof payload { - BootstrapEncryptedKey encrypted_key = 2; - DeclareCommittee declare_committee = 3; - ContributePassphrase contribute_passphrase = 4; + BootstrapEncryptedKey encrypted_key = 2; + DeclareCommittee declare_committee = 3; + ContributePassphrase contribute_passphrase = 4; + ContributeRecoveryPassphrase contribute_recovery_passphrase = 5; } } diff --git a/protobufs/operator/vault/unseal.proto b/protobufs/operator/vault/unseal.proto index 598c81c..5e770fd 100644 --- a/protobufs/operator/vault/unseal.proto +++ b/protobufs/operator/vault/unseal.proto @@ -19,6 +19,11 @@ message ContributePassphrase { bytes passphrase = 1; } +message ContributeRecoveryPassphrase { + int32 recovery_operator_id = 1; + bytes passphrase = 2; +} + enum UnsealResult { UNSEAL_RESULT_UNSPECIFIED = 0; UNSEAL_RESULT_SUCCESS = 1; @@ -29,9 +34,10 @@ enum UnsealResult { message Request { oneof payload { - UnsealStart start = 1; - UnsealEncryptedKey encrypted_key = 2; - ContributePassphrase contribute_passphrase = 3; + UnsealStart start = 1; + UnsealEncryptedKey encrypted_key = 2; + ContributePassphrase contribute_passphrase = 3; + ContributeRecoveryPassphrase contribute_recovery_passphrase = 4; } } diff --git a/server/crates/arbiter-server/migrations/2026-02-14-171124-0000_init/up.sql b/server/crates/arbiter-server/migrations/2026-02-14-171124-0000_init/up.sql index 08ba766..0168767 100644 --- a/server/crates/arbiter-server/migrations/2026-02-14-171124-0000_init/up.sql +++ b/server/crates/arbiter-server/migrations/2026-02-14-171124-0000_init/up.sql @@ -246,9 +246,19 @@ create table if not exists proposal_result ( ) STRICT; -- =============================== --- Recovery Operators (§3.5/§3.6) +-- Recovery Operators (§3.4/§3.5/§3.6) -- =============================== +-- Encrypted Shamir shares for recovery operators (mirrors the `operator` table). +create table if not exists recovery_operator ( + id integer not null primary key references recovery_operator_identity(id) on delete restrict, + share blob not null, + share_nonce blob not null, + share_salt blob not null, + created_at integer not null default(unixepoch('now')), + updated_at integer not null default(unixepoch('now')) +) STRICT; + create table if not exists recovery_operator_identity ( id integer not null primary key, public_key blob not null unique, diff --git a/server/crates/arbiter-server/src/db/schema.rs b/server/crates/arbiter-server/src/db/schema.rs index 4b6aa1d..cbc7705 100644 --- a/server/crates/arbiter-server/src/db/schema.rs +++ b/server/crates/arbiter-server/src/db/schema.rs @@ -192,6 +192,17 @@ diesel::table! { } } +diesel::table! { + recovery_operator (id) { + id -> Integer, + share -> Binary, + share_nonce -> Binary, + share_salt -> Binary, + created_at -> Integer, + updated_at -> Integer, + } +} + diesel::table! { recovery_operator_identity (id) { id -> Integer, @@ -290,6 +301,7 @@ diesel::joinable!(proposal -> operator_identity (initiator_id)); diesel::joinable!(proposal_result -> proposal (proposal_id)); diesel::joinable!(proposal_vote -> proposal (proposal_id)); diesel::joinable!(proposal_vote -> operator_identity (operator_id)); +diesel::joinable!(recovery_operator -> recovery_operator_identity (id)); diesel::joinable!(recovery_proposal_vote -> proposal (proposal_id)); diesel::joinable!(recovery_proposal_vote -> recovery_operator_identity (recovery_operator_id)); diesel::joinable!(recovery_wakeup_request -> operator_identity (requested_by)); @@ -297,6 +309,7 @@ diesel::joinable!(recovery_wakeup_request -> operator_identity (requested_by)); diesel::allow_tables_to_appear_in_same_query!( aead_encrypted, proposal_result, + recovery_operator, recovery_operator_identity, recovery_wakeup_request, recovery_proposal_vote, diff --git a/server/crates/arbiter-server/src/grpc/operator/vault_gate/inbound.rs b/server/crates/arbiter-server/src/grpc/operator/vault_gate/inbound.rs index 5342282..01a1a10 100644 --- a/server/crates/arbiter-server/src/grpc/operator/vault_gate/inbound.rs +++ b/server/crates/arbiter-server/src/grpc/operator/vault_gate/inbound.rs @@ -2,6 +2,7 @@ use crate::{ grpc::{Convert, TryConvert}, peers::operator::vault_gate::{ self as vault_gate, HandleBootstrapEncryptedKey, HandleContributeBootstrapPassphrase, + HandleContributeRecoveryBootstrapPassphrase, HandleContributeRecoveryUnsealPassphrase, HandleContributeUnsealPassphrase, HandleDeclareCommittee, HandleHandshake, HandleUnsealEncryptedKey, }, @@ -82,6 +83,14 @@ impl TryConvert for UnsealRequestPayload { }, ), ), + Self::ContributeRecoveryPassphrase(crp) => Ok( + vault_gate::Inbound::HandleContributeRecoveryUnsealPassphrase( + HandleContributeRecoveryUnsealPassphrase { + recovery_operator_id: crp.recovery_operator_id, + passphrase: crp.passphrase, + }, + ), + ), } } } @@ -142,6 +151,14 @@ impl TryConvert for BootstrapRequestPayload { }, ), ), + Self::ContributeRecoveryPassphrase(crp) => Ok( + vault_gate::Inbound::HandleContributeRecoveryBootstrapPassphrase( + HandleContributeRecoveryBootstrapPassphrase { + recovery_operator_id: crp.recovery_operator_id, + passphrase: crp.passphrase, + }, + ), + ), } } } diff --git a/server/crates/arbiter-server/src/grpc/operator/vault_gate/outbound.rs b/server/crates/arbiter-server/src/grpc/operator/vault_gate/outbound.rs index 1e44ca4..fc8bf7f 100644 --- a/server/crates/arbiter-server/src/grpc/operator/vault_gate/outbound.rs +++ b/server/crates/arbiter-server/src/grpc/operator/vault_gate/outbound.rs @@ -131,6 +131,19 @@ impl TryConvert for vault_gate::Outbound { }; Ok(wrap_bootstrap_response(proto_result)) } + Self::HandleContributeRecoveryBootstrapPassphrase(result) => { + let proto_result = match result { + Ok(true) => ProtoBootstrapResult::Success, + Ok(false) => ProtoBootstrapResult::AwaitingContributions, + Err(err) => { + warn!(?err, "contribute recovery bootstrap passphrase failed"); + return Err(Status::internal( + "Failed to contribute recovery bootstrap passphrase", + )); + } + }; + Ok(wrap_bootstrap_response(proto_result)) + } Self::HandleContributeUnsealPassphrase(result) => { let proto_result = match result { Ok(true) => ProtoUnsealResult::Success, @@ -144,6 +157,21 @@ impl TryConvert for vault_gate::Outbound { proto_result.into(), ))) } + Self::HandleContributeRecoveryUnsealPassphrase(result) => { + let proto_result = match result { + Ok(true) => ProtoUnsealResult::Success, + Ok(false) => ProtoUnsealResult::AwaitingContributions, + Err(err) => { + warn!(?err, "contribute recovery unseal passphrase failed"); + return Err(Status::internal( + "Failed to contribute recovery unseal passphrase", + )); + } + }; + Ok(wrap_unseal_response(UnsealResponsePayload::Result( + proto_result.into(), + ))) + } } } } diff --git a/server/crates/arbiter-server/src/peers/operator/vault_gate/mod.rs b/server/crates/arbiter-server/src/peers/operator/vault_gate/mod.rs index 611ba82..d416401 100644 --- a/server/crates/arbiter-server/src/peers/operator/vault_gate/mod.rs +++ b/server/crates/arbiter-server/src/peers/operator/vault_gate/mod.rs @@ -3,7 +3,10 @@ use crate::{ actors::{ GlobalActors, vault::{self, Bootstrap, GetState, TryUnseal, VaultState, events}, - vault_coordinator::{ContributeBootstrap, ContributeUnseal, StartBootstrap}, + vault_coordinator::{ + ContributeBootstrap, ContributeRecoveryBootstrap, ContributeRecoveryUnseal, + ContributeUnseal, StartBootstrap, + }, }, crypto::{KeyCell, integrity::{self}}, db::DatabasePool, @@ -266,6 +269,23 @@ impl VaultGate { .map_err(|_| Error::internal("VaultCoordinator unavailable")) } + #[message] + pub async fn handle_contribute_recovery_bootstrap_passphrase( + &mut self, + recovery_operator_id: i32, + passphrase: Vec, + ) -> Result { + let passphrase_cell = SafeCell::new(passphrase); + self.actors + .vault_coordinator + .ask(ContributeRecoveryBootstrap { + recovery_operator_id, + passphrase: passphrase_cell, + }) + .await + .map_err(|_| Error::internal("VaultCoordinator unavailable")) + } + #[message] pub async fn handle_contribute_unseal_passphrase( &mut self, @@ -281,6 +301,23 @@ impl VaultGate { .await .map_err(|_| Error::internal("VaultCoordinator unavailable")) } + + #[message] + pub async fn handle_contribute_recovery_unseal_passphrase( + &mut self, + recovery_operator_id: i32, + passphrase: Vec, + ) -> Result { + let passphrase_cell = SafeCell::new(passphrase); + self.actors + .vault_coordinator + .ask(ContributeRecoveryUnseal { + recovery_operator_id, + passphrase: passphrase_cell, + }) + .await + .map_err(|_| Error::internal("VaultCoordinator unavailable")) + } } impl Message for VaultGate {