feat(vault): add recovery passphrase handling for bootstrap and unseal processes
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 23:09:49 +02:00
parent 6017ef29ca
commit 9f9b6820c2
7 changed files with 125 additions and 8 deletions

View File

@@ -17,6 +17,11 @@ message ContributePassphrase {
bytes passphrase = 1; bytes passphrase = 1;
} }
message ContributeRecoveryPassphrase {
int32 recovery_operator_id = 1;
bytes passphrase = 2;
}
enum BootstrapResult { enum BootstrapResult {
BOOTSTRAP_RESULT_UNSPECIFIED = 0; BOOTSTRAP_RESULT_UNSPECIFIED = 0;
BOOTSTRAP_RESULT_SUCCESS = 1; BOOTSTRAP_RESULT_SUCCESS = 1;
@@ -27,9 +32,10 @@ enum BootstrapResult {
message Request { message Request {
oneof payload { oneof payload {
BootstrapEncryptedKey encrypted_key = 2; BootstrapEncryptedKey encrypted_key = 2;
DeclareCommittee declare_committee = 3; DeclareCommittee declare_committee = 3;
ContributePassphrase contribute_passphrase = 4; ContributePassphrase contribute_passphrase = 4;
ContributeRecoveryPassphrase contribute_recovery_passphrase = 5;
} }
} }

View File

@@ -19,6 +19,11 @@ message ContributePassphrase {
bytes passphrase = 1; bytes passphrase = 1;
} }
message ContributeRecoveryPassphrase {
int32 recovery_operator_id = 1;
bytes passphrase = 2;
}
enum UnsealResult { enum UnsealResult {
UNSEAL_RESULT_UNSPECIFIED = 0; UNSEAL_RESULT_UNSPECIFIED = 0;
UNSEAL_RESULT_SUCCESS = 1; UNSEAL_RESULT_SUCCESS = 1;
@@ -29,9 +34,10 @@ enum UnsealResult {
message Request { message Request {
oneof payload { oneof payload {
UnsealStart start = 1; UnsealStart start = 1;
UnsealEncryptedKey encrypted_key = 2; UnsealEncryptedKey encrypted_key = 2;
ContributePassphrase contribute_passphrase = 3; ContributePassphrase contribute_passphrase = 3;
ContributeRecoveryPassphrase contribute_recovery_passphrase = 4;
} }
} }

View File

@@ -246,9 +246,19 @@ create table if not exists proposal_result (
) STRICT; ) 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 ( create table if not exists recovery_operator_identity (
id integer not null primary key, id integer not null primary key,
public_key blob not null unique, public_key blob not null unique,

View File

@@ -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! { diesel::table! {
recovery_operator_identity (id) { recovery_operator_identity (id) {
id -> Integer, id -> Integer,
@@ -290,6 +301,7 @@ diesel::joinable!(proposal -> operator_identity (initiator_id));
diesel::joinable!(proposal_result -> proposal (proposal_id)); diesel::joinable!(proposal_result -> proposal (proposal_id));
diesel::joinable!(proposal_vote -> proposal (proposal_id)); diesel::joinable!(proposal_vote -> proposal (proposal_id));
diesel::joinable!(proposal_vote -> operator_identity (operator_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 -> proposal (proposal_id));
diesel::joinable!(recovery_proposal_vote -> recovery_operator_identity (recovery_operator_id)); diesel::joinable!(recovery_proposal_vote -> recovery_operator_identity (recovery_operator_id));
diesel::joinable!(recovery_wakeup_request -> operator_identity (requested_by)); 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!( diesel::allow_tables_to_appear_in_same_query!(
aead_encrypted, aead_encrypted,
proposal_result, proposal_result,
recovery_operator,
recovery_operator_identity, recovery_operator_identity,
recovery_wakeup_request, recovery_wakeup_request,
recovery_proposal_vote, recovery_proposal_vote,

View File

@@ -2,6 +2,7 @@ use crate::{
grpc::{Convert, TryConvert}, grpc::{Convert, TryConvert},
peers::operator::vault_gate::{ peers::operator::vault_gate::{
self as vault_gate, HandleBootstrapEncryptedKey, HandleContributeBootstrapPassphrase, self as vault_gate, HandleBootstrapEncryptedKey, HandleContributeBootstrapPassphrase,
HandleContributeRecoveryBootstrapPassphrase, HandleContributeRecoveryUnsealPassphrase,
HandleContributeUnsealPassphrase, HandleDeclareCommittee, HandleHandshake, HandleContributeUnsealPassphrase, HandleDeclareCommittee, HandleHandshake,
HandleUnsealEncryptedKey, 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,
},
),
),
} }
} }
} }

View File

@@ -131,6 +131,19 @@ impl TryConvert for vault_gate::Outbound {
}; };
Ok(wrap_bootstrap_response(proto_result)) 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) => { Self::HandleContributeUnsealPassphrase(result) => {
let proto_result = match result { let proto_result = match result {
Ok(true) => ProtoUnsealResult::Success, Ok(true) => ProtoUnsealResult::Success,
@@ -144,6 +157,21 @@ impl TryConvert for vault_gate::Outbound {
proto_result.into(), 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(),
)))
}
} }
} }
} }

View File

@@ -3,7 +3,10 @@ use crate::{
actors::{ actors::{
GlobalActors, GlobalActors,
vault::{self, Bootstrap, GetState, TryUnseal, VaultState, events}, vault::{self, Bootstrap, GetState, TryUnseal, VaultState, events},
vault_coordinator::{ContributeBootstrap, ContributeUnseal, StartBootstrap}, vault_coordinator::{
ContributeBootstrap, ContributeRecoveryBootstrap, ContributeRecoveryUnseal,
ContributeUnseal, StartBootstrap,
},
}, },
crypto::{KeyCell, integrity::{self}}, crypto::{KeyCell, integrity::{self}},
db::DatabasePool, db::DatabasePool,
@@ -266,6 +269,23 @@ impl VaultGate {
.map_err(|_| Error::internal("VaultCoordinator unavailable")) .map_err(|_| Error::internal("VaultCoordinator unavailable"))
} }
#[message]
pub async fn handle_contribute_recovery_bootstrap_passphrase(
&mut self,
recovery_operator_id: i32,
passphrase: Vec<u8>,
) -> Result<bool, Error> {
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] #[message]
pub async fn handle_contribute_unseal_passphrase( pub async fn handle_contribute_unseal_passphrase(
&mut self, &mut self,
@@ -281,6 +301,23 @@ impl VaultGate {
.await .await
.map_err(|_| Error::internal("VaultCoordinator unavailable")) .map_err(|_| Error::internal("VaultCoordinator unavailable"))
} }
#[message]
pub async fn handle_contribute_recovery_unseal_passphrase(
&mut self,
recovery_operator_id: i32,
passphrase: Vec<u8>,
) -> Result<bool, Error> {
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<events::Bootstrapped> for VaultGate { impl Message<events::Bootstrapped> for VaultGate {