Compare commits
7 Commits
9dbb18ae82
...
0d364d1951
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d364d1951 | ||
|
|
6f65c907a3 | ||
|
|
9764b0d5ce | ||
|
|
50fe18d6ce | ||
|
|
3e5f0cb3df | ||
|
|
34850137df | ||
|
|
d1b96c8409 |
@@ -8,15 +8,28 @@ message BootstrapEncryptedKey {
|
|||||||
bytes associated_data = 3;
|
bytes associated_data = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message DeclareCommittee {
|
||||||
|
uint32 count = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ContributePassphrase {
|
||||||
|
bytes passphrase = 1;
|
||||||
|
}
|
||||||
|
|
||||||
enum BootstrapResult {
|
enum BootstrapResult {
|
||||||
BOOTSTRAP_RESULT_UNSPECIFIED = 0;
|
BOOTSTRAP_RESULT_UNSPECIFIED = 0;
|
||||||
BOOTSTRAP_RESULT_SUCCESS = 1;
|
BOOTSTRAP_RESULT_SUCCESS = 1;
|
||||||
BOOTSTRAP_RESULT_ALREADY_BOOTSTRAPPED = 2;
|
BOOTSTRAP_RESULT_ALREADY_BOOTSTRAPPED = 2;
|
||||||
BOOTSTRAP_RESULT_INVALID_KEY = 3;
|
BOOTSTRAP_RESULT_INVALID_KEY = 3;
|
||||||
|
BOOTSTRAP_RESULT_AWAITING_CONTRIBUTIONS = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
message Request {
|
message Request {
|
||||||
BootstrapEncryptedKey encrypted_key = 2;
|
oneof payload {
|
||||||
|
BootstrapEncryptedKey encrypted_key = 2;
|
||||||
|
DeclareCommittee declare_committee = 3;
|
||||||
|
ContributePassphrase contribute_passphrase = 4;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
message Response {
|
message Response {
|
||||||
|
|||||||
@@ -15,17 +15,23 @@ message UnsealEncryptedKey {
|
|||||||
bytes associated_data = 3;
|
bytes associated_data = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message ContributePassphrase {
|
||||||
|
bytes passphrase = 1;
|
||||||
|
}
|
||||||
|
|
||||||
enum UnsealResult {
|
enum UnsealResult {
|
||||||
UNSEAL_RESULT_UNSPECIFIED = 0;
|
UNSEAL_RESULT_UNSPECIFIED = 0;
|
||||||
UNSEAL_RESULT_SUCCESS = 1;
|
UNSEAL_RESULT_SUCCESS = 1;
|
||||||
UNSEAL_RESULT_INVALID_KEY = 2;
|
UNSEAL_RESULT_INVALID_KEY = 2;
|
||||||
UNSEAL_RESULT_UNBOOTSTRAPPED = 3;
|
UNSEAL_RESULT_UNBOOTSTRAPPED = 3;
|
||||||
|
UNSEAL_RESULT_AWAITING_CONTRIBUTIONS = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
8
server/Cargo.lock
generated
8
server/Cargo.lock
generated
@@ -771,6 +771,7 @@ dependencies = [
|
|||||||
"proptest",
|
"proptest",
|
||||||
"prost-types",
|
"prost-types",
|
||||||
"rand 0.10.1",
|
"rand 0.10.1",
|
||||||
|
"rand_core 0.6.4",
|
||||||
"rcgen",
|
"rcgen",
|
||||||
"restructed",
|
"restructed",
|
||||||
"rstest",
|
"rstest",
|
||||||
@@ -3027,7 +3028,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "kameo"
|
name = "kameo"
|
||||||
version = "0.20.0"
|
version = "0.20.0"
|
||||||
source = "git+https://github.com/hdbg/kameo.git?rev=805b417#805b41783fe90b54827ecad142b422c7a9b69b9a"
|
source = "git+https://github.com/hdbg/kameo.git?rev=3e18ba2#3e18ba24023d0422034e60ff2ea1ecd49e8c3c93"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"downcast-rs",
|
"downcast-rs",
|
||||||
"dyn-clone",
|
"dyn-clone",
|
||||||
@@ -3041,7 +3042,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "kameo_actors"
|
name = "kameo_actors"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
source = "git+https://github.com/hdbg/kameo.git?rev=805b417#805b41783fe90b54827ecad142b422c7a9b69b9a"
|
source = "git+https://github.com/hdbg/kameo.git?rev=3e18ba2#3e18ba24023d0422034e60ff2ea1ecd49e8c3c93"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures",
|
"futures",
|
||||||
"glob",
|
"glob",
|
||||||
@@ -3053,9 +3054,8 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "kameo_macros"
|
name = "kameo_macros"
|
||||||
version = "0.20.0"
|
version = "0.20.0"
|
||||||
source = "git+https://github.com/hdbg/kameo.git?rev=805b417#805b41783fe90b54827ecad142b422c7a9b69b9a"
|
source = "git+https://github.com/hdbg/kameo.git?rev=3e18ba2#3e18ba24023d0422034e60ff2ea1ecd49e8c3c93"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling 0.23.0",
|
|
||||||
"heck",
|
"heck",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ base64 = "0.22.1"
|
|||||||
chrono = { version = "0.4.44", features = ["serde"] }
|
chrono = { version = "0.4.44", features = ["serde"] }
|
||||||
futures = "0.3.32"
|
futures = "0.3.32"
|
||||||
k256 = { version = "0.13.4", features = ["ecdsa", "pkcs8"] }
|
k256 = { version = "0.13.4", features = ["ecdsa", "pkcs8"] }
|
||||||
kameo = {git = "https://github.com/hdbg/kameo.git", rev = "805b417"}
|
kameo = {git = "https://github.com/hdbg/kameo.git", rev = "3e18ba2"}
|
||||||
kameo_actors = {git = "https://github.com/hdbg/kameo.git", rev = "805b417"}
|
kameo_actors = {git = "https://github.com/hdbg/kameo.git", rev = "3e18ba2"}
|
||||||
hmac = "0.13.0"
|
hmac = "0.13.0"
|
||||||
miette = { version = "7.6.0", features = ["fancy", "serde"] }
|
miette = { version = "7.6.0", features = ["fancy", "serde"] }
|
||||||
ml-dsa = { version = "0.1.0-rc.9", features = ["zeroize"] }
|
ml-dsa = { version = "0.1.0-rc.9", features = ["zeroize"] }
|
||||||
@@ -106,7 +106,6 @@ indexing_slicing = "warn"
|
|||||||
infinite_loop = "warn"
|
infinite_loop = "warn"
|
||||||
inline_asm_x86_att_syntax = "warn"
|
inline_asm_x86_att_syntax = "warn"
|
||||||
inline_asm_x86_intel_syntax = "warn"
|
inline_asm_x86_intel_syntax = "warn"
|
||||||
integer_division = "warn"
|
|
||||||
large_include_file = "warn"
|
large_include_file = "warn"
|
||||||
lossy_float_literal = "warn"
|
lossy_float_literal = "warn"
|
||||||
map_with_unused_argument_over_ranges = "warn"
|
map_with_unused_argument_over_ranges = "warn"
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ x25519-dalek.workspace = true
|
|||||||
k256.workspace = true
|
k256.workspace = true
|
||||||
kameo_actors.workspace = true
|
kameo_actors.workspace = true
|
||||||
vsss-rs = "5.4.0"
|
vsss-rs = "5.4.0"
|
||||||
|
rand_core = "0.6"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
proptest = "1.11.0"
|
proptest = "1.11.0"
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ create table if not exists operator (
|
|||||||
|
|
||||||
share blob not null,
|
share blob not null,
|
||||||
share_nonce blob not null,
|
share_nonce blob not null,
|
||||||
|
share_salt blob not null default (randomblob(32)),
|
||||||
|
|
||||||
created_at integer not null default(unixepoch ('now')),
|
created_at integer not null default(unixepoch ('now')),
|
||||||
updated_at integer not null default(unixepoch ('now'))
|
updated_at integer not null default(unixepoch ('now'))
|
||||||
|
|||||||
@@ -160,29 +160,23 @@ impl EvmActor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[message]
|
#[message]
|
||||||
#[expect(clippy::unused_async, reason = "reserved for impl")]
|
pub async fn operator_delete_grant(&mut self, grant_id: i32) -> Result<(), Error> {
|
||||||
pub async fn operator_delete_grant(&mut self, _grant_id: i32) -> Result<(), Error> {
|
let mut conn = self.db.get().await.map_err(DatabaseError::from)?;
|
||||||
// let mut conn = self.db.get().await.map_err(DatabaseError::from)?;
|
|
||||||
// let vault = self.vault.clone();
|
|
||||||
|
|
||||||
// diesel_async::AsyncConnection::transaction(&mut conn, |conn| {
|
let affected = diesel::update(schema::evm_basic_grant::table)
|
||||||
// Box::pin(async move {
|
.filter(schema::evm_basic_grant::id.eq(grant_id))
|
||||||
// diesel::update(schema::evm_basic_grant::table)
|
.set(schema::evm_basic_grant::revoked_at.eq(models::SqliteTimestamp::now()))
|
||||||
// .filter(schema::evm_basic_grant::id.eq(grant_id))
|
.execute(&mut conn)
|
||||||
// .set(schema::evm_basic_grant::revoked_at.eq(SqliteTimestamp::now()))
|
.await
|
||||||
// .execute(conn)
|
.map_err(DatabaseError::from)?;
|
||||||
// .await?;
|
|
||||||
|
|
||||||
// let signed = integrity::evm::load_signed_grant_by_basic_id(conn, grant_id).await?;
|
if affected == 0 {
|
||||||
|
return Err(Error::Database(DatabaseError::from(
|
||||||
|
diesel::result::Error::NotFound,
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
// diesel::result::QueryResult::Ok(())
|
Ok(())
|
||||||
// })
|
|
||||||
// })
|
|
||||||
// .await
|
|
||||||
// .map_err(DatabaseError::from)?;
|
|
||||||
|
|
||||||
// Ok(())
|
|
||||||
todo!()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[message]
|
#[message]
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use crate::{
|
|||||||
actors::{
|
actors::{
|
||||||
bootstrap::Bootstrapper, evm::EvmActor, flow_coordinator::FlowCoordinator,
|
bootstrap::Bootstrapper, evm::EvmActor, flow_coordinator::FlowCoordinator,
|
||||||
operator_registry::OperatorRegistry, vault::Vault,
|
operator_registry::OperatorRegistry, vault::Vault,
|
||||||
|
vault_coordinator::VaultCoordinator,
|
||||||
},
|
},
|
||||||
db,
|
db,
|
||||||
};
|
};
|
||||||
@@ -15,6 +16,7 @@ pub mod evm;
|
|||||||
pub mod flow_coordinator;
|
pub mod flow_coordinator;
|
||||||
pub mod operator_registry;
|
pub mod operator_registry;
|
||||||
pub mod vault;
|
pub mod vault;
|
||||||
|
pub mod vault_coordinator;
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum SpawnError {
|
pub enum SpawnError {
|
||||||
@@ -30,6 +32,7 @@ pub enum SpawnError {
|
|||||||
pub struct GlobalActors {
|
pub struct GlobalActors {
|
||||||
pub vault: ActorRef<Vault>,
|
pub vault: ActorRef<Vault>,
|
||||||
pub bootstrapper: ActorRef<Bootstrapper>,
|
pub bootstrapper: ActorRef<Bootstrapper>,
|
||||||
|
pub vault_coordinator: ActorRef<VaultCoordinator>,
|
||||||
pub flow_coordinator: ActorRef<FlowCoordinator>,
|
pub flow_coordinator: ActorRef<FlowCoordinator>,
|
||||||
pub operator_registry: ActorRef<OperatorRegistry>,
|
pub operator_registry: ActorRef<OperatorRegistry>,
|
||||||
pub evm: ActorRef<EvmActor>,
|
pub evm: ActorRef<EvmActor>,
|
||||||
@@ -47,7 +50,11 @@ impl GlobalActors {
|
|||||||
let operator_registry = OperatorRegistry::spawn(OperatorRegistry::default());
|
let operator_registry = OperatorRegistry::spawn(OperatorRegistry::default());
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
bootstrapper: Bootstrapper::spawn(Bootstrapper::new(&db).await?),
|
bootstrapper: Bootstrapper::spawn(Bootstrapper::new(&db).await?),
|
||||||
evm: EvmActor::spawn(EvmActor::new(key_holder.clone(), db)),
|
evm: EvmActor::spawn(EvmActor::new(key_holder.clone(), db.clone())),
|
||||||
|
vault_coordinator: VaultCoordinator::spawn(VaultCoordinator::new(
|
||||||
|
db,
|
||||||
|
key_holder.clone(),
|
||||||
|
)),
|
||||||
vault: key_holder,
|
vault: key_holder,
|
||||||
flow_coordinator: FlowCoordinator::spawn(FlowCoordinator::new(
|
flow_coordinator: FlowCoordinator::spawn(FlowCoordinator::new(
|
||||||
operator_registry.clone(),
|
operator_registry.clone(),
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
crypto::{
|
crypto::{
|
||||||
KeyCell, derive_key,
|
KeyCell,
|
||||||
encryption::v1::{self, Nonce},
|
encryption::v1::{self, Nonce},
|
||||||
integrity::v1::HmacSha256,
|
integrity::v1::HmacSha256,
|
||||||
},
|
},
|
||||||
db::{
|
db::{
|
||||||
self,
|
self,
|
||||||
models::{self, OperatorId, OperatorIdentityId, RootKeyHistory, RootKeyHistoryId},
|
models::{self, RootKeyHistory, RootKeyHistoryId},
|
||||||
schema::{self},
|
schema,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
|
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
|
||||||
@@ -17,11 +15,10 @@ use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
|
|||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use diesel::{
|
use diesel::{
|
||||||
ExpressionMethods as _, OptionalExtension, QueryDsl, SelectableHelper,
|
ExpressionMethods as _, OptionalExtension, QueryDsl, SelectableHelper,
|
||||||
dsl::{count, insert_into, update},
|
dsl::{insert_into, update},
|
||||||
select,
|
|
||||||
};
|
};
|
||||||
use diesel_async::{AsyncConnection, RunQueryDsl};
|
use diesel_async::{AsyncConnection, RunQueryDsl};
|
||||||
use hmac::{KeyInit as _, Mac as _, digest::common};
|
use hmac::{KeyInit as _, Mac as _};
|
||||||
use kameo::{Actor, Reply, actor::ActorRef, messages};
|
use kameo::{Actor, Reply, actor::ActorRef, messages};
|
||||||
use kameo_actors::message_bus::{MessageBus, Publish};
|
use kameo_actors::message_bus::{MessageBus, Publish};
|
||||||
use strum::{EnumDiscriminants, IntoDiscriminant};
|
use strum::{EnumDiscriminants, IntoDiscriminant};
|
||||||
@@ -65,15 +62,6 @@ pub enum Error {
|
|||||||
BrokenDatabase,
|
BrokenDatabase,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum UnsealError {}
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum BootstrapError {
|
|
||||||
#[error("That operator already contributed his share")]
|
|
||||||
AlreadyContributed,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Unsealed {
|
struct Unsealed {
|
||||||
root_key_history_id: RootKeyHistoryId,
|
root_key_history_id: RootKeyHistoryId,
|
||||||
root_key: KeyCell,
|
root_key: KeyCell,
|
||||||
@@ -85,15 +73,8 @@ enum State {
|
|||||||
#[default]
|
#[default]
|
||||||
Unbootstrapped,
|
Unbootstrapped,
|
||||||
|
|
||||||
Bootstrapping {
|
|
||||||
declared_operators: u64,
|
|
||||||
current_passphrases: HashMap<OperatorIdentityId, SafeCell<Vec<u8>>>,
|
|
||||||
},
|
|
||||||
|
|
||||||
Sealed {
|
Sealed {
|
||||||
threshold: u64, // basically, quorum size
|
|
||||||
root_key_history_id: RootKeyHistoryId,
|
root_key_history_id: RootKeyHistoryId,
|
||||||
current_shares: HashMap<OperatorId, SafeCell<Vec<u8>>>,
|
|
||||||
},
|
},
|
||||||
Unsealed(Unsealed),
|
Unsealed(Unsealed),
|
||||||
}
|
}
|
||||||
@@ -121,17 +102,9 @@ impl Vault {
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
match root_key_history {
|
match root_key_history {
|
||||||
Some(root_key_history) => {
|
Some(root_key_history) => State::Sealed {
|
||||||
let operator_count: i64 = schema::operator::table
|
root_key_history_id: root_key_history.id,
|
||||||
.count()
|
},
|
||||||
.get_result(&mut conn)
|
|
||||||
.await?;
|
|
||||||
State::Sealed {
|
|
||||||
root_key_history_id: root_key_history.id,
|
|
||||||
current_shares: HashMap::default(),
|
|
||||||
threshold: shamir_threshold(operator_count.cast_unsigned()), // invariant: db couldn't return negative number of rows
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => State::Unbootstrapped,
|
None => State::Unbootstrapped,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -139,7 +112,7 @@ impl Vault {
|
|||||||
Ok(Self { db, state, events })
|
Ok(Self { db, state, events })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exclusive transaction to avoid race condtions if multiple vaults write
|
// Exclusive transaction to avoid race conditions if multiple vaults write
|
||||||
// additional layer of protection against nonce-reuse
|
// additional layer of protection against nonce-reuse
|
||||||
async fn get_new_nonce(
|
async fn get_new_nonce(
|
||||||
pool: &db::DatabasePool,
|
pool: &db::DatabasePool,
|
||||||
@@ -180,37 +153,33 @@ impl Vault {
|
|||||||
const fn expect_unsealed(state: &mut State) -> Result<&mut Unsealed, Error> {
|
const fn expect_unsealed(state: &mut State) -> Result<&mut Unsealed, Error> {
|
||||||
match state {
|
match state {
|
||||||
State::Unsealed(unsealed) => Ok(unsealed),
|
State::Unsealed(unsealed) => Ok(unsealed),
|
||||||
State::Bootstrapping { .. } => Err(Error::NotBootstrapped),
|
|
||||||
State::Unbootstrapped => Err(Error::NotBootstrapped),
|
State::Unbootstrapped => Err(Error::NotBootstrapped),
|
||||||
State::Sealed { .. } => Err(Error::Sealed),
|
State::Sealed { .. } => Err(Error::Sealed),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn finalize_bootstrap(&mut self) -> Result<(), Error> {
|
#[messages]
|
||||||
let State::Bootstrapping {
|
impl Vault {
|
||||||
declared_operators,
|
#[message]
|
||||||
current_passphrases,
|
pub async fn bootstrap(&mut self, seal_key_raw: SafeCell<Vec<u8>>) -> Result<(), Error> {
|
||||||
} = &mut self.state
|
if !matches!(&self.state, State::Unbootstrapped) {
|
||||||
else {
|
|
||||||
return Err(Error::AlreadyBootstrapped);
|
return Err(Error::AlreadyBootstrapped);
|
||||||
};
|
}
|
||||||
|
|
||||||
let mut root_key = KeyCell::new_secure_random();
|
let mut root_key = KeyCell::new_secure_random();
|
||||||
let root_key_salt = v1::generate_salt();
|
let mut seal_key = KeyCell::try_from(seal_key_raw).map_err(|()| Error::InvalidKey)?;
|
||||||
|
|
||||||
let mut seal_key = KeyCell::new_secure_random();
|
|
||||||
|
|
||||||
let shares = seal_key.0.read_inline(|seal_key| {
|
|
||||||
generate_shamir_shares(current_passphrases.len() as u64, seal_key.as_slice())
|
|
||||||
});
|
|
||||||
|
|
||||||
// Zero nonces are fine because they are one-time
|
// Zero nonces are fine because they are one-time
|
||||||
let root_key_nonce = Nonce::default();
|
let root_key_nonce = Nonce::default();
|
||||||
let data_encryption_nonce = Nonce::default();
|
let data_encryption_nonce = Nonce::default();
|
||||||
|
|
||||||
let root_key_ciphertext: Vec<u8> = root_key.0.read_inline(|reader| {
|
// Generate salt (kept for schema compat)
|
||||||
let root_key_reader = reader.as_slice();
|
let root_key_salt = v1::generate_salt();
|
||||||
|
|
||||||
|
let root_key_ciphertext: Vec<u8> = root_key.0.read_inline(|rk| {
|
||||||
seal_key
|
seal_key
|
||||||
.encrypt(&root_key_nonce, v1::ROOT_KEY_TAG, root_key_reader)
|
.encrypt(&root_key_nonce, v1::ROOT_KEY_TAG, rk.as_slice())
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
error!(?err, "Fatal bootstrap error");
|
error!(?err, "Fatal bootstrap error");
|
||||||
Error::Encryption(err)
|
Error::Encryption(err)
|
||||||
@@ -222,16 +191,6 @@ impl Vault {
|
|||||||
|
|
||||||
let root_key_history_id = conn
|
let root_key_history_id = conn
|
||||||
.transaction(async |conn| {
|
.transaction(async |conn| {
|
||||||
for ((operator_id, raw_passphrase), raw_share) in
|
|
||||||
current_passphrases.iter_mut().zip(shares.iter())
|
|
||||||
{
|
|
||||||
let salt = v1::generate_salt();
|
|
||||||
let mut share_seal_key = derive_key(&mut raw_passphrase, &salt);
|
|
||||||
let share_encryption_nonce = Nonce::default();
|
|
||||||
|
|
||||||
let share_key = derive_key(&mut raw_passphrase, &salt);
|
|
||||||
}
|
|
||||||
|
|
||||||
let root_key_history_id = insert_into(schema::root_key_history::table)
|
let root_key_history_id = insert_into(schema::root_key_history::table)
|
||||||
.values(&models::NewRootKeyHistory {
|
.values(&models::NewRootKeyHistory {
|
||||||
ciphertext: root_key_ciphertext.clone(),
|
ciphertext: root_key_ciphertext.clone(),
|
||||||
@@ -266,82 +225,28 @@ impl Vault {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Seal / unseal / bootstrap stuff. Will be separated into another actor, eventually
|
|
||||||
#[messages]
|
|
||||||
impl Vault {
|
|
||||||
#[message]
|
|
||||||
pub async fn start_bootstrap(&mut self, declared_operators: u64) -> Result<(), Error> {
|
|
||||||
if !matches!(&self.state, State::Unbootstrapped) {
|
|
||||||
return Err(Error::AlreadyBootstrapped);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.state = State::Bootstrapping {
|
|
||||||
declared_operators,
|
|
||||||
current_passphrases: HashMap::default(),
|
|
||||||
};
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[message]
|
#[message]
|
||||||
pub async fn contribute_bootstrap(
|
pub async fn try_unseal(&mut self, seal_key_raw: SafeCell<Vec<u8>>) -> Result<(), Error> {
|
||||||
&mut self,
|
|
||||||
operator: OperatorIdentityId,
|
|
||||||
key_raw: SafeCell<Vec<u8>>,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
let State::Bootstrapping {
|
|
||||||
current_passphrases,
|
|
||||||
declared_operators,
|
|
||||||
} = &mut self.state
|
|
||||||
else {
|
|
||||||
return Err(Error::AlreadyBootstrapped);
|
|
||||||
};
|
|
||||||
|
|
||||||
if current_passphrases.contains_key(&operator) {
|
|
||||||
return Err(Error::AlreadyBootstrapped);
|
|
||||||
}
|
|
||||||
current_passphrases.insert(operator, key_raw);
|
|
||||||
|
|
||||||
if current_passphrases.len() == declared_operators {
|
|
||||||
return self.finalize_bootstrap(seal_key_raw);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[message]
|
|
||||||
pub async fn contribute_unseal(
|
|
||||||
&mut self,
|
|
||||||
operator: OperatorId,
|
|
||||||
key_raw: SafeCell<Vec<u8>>,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
let State::Sealed {
|
let State::Sealed {
|
||||||
root_key_history_id,
|
root_key_history_id,
|
||||||
current_shares,
|
|
||||||
} = &self.state
|
} = &self.state
|
||||||
else {
|
else {
|
||||||
return Err(Error::NotBootstrapped);
|
return Err(Error::NotBootstrapped);
|
||||||
};
|
};
|
||||||
|
let root_key_history_id = *root_key_history_id;
|
||||||
|
|
||||||
// We don't want to hold connection while doing expensive KDF work
|
// We don't want to hold connection while doing expensive work
|
||||||
let current_key = {
|
let current_key = {
|
||||||
let mut conn = self.db.get().await?;
|
let mut conn = self.db.get().await?;
|
||||||
schema::root_key_history::table
|
schema::root_key_history::table
|
||||||
.filter(schema::root_key_history::id.eq(*root_key_history_id))
|
.filter(schema::root_key_history::id.eq(root_key_history_id))
|
||||||
.select(RootKeyHistory::as_select())
|
.select(RootKeyHistory::as_select())
|
||||||
.first(&mut conn)
|
.first(&mut conn)
|
||||||
.await?
|
.await?
|
||||||
};
|
};
|
||||||
|
|
||||||
let salt = ¤t_key.salt;
|
let mut seal_key = KeyCell::try_from(seal_key_raw).map_err(|()| Error::InvalidKey)?;
|
||||||
let salt = v1::Salt::try_from(salt.as_slice()).map_err(|_| {
|
|
||||||
error!("Broken database: invalid salt for root key");
|
|
||||||
Error::BrokenDatabase
|
|
||||||
})?;
|
|
||||||
let mut seal_key = derive_key(key_raw, &salt);
|
|
||||||
|
|
||||||
let mut root_key = SafeCell::new(current_key.ciphertext.clone());
|
|
||||||
|
|
||||||
let nonce =
|
let nonce =
|
||||||
Nonce::try_from(current_key.root_key_encryption_nonce.as_slice()).map_err(|()| {
|
Nonce::try_from(current_key.root_key_encryption_nonce.as_slice()).map_err(|()| {
|
||||||
@@ -349,19 +254,22 @@ impl Vault {
|
|||||||
Error::BrokenDatabase
|
Error::BrokenDatabase
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
let mut root_key_bytes = SafeCell::new(current_key.ciphertext.clone());
|
||||||
seal_key
|
seal_key
|
||||||
.decrypt_in_place(&nonce, v1::ROOT_KEY_TAG, &mut root_key)
|
.decrypt_in_place(&nonce, v1::ROOT_KEY_TAG, &mut root_key_bytes)
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
error!(?err, "Failed to unseal root key: invalid seal key");
|
error!(?err, "Failed to unseal root key: invalid seal key");
|
||||||
Error::InvalidKey
|
Error::InvalidKey
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
let root_key = KeyCell::try_from(root_key_bytes).map_err(|()| {
|
||||||
|
error!("Broken database: invalid encryption key size");
|
||||||
|
Error::BrokenDatabase
|
||||||
|
})?;
|
||||||
|
|
||||||
self.state = State::Unsealed(Unsealed {
|
self.state = State::Unsealed(Unsealed {
|
||||||
root_key_history_id: current_key.id,
|
root_key_history_id: current_key.id,
|
||||||
root_key: KeyCell::try_from(root_key).map_err(|err| {
|
root_key,
|
||||||
error!(?err, "Broken database: invalid encryption key size");
|
|
||||||
Error::BrokenDatabase
|
|
||||||
})?,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
info!("Vault unsealed successfully");
|
info!("Vault unsealed successfully");
|
||||||
@@ -379,7 +287,6 @@ impl Vault {
|
|||||||
|
|
||||||
self.state = State::Sealed {
|
self.state = State::Sealed {
|
||||||
root_key_history_id: *root_key_history_id,
|
root_key_history_id: *root_key_history_id,
|
||||||
current_shares: HashMap::new(),
|
|
||||||
};
|
};
|
||||||
let _ = self.events.tell(Publish(events::VaultResealed)).await;
|
let _ = self.events.tell(Publish(events::VaultResealed)).await;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -466,12 +373,10 @@ impl Vault {
|
|||||||
root_key_history_id,
|
root_key_history_id,
|
||||||
} = Self::expect_unsealed(&mut self.state)?;
|
} = Self::expect_unsealed(&mut self.state)?;
|
||||||
|
|
||||||
let mut hmac = root_key
|
let mut hmac = root_key.0.read_inline(|k| {
|
||||||
.0
|
HmacSha256::new_from_slice(k)
|
||||||
.read_inline(|k| match HmacSha256::new_from_slice(k) {
|
.unwrap_or_else(|_| unreachable!("HMAC accepts keys of any size"))
|
||||||
Ok(v) => v,
|
});
|
||||||
Err(_) => unreachable!("HMAC accepts keys of any size"),
|
|
||||||
});
|
|
||||||
hmac.update(&root_key_history_id.to_raw().to_be_bytes());
|
hmac.update(&root_key_history_id.to_raw().to_be_bytes());
|
||||||
hmac.update(&mac_input);
|
hmac.update(&mac_input);
|
||||||
|
|
||||||
@@ -495,12 +400,10 @@ impl Vault {
|
|||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut hmac = root_key
|
let mut hmac = root_key.0.read_inline(|k| {
|
||||||
.0
|
HmacSha256::new_from_slice(k)
|
||||||
.read_inline(|k| match HmacSha256::new_from_slice(k) {
|
.unwrap_or_else(|_| unreachable!("HMAC accepts keys of any size"))
|
||||||
Ok(v) => v,
|
});
|
||||||
Err(_) => unreachable!("HMAC accepts keys of any size"),
|
|
||||||
});
|
|
||||||
hmac.update(&key_version.to_raw().to_be_bytes());
|
hmac.update(&key_version.to_raw().to_be_bytes());
|
||||||
hmac.update(&mac_input);
|
hmac.update(&mac_input);
|
||||||
|
|
||||||
@@ -508,42 +411,6 @@ impl Vault {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// According to the spec, the quorum is 50% + 1
|
|
||||||
/// with exception for 1 and 2 operators, those require exactly the number of operators registered
|
|
||||||
fn shamir_threshold(comittee_size: u64) -> u64 {
|
|
||||||
if comittee_size == 2 || comittee_size == 1 {
|
|
||||||
return comittee_size;
|
|
||||||
}
|
|
||||||
|
|
||||||
let half_comittee = match comittee_size % 2 != 0 {
|
|
||||||
true => (comittee_size - 1) / 2,
|
|
||||||
false => comittee_size / 2,
|
|
||||||
};
|
|
||||||
|
|
||||||
half_comittee + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Beware: this function accepts raw key references (without memory protection)
|
|
||||||
fn generate_shamir_shares(threshold: u64, key: &[u8]) -> Vec<SafeCell<Vec<u8>>> {
|
|
||||||
use vsss_rs::{shamir, *};
|
|
||||||
|
|
||||||
type P256Share = DefaultShare<IdentifierPrimeField<Scalar>, IdentifierPrimeField<Scalar>>;
|
|
||||||
|
|
||||||
let mut osrng = rand_core::OsRng::default();
|
|
||||||
let sk = SecretKey::random(&mut osrng);
|
|
||||||
let nzs = sk.to_nonzero_scalar();
|
|
||||||
let shared_secret = IdentifierPrimeField(*nzs.as_ref());
|
|
||||||
let res = shamir::split_secret::<P256Share>(2, 3, &shared_secret, &mut osrng);
|
|
||||||
assert!(res.is_ok());
|
|
||||||
let shares = res.unwrap();
|
|
||||||
let res = shares.combine();
|
|
||||||
assert!(res.is_ok());
|
|
||||||
let scalar = res.unwrap();
|
|
||||||
let nzs_dup = NonZeroScalar::from_repr(scalar.0.to_repr()).unwrap();
|
|
||||||
let sk_dup = SecretKey::from(nzs_dup);
|
|
||||||
assert_eq!(sk_dup.to_bytes(), sk.to_bytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::actors::GlobalActors;
|
use crate::actors::GlobalActors;
|
||||||
@@ -555,8 +422,8 @@ mod tests {
|
|||||||
let mut actor = Vault::new(db.clone(), GlobalActors::spawn_message_bus())
|
let mut actor = Vault::new(db.clone(), GlobalActors::spawn_message_bus())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let seal_key = SafeCell::new(b"test-seal-key".to_vec());
|
let seal_key = SafeCell::new([0u8; 32].to_vec());
|
||||||
actor.finalize_bootstrap(seal_key).await.unwrap();
|
actor.bootstrap(seal_key).await.unwrap();
|
||||||
actor
|
actor
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -565,12 +432,12 @@ mod tests {
|
|||||||
async fn nonce_monotonic_even_when_nonce_allocation_interleaves() {
|
async fn nonce_monotonic_even_when_nonce_allocation_interleaves() {
|
||||||
let db = db::create_test_pool().await;
|
let db = db::create_test_pool().await;
|
||||||
let mut actor = bootstrapped_actor(&db).await;
|
let mut actor = bootstrapped_actor(&db).await;
|
||||||
let root_key_history_id = match actor.state {
|
let State::Unsealed(Unsealed {
|
||||||
State::Unsealed(Unsealed {
|
root_key_history_id,
|
||||||
root_key_history_id,
|
..
|
||||||
..
|
}) = actor.state
|
||||||
}) => root_key_history_id,
|
else {
|
||||||
_ => panic!("expected unsealed state"),
|
panic!("expected unsealed state");
|
||||||
};
|
};
|
||||||
|
|
||||||
let n1 = Vault::get_new_nonce(&db, root_key_history_id)
|
let n1 = Vault::get_new_nonce(&db, root_key_history_id)
|
||||||
|
|||||||
316
server/crates/arbiter-server/src/actors/vault_coordinator/mod.rs
Normal file
316
server/crates/arbiter-server/src/actors/vault_coordinator/mod.rs
Normal file
@@ -0,0 +1,316 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
|
||||||
|
use diesel::{ExpressionMethods as _, QueryDsl};
|
||||||
|
use diesel_async::RunQueryDsl;
|
||||||
|
use kameo::{Actor, actor::ActorRef, messages};
|
||||||
|
use rand_core::{OsRng, RngCore as _};
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
actors::vault::{Bootstrap, TryUnseal, Vault},
|
||||||
|
crypto::{derive_key, encryption::v1::Nonce, shamir},
|
||||||
|
db::{self, models, schema},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("Already coordinating a bootstrap")]
|
||||||
|
AlreadyBootstrapping,
|
||||||
|
#[error("Already coordinating an unseal")]
|
||||||
|
AlreadyUnsealing,
|
||||||
|
#[error("Bootstrap not in progress")]
|
||||||
|
NotBootstrapping,
|
||||||
|
#[error("Unseal not in progress")]
|
||||||
|
NotUnsealing,
|
||||||
|
#[error("Operator already contributed")]
|
||||||
|
DuplicateContribution,
|
||||||
|
#[error("Operator not found in database")]
|
||||||
|
OperatorNotFound,
|
||||||
|
#[error("Invalid passphrase (decryption failed)")]
|
||||||
|
InvalidPassphrase,
|
||||||
|
#[error("Shamir error: {0}")]
|
||||||
|
Shamir(String),
|
||||||
|
#[error("Database connection error: {0}")]
|
||||||
|
DatabaseConnection(#[from] db::PoolError),
|
||||||
|
#[error("Database query error: {0}")]
|
||||||
|
DatabaseQuery(#[from] diesel::result::Error),
|
||||||
|
#[error("Encryption error")]
|
||||||
|
Encryption,
|
||||||
|
#[error("Vault error")]
|
||||||
|
VaultError,
|
||||||
|
#[error("Broken database")]
|
||||||
|
BrokenDatabase,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Passphrases stored as plain Vec<u8> (not SafeCell) so CoordinatorState is Sync.
|
||||||
|
// They are ephemeral and dropped immediately after use.
|
||||||
|
enum CoordinatorState {
|
||||||
|
Idle,
|
||||||
|
Bootstrapping {
|
||||||
|
declared_count: usize,
|
||||||
|
passphrases: HashMap<i32, Vec<u8>>,
|
||||||
|
},
|
||||||
|
Unsealing {
|
||||||
|
threshold: usize,
|
||||||
|
passphrases: HashMap<i32, Vec<u8>>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Actor)]
|
||||||
|
pub struct VaultCoordinator {
|
||||||
|
db: db::DatabasePool,
|
||||||
|
vault: ActorRef<Vault>,
|
||||||
|
state: CoordinatorState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VaultCoordinator {
|
||||||
|
pub const fn new(db: db::DatabasePool, vault: ActorRef<Vault>) -> Self {
|
||||||
|
Self {
|
||||||
|
db,
|
||||||
|
vault,
|
||||||
|
state: CoordinatorState::Idle,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const SHARE_AAD: &[u8] = b"arbiter/shamir-share/v1";
|
||||||
|
|
||||||
|
const fn shamir_threshold(n: usize) -> usize {
|
||||||
|
match n {
|
||||||
|
0 => panic!("No operators"),
|
||||||
|
1 => 1,
|
||||||
|
2 => 2,
|
||||||
|
n => n / 2 + 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn finalize_bootstrap(
|
||||||
|
db: db::DatabasePool,
|
||||||
|
vault: ActorRef<Vault>,
|
||||||
|
passphrases: HashMap<i32, Vec<u8>>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let total = passphrases.len();
|
||||||
|
let threshold = shamir_threshold(total);
|
||||||
|
|
||||||
|
// Generate random 32-byte seal key
|
||||||
|
let mut seal_key_bytes = vec![0u8; 32];
|
||||||
|
OsRng.fill_bytes(&mut seal_key_bytes);
|
||||||
|
|
||||||
|
// Split seal key into shares using Shamir (OsRng from rand_core 0.6, compatible with vsss-rs)
|
||||||
|
let shares = shamir::split_key(threshold, total, &seal_key_bytes, OsRng)
|
||||||
|
.map_err(|e| Error::Shamir(e.to_string()))?;
|
||||||
|
|
||||||
|
let seal_key = SafeCell::new(seal_key_bytes);
|
||||||
|
|
||||||
|
let mut conn = db.get().await?;
|
||||||
|
|
||||||
|
for ((operator_id_raw, passphrase_bytes), share) in passphrases.into_iter().zip(shares) {
|
||||||
|
// Generate a fresh share_salt for this operator
|
||||||
|
let mut share_salt = vec![0u8; 32];
|
||||||
|
OsRng.fill_bytes(&mut share_salt);
|
||||||
|
|
||||||
|
// Derive share encryption key from passphrase + salt
|
||||||
|
let mut passphrase_cell = SafeCell::new(passphrase_bytes);
|
||||||
|
let mut share_seal_key = derive_key(&mut passphrase_cell, &share_salt);
|
||||||
|
|
||||||
|
// Encrypt this operator's share
|
||||||
|
let nonce = Nonce::default();
|
||||||
|
let encrypted_share = share_seal_key
|
||||||
|
.encrypt(&nonce, SHARE_AAD, &share)
|
||||||
|
.map_err(|_| Error::Encryption)?;
|
||||||
|
|
||||||
|
let nonce_bytes = nonce.to_vec();
|
||||||
|
|
||||||
|
diesel::replace_into(schema::operator::table)
|
||||||
|
.values((
|
||||||
|
schema::operator::id.eq(Some(operator_id_raw)),
|
||||||
|
schema::operator::share.eq(&encrypted_share),
|
||||||
|
schema::operator::share_nonce.eq(&nonce_bytes),
|
||||||
|
schema::operator::share_salt.eq(&share_salt),
|
||||||
|
schema::operator::created_at.eq(models::SqliteTimestamp::now()),
|
||||||
|
schema::operator::updated_at.eq(models::SqliteTimestamp::now()),
|
||||||
|
))
|
||||||
|
.execute(&mut conn)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
vault
|
||||||
|
.ask(Bootstrap {
|
||||||
|
seal_key_raw: seal_key,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|err| {
|
||||||
|
error!(?err, "Vault bootstrap failed");
|
||||||
|
Error::VaultError
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn finalize_unseal(
|
||||||
|
db: db::DatabasePool,
|
||||||
|
vault: ActorRef<Vault>,
|
||||||
|
passphrases: HashMap<i32, Vec<u8>>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let mut conn = db.get().await?;
|
||||||
|
let mut shares: Vec<Vec<u8>> = Vec::new();
|
||||||
|
|
||||||
|
for (operator_id_raw, passphrase_bytes) in passphrases {
|
||||||
|
let (encrypted_share, share_nonce_bytes, share_salt): (Vec<u8>, Vec<u8>, Vec<u8>) =
|
||||||
|
schema::operator::table
|
||||||
|
.filter(schema::operator::id.eq(Some(operator_id_raw)))
|
||||||
|
.select((
|
||||||
|
schema::operator::share,
|
||||||
|
schema::operator::share_nonce,
|
||||||
|
schema::operator::share_salt,
|
||||||
|
))
|
||||||
|
.first(&mut conn)
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::OperatorNotFound)?;
|
||||||
|
|
||||||
|
let nonce = Nonce::try_from(share_nonce_bytes.as_slice()).map_err(|()| {
|
||||||
|
error!(operator_id = operator_id_raw, "Invalid nonce in DB");
|
||||||
|
Error::BrokenDatabase
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut passphrase_cell = SafeCell::new(passphrase_bytes);
|
||||||
|
let mut share_seal_key = derive_key(&mut passphrase_cell, &share_salt);
|
||||||
|
|
||||||
|
let mut share_buffer = SafeCell::new(encrypted_share);
|
||||||
|
share_seal_key
|
||||||
|
.decrypt_in_place(&nonce, SHARE_AAD, &mut share_buffer)
|
||||||
|
.map_err(|_| Error::InvalidPassphrase)?;
|
||||||
|
|
||||||
|
let decrypted_share = share_buffer.read().clone();
|
||||||
|
shares.push(decrypted_share);
|
||||||
|
}
|
||||||
|
|
||||||
|
let seal_key_bytes =
|
||||||
|
shamir::combine_shares(&shares).map_err(|e| Error::Shamir(e.to_string()))?;
|
||||||
|
|
||||||
|
let seal_key = SafeCell::new(seal_key_bytes);
|
||||||
|
|
||||||
|
vault
|
||||||
|
.ask(TryUnseal {
|
||||||
|
seal_key_raw: seal_key,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|err| {
|
||||||
|
error!(?err, "Vault unseal failed");
|
||||||
|
Error::VaultError
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[messages]
|
||||||
|
impl VaultCoordinator {
|
||||||
|
/// Phase 1 of multi-operator bootstrap: declare the committee size.
|
||||||
|
#[message]
|
||||||
|
#[expect(clippy::unused_async, reason = "kameo requires messages to be async")]
|
||||||
|
pub async fn start_bootstrap(
|
||||||
|
&mut self,
|
||||||
|
operator_id: i32,
|
||||||
|
declared_count: usize,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let _ = operator_id;
|
||||||
|
if !matches!(self.state, CoordinatorState::Idle) {
|
||||||
|
return Err(Error::AlreadyBootstrapping);
|
||||||
|
}
|
||||||
|
self.state = CoordinatorState::Bootstrapping {
|
||||||
|
declared_count,
|
||||||
|
passphrases: HashMap::new(),
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Phase 2 of multi-operator bootstrap: contribute a passphrase.
|
||||||
|
/// Returns Ok(true) when all operators contributed and bootstrap finalized.
|
||||||
|
#[message]
|
||||||
|
pub async fn contribute_bootstrap(
|
||||||
|
&mut self,
|
||||||
|
operator_id: i32,
|
||||||
|
mut passphrase: SafeCell<Vec<u8>>,
|
||||||
|
) -> Result<bool, Error> {
|
||||||
|
let CoordinatorState::Bootstrapping {
|
||||||
|
declared_count,
|
||||||
|
passphrases,
|
||||||
|
} = &mut self.state
|
||||||
|
else {
|
||||||
|
return Err(Error::NotBootstrapping);
|
||||||
|
};
|
||||||
|
|
||||||
|
if passphrases.contains_key(&operator_id) {
|
||||||
|
return Err(Error::DuplicateContribution);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract bytes immediately so state stays Sync
|
||||||
|
let passphrase_bytes = passphrase.read().to_vec();
|
||||||
|
passphrases.insert(operator_id, passphrase_bytes);
|
||||||
|
|
||||||
|
if passphrases.len() < *declared_count {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
let CoordinatorState::Bootstrapping { passphrases, .. } =
|
||||||
|
std::mem::replace(&mut self.state, CoordinatorState::Idle)
|
||||||
|
else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
|
||||||
|
finalize_bootstrap(self.db.clone(), self.vault.clone(), passphrases).await?;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Contribute a passphrase for vault unseal.
|
||||||
|
/// Returns Ok(true) when threshold reached and vault is unsealed.
|
||||||
|
#[message]
|
||||||
|
pub async fn contribute_unseal(
|
||||||
|
&mut self,
|
||||||
|
operator_id: i32,
|
||||||
|
mut passphrase: SafeCell<Vec<u8>>,
|
||||||
|
) -> Result<bool, Error> {
|
||||||
|
if matches!(self.state, CoordinatorState::Idle) {
|
||||||
|
let mut conn = self.db.get().await?;
|
||||||
|
let count: i64 = schema::operator::table
|
||||||
|
.count()
|
||||||
|
.get_result(&mut conn)
|
||||||
|
.await?;
|
||||||
|
let threshold = shamir_threshold(usize::try_from(count).unwrap_or_default());
|
||||||
|
|
||||||
|
self.state = CoordinatorState::Unsealing {
|
||||||
|
threshold,
|
||||||
|
passphrases: HashMap::new(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let CoordinatorState::Unsealing {
|
||||||
|
threshold,
|
||||||
|
passphrases,
|
||||||
|
} = &mut self.state
|
||||||
|
else {
|
||||||
|
return Err(Error::NotUnsealing);
|
||||||
|
};
|
||||||
|
|
||||||
|
if passphrases.contains_key(&operator_id) {
|
||||||
|
return Err(Error::DuplicateContribution);
|
||||||
|
}
|
||||||
|
|
||||||
|
let passphrase_bytes = passphrase.read().to_vec();
|
||||||
|
passphrases.insert(operator_id, passphrase_bytes);
|
||||||
|
|
||||||
|
if passphrases.len() < *threshold {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
let CoordinatorState::Unsealing { passphrases, .. } =
|
||||||
|
std::mem::replace(&mut self.state, CoordinatorState::Idle)
|
||||||
|
else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
|
||||||
|
finalize_unseal(self.db.clone(), self.vault.clone(), passphrases).await?;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -61,12 +61,12 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn derive_seal_key_deterministic() {
|
fn derive_seal_key_deterministic() {
|
||||||
static PASSWORD: &[u8] = b"password";
|
static PASSWORD: &[u8] = b"password";
|
||||||
let password = SafeCell::new(PASSWORD.to_vec());
|
let mut password = SafeCell::new(PASSWORD.to_vec());
|
||||||
let password2 = SafeCell::new(PASSWORD.to_vec());
|
let mut password2 = SafeCell::new(PASSWORD.to_vec());
|
||||||
let salt = generate_salt();
|
let salt = generate_salt();
|
||||||
|
|
||||||
let mut key1 = derive_key(password, &salt);
|
let mut key1 = derive_key(&mut password, &salt);
|
||||||
let mut key2 = derive_key(password2, &salt);
|
let mut key2 = derive_key(&mut password2, &salt);
|
||||||
|
|
||||||
let key1_reader = key1.0.read();
|
let key1_reader = key1.0.read();
|
||||||
let key2_reader = key2.0.read();
|
let key2_reader = key2.0.read();
|
||||||
@@ -77,10 +77,10 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn successful_derive() {
|
fn successful_derive() {
|
||||||
static PASSWORD: &[u8] = b"password";
|
static PASSWORD: &[u8] = b"password";
|
||||||
let password = SafeCell::new(PASSWORD.to_vec());
|
let mut password = SafeCell::new(PASSWORD.to_vec());
|
||||||
let salt = generate_salt();
|
let salt = generate_salt();
|
||||||
|
|
||||||
let mut key = derive_key(password, &salt);
|
let mut key = derive_key(&mut password, &salt);
|
||||||
let key_reader = key.0.read();
|
let key_reader = key.0.read();
|
||||||
|
|
||||||
assert_ne!(key_reader.as_slice(), &[0u8; 32][..]);
|
assert_ne!(key_reader.as_slice(), &[0u8; 32][..]);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
|
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
|
||||||
use encryption::v1::{Nonce, Salt};
|
use encryption::v1::Nonce;
|
||||||
|
|
||||||
use argon2::{Algorithm, Argon2};
|
use argon2::{Algorithm, Argon2};
|
||||||
use chacha20poly1305::{
|
use chacha20poly1305::{
|
||||||
@@ -13,6 +13,7 @@ use rand::{
|
|||||||
|
|
||||||
pub mod encryption;
|
pub mod encryption;
|
||||||
pub mod integrity;
|
pub mod integrity;
|
||||||
|
pub mod shamir;
|
||||||
|
|
||||||
pub struct KeyCell(pub SafeCell<Key>);
|
pub struct KeyCell(pub SafeCell<Key>);
|
||||||
impl From<SafeCell<Key>> for KeyCell {
|
impl From<SafeCell<Key>> for KeyCell {
|
||||||
@@ -94,7 +95,7 @@ impl KeyCell {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Derive a fixed-length key from the password using Argon2id, which is designed for password hashing and key derivation.
|
/// Derive a fixed-length key from the password using Argon2id, which is designed for password hashing and key derivation.
|
||||||
pub fn derive_key(password: &mut SafeCell<Vec<u8>>, salt: &Salt) -> KeyCell {
|
pub fn derive_key(password: &mut SafeCell<Vec<u8>>, salt: &[u8]) -> KeyCell {
|
||||||
let params = {
|
let params = {
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
{
|
{
|
||||||
@@ -132,10 +133,10 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn encrypt_decrypt() {
|
fn encrypt_decrypt() {
|
||||||
static PASSWORD: &[u8] = b"password";
|
static PASSWORD: &[u8] = b"password";
|
||||||
let password = SafeCell::new(PASSWORD.to_vec());
|
let mut password = SafeCell::new(PASSWORD.to_vec());
|
||||||
let salt = generate_salt();
|
let salt = generate_salt();
|
||||||
|
|
||||||
let mut key = derive_key(password, &salt);
|
let mut key = derive_key(&mut password, &salt);
|
||||||
let nonce = Nonce(*b"unique nonce 123 1231233"); // 24 bytes for XChaCha20Poly1305
|
let nonce = Nonce(*b"unique nonce 123 1231233"); // 24 bytes for XChaCha20Poly1305
|
||||||
let associated_data = b"associated data";
|
let associated_data = b"associated data";
|
||||||
let mut buffer = b"secret data".to_vec();
|
let mut buffer = b"secret data".to_vec();
|
||||||
|
|||||||
27
server/crates/arbiter-server/src/crypto/shamir.rs
Normal file
27
server/crates/arbiter-server/src/crypto/shamir.rs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
use vsss_rs::Gf256;
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum ShamirError {
|
||||||
|
#[error("Failed to split key: {0}")]
|
||||||
|
Split(String),
|
||||||
|
#[error("Failed to combine shares: {0}")]
|
||||||
|
Combine(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Split `key` into `total` shares where any `threshold` shares can reconstruct it.
|
||||||
|
/// Each returned Vec<u8> is a share with format [`identifier_byte`, `value_bytes`...].
|
||||||
|
pub fn split_key(
|
||||||
|
threshold: usize,
|
||||||
|
total: usize,
|
||||||
|
key: &[u8],
|
||||||
|
rng: impl rand_core::RngCore + rand_core::CryptoRng,
|
||||||
|
) -> Result<Vec<Vec<u8>>, ShamirError> {
|
||||||
|
Gf256::split_array(threshold, total, key, rng)
|
||||||
|
.map_err(|e| ShamirError::Split(format!("{e:?}")))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reconstruct the secret from `threshold` or more shares.
|
||||||
|
pub fn combine_shares(shares: &[Vec<u8>]) -> Result<Vec<u8>, ShamirError> {
|
||||||
|
Gf256::combine_array(shares)
|
||||||
|
.map_err(|e| ShamirError::Combine(format!("{e:?}")))
|
||||||
|
}
|
||||||
@@ -285,6 +285,7 @@ pub struct Operator {
|
|||||||
pub id: OperatorId,
|
pub id: OperatorId,
|
||||||
pub share: Vec<u8>,
|
pub share: Vec<u8>,
|
||||||
pub share_nonce: Vec<u8>,
|
pub share_nonce: Vec<u8>,
|
||||||
|
pub share_salt: Vec<u8>,
|
||||||
pub created_at: SqliteTimestamp,
|
pub created_at: SqliteTimestamp,
|
||||||
pub updated_at: SqliteTimestamp,
|
pub updated_at: SqliteTimestamp,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -157,6 +157,7 @@ diesel::table! {
|
|||||||
id -> Nullable<Integer>,
|
id -> Nullable<Integer>,
|
||||||
share -> Binary,
|
share -> Binary,
|
||||||
share_nonce -> Binary,
|
share_nonce -> Binary,
|
||||||
|
share_salt -> Binary,
|
||||||
created_at -> Integer,
|
created_at -> Integer,
|
||||||
updated_at -> Integer,
|
updated_at -> Integer,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ pub(super) async fn dispatch(
|
|||||||
VaultRequestPayload::QueryState(()) => {
|
VaultRequestPayload::QueryState(()) => {
|
||||||
let state = match actor.ask(HandleQueryVaultState {}).await {
|
let state = match actor.ask(HandleQueryVaultState {}).await {
|
||||||
Ok(VaultState::Unbootstrapped) => ProtoVaultState::Unbootstrapped,
|
Ok(VaultState::Unbootstrapped) => ProtoVaultState::Unbootstrapped,
|
||||||
Ok(VaultState::Bootstrapping) => ProtoVaultState::Boostrapping,
|
|
||||||
Ok(VaultState::Sealed) => ProtoVaultState::Sealed,
|
Ok(VaultState::Sealed) => ProtoVaultState::Sealed,
|
||||||
Ok(VaultState::Unsealed) => ProtoVaultState::Unsealed,
|
Ok(VaultState::Unsealed) => ProtoVaultState::Unsealed,
|
||||||
Err(SendError::HandlerError(Error::Internal)) => ProtoVaultState::Error,
|
Err(SendError::HandlerError(Error::Internal)) => ProtoVaultState::Error,
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ async fn handle_query_vault_state(
|
|||||||
let state = match actor.ask(HandleQueryVaultState {}).await {
|
let state = match actor.ask(HandleQueryVaultState {}).await {
|
||||||
Ok(VaultState::Unbootstrapped) => ProtoVaultState::Unbootstrapped,
|
Ok(VaultState::Unbootstrapped) => ProtoVaultState::Unbootstrapped,
|
||||||
Ok(VaultState::Sealed) => ProtoVaultState::Sealed,
|
Ok(VaultState::Sealed) => ProtoVaultState::Sealed,
|
||||||
Ok(VaultState::Bootstrapping) => ProtoVaultState::Boostrapping,
|
|
||||||
Ok(VaultState::Unsealed) => ProtoVaultState::Unsealed,
|
Ok(VaultState::Unsealed) => ProtoVaultState::Unsealed,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
warn!(error = ?err, "Failed to query vault state");
|
warn!(error = ?err, "Failed to query vault state");
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
grpc::{Convert, TryConvert},
|
grpc::{Convert, TryConvert},
|
||||||
peers::operator::vault_gate::{
|
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::{
|
use arbiter_proto::proto::operator::{
|
||||||
operator_request::Payload as OperatorRequestPayload,
|
operator_request::Payload as OperatorRequestPayload,
|
||||||
vault::{
|
vault::{
|
||||||
self as proto_vault,
|
self as proto_vault,
|
||||||
bootstrap::{self as proto_bootstrap},
|
bootstrap::{self as proto_bootstrap, request::Payload as BootstrapRequestPayload},
|
||||||
request::Payload as VaultRequestPayload,
|
request::Payload as VaultRequestPayload,
|
||||||
unseal::{self as proto_unseal, request::Payload as UnsealRequestPayload},
|
unseal::{self as proto_unseal, request::Payload as UnsealRequestPayload},
|
||||||
},
|
},
|
||||||
@@ -73,6 +75,13 @@ impl TryConvert for UnsealRequestPayload {
|
|||||||
match self {
|
match self {
|
||||||
Self::Start(start) => start.try_convert(),
|
Self::Start(start) => start.try_convert(),
|
||||||
Self::EncryptedKey(key) => Ok(key.convert()),
|
Self::EncryptedKey(key) => Ok(key.convert()),
|
||||||
|
Self::ContributePassphrase(cp) => Ok(
|
||||||
|
vault_gate::Inbound::HandleContributeUnsealPassphrase(
|
||||||
|
HandleContributeUnsealPassphrase {
|
||||||
|
passphrase: cp.passphrase,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,12 +116,35 @@ impl TryConvert for proto_bootstrap::Request {
|
|||||||
type Error = Status;
|
type Error = Status;
|
||||||
|
|
||||||
fn try_convert(self) -> Result<vault_gate::Inbound, Status> {
|
fn try_convert(self) -> Result<vault_gate::Inbound, Status> {
|
||||||
self.encrypted_key
|
self.payload
|
||||||
.ok_or_else(|| Status::invalid_argument("Missing bootstrap encrypted key"))?
|
.ok_or_else(|| Status::invalid_argument("Missing bootstrap payload"))?
|
||||||
.try_convert()
|
.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) => Ok(
|
||||||
|
vault_gate::Inbound::HandleDeclareCommittee(HandleDeclareCommittee {
|
||||||
|
count: dc.count as usize,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
Self::ContributePassphrase(cp) => Ok(
|
||||||
|
vault_gate::Inbound::HandleContributeBootstrapPassphrase(
|
||||||
|
HandleContributeBootstrapPassphrase {
|
||||||
|
passphrase: cp.passphrase,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl TryConvert for proto_bootstrap::BootstrapEncryptedKey {
|
impl TryConvert for proto_bootstrap::BootstrapEncryptedKey {
|
||||||
type Output = vault_gate::Inbound;
|
type Output = vault_gate::Inbound;
|
||||||
type Error = Status;
|
type Error = Status;
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ impl Convert for VaultState {
|
|||||||
fn convert(self) -> OperatorResponsePayload {
|
fn convert(self) -> OperatorResponsePayload {
|
||||||
let proto_state = match self {
|
let proto_state = match self {
|
||||||
Self::Unbootstrapped => ProtoVaultState::Unbootstrapped,
|
Self::Unbootstrapped => ProtoVaultState::Unbootstrapped,
|
||||||
Self::Bootstrapping => ProtoVaultState::Boostrapping,
|
|
||||||
Self::Sealed => ProtoVaultState::Sealed,
|
Self::Sealed => ProtoVaultState::Sealed,
|
||||||
Self::Unsealed => ProtoVaultState::Unsealed,
|
Self::Unsealed => ProtoVaultState::Unsealed,
|
||||||
};
|
};
|
||||||
@@ -111,6 +110,40 @@ impl TryConvert for vault_gate::Outbound {
|
|||||||
};
|
};
|
||||||
Ok(wrap_bootstrap_response(proto_result))
|
Ok(wrap_bootstrap_response(proto_result))
|
||||||
}
|
}
|
||||||
|
Self::HandleDeclareCommittee(result) => {
|
||||||
|
let proto_result = match result {
|
||||||
|
Ok(()) => ProtoBootstrapResult::Success,
|
||||||
|
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(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(),
|
||||||
|
)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -298,7 +298,7 @@ where
|
|||||||
|
|
||||||
let signature = expect_message(transport, |req: Inbound| match req {
|
let signature = expect_message(transport, |req: Inbound| match req {
|
||||||
Inbound::AuthChallengeSolution { signature } => Some(signature),
|
Inbound::AuthChallengeSolution { signature } => Some(signature),
|
||||||
_ => None,
|
Inbound::AuthChallengeRequest { .. } => None,
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
|
|||||||
@@ -14,19 +14,19 @@ use diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl};
|
|||||||
use diesel_async::RunQueryDsl;
|
use diesel_async::RunQueryDsl;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
pub(super) struct ChallengeRequest {
|
pub(crate) struct ChallengeRequest {
|
||||||
pub(super) pubkey: authn::PublicKey,
|
pub(crate) pubkey: authn::PublicKey,
|
||||||
pub(super) bootstrap_token: Option<String>,
|
pub(crate) bootstrap_token: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) struct ChallengeContext {
|
pub struct ChallengeContext {
|
||||||
pub(super) challenge: AuthChallenge,
|
pub challenge: AuthChallenge,
|
||||||
pub(super) pubkey: authn::PublicKey,
|
pub pubkey: authn::PublicKey,
|
||||||
pub(super) bootstrap_token: Option<String>,
|
pub bootstrap_token: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) struct ChallengeSolution {
|
pub(crate) struct ChallengeSolution {
|
||||||
pub(super) solution: Vec<u8>,
|
pub(crate) solution: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
smlang::statemachine!(
|
smlang::statemachine!(
|
||||||
@@ -127,8 +127,6 @@ where
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(missing_docs)]
|
|
||||||
#[allow(clippy::unused_unit)]
|
|
||||||
async fn verify_solution(
|
async fn verify_solution(
|
||||||
&mut self,
|
&mut self,
|
||||||
ChallengeContext {
|
ChallengeContext {
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ use super::{Error, OperatorSession};
|
|||||||
use crate::{
|
use crate::{
|
||||||
actors::{
|
actors::{
|
||||||
evm::{
|
evm::{
|
||||||
ClientSignTransaction, Generate, ListWallets, OperatorCreateGrant, OperatorListGrants,
|
ClientSignTransaction, Generate, ListWallets, OperatorCreateGrant, OperatorDeleteGrant,
|
||||||
SignTransactionError as EvmSignError,
|
OperatorListGrants, SignTransactionError as EvmSignError,
|
||||||
},
|
},
|
||||||
flow_coordinator::client_connect_approval::ClientApprovalAnswer,
|
flow_coordinator::client_connect_approval::ClientApprovalAnswer,
|
||||||
vault::VaultState,
|
vault::VaultState,
|
||||||
@@ -122,22 +122,23 @@ impl OperatorSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[message]
|
#[message]
|
||||||
pub(crate) fn handle_grant_delete(&mut self, grant_id: i32) -> Result<(), GrantMutationError> {
|
pub(crate) async fn handle_grant_delete(
|
||||||
// match self
|
&mut self,
|
||||||
// .props
|
grant_id: i32,
|
||||||
// .actors
|
) -> Result<(), GrantMutationError> {
|
||||||
// .evm
|
match self
|
||||||
// .ask(OperatorDeleteGrant { grant_id })
|
.props
|
||||||
// .await
|
.actors
|
||||||
// {
|
.evm
|
||||||
// Ok(()) => Ok(()),
|
.ask(OperatorDeleteGrant { grant_id })
|
||||||
// Err(err) => {
|
.await
|
||||||
// error!(?err, "EVM grant delete failed");
|
{
|
||||||
// Err(GrantMutationError::Internal)
|
Ok(()) => Ok(()),
|
||||||
// }
|
Err(err) => {
|
||||||
// }
|
error!(?err, "EVM grant delete failed");
|
||||||
let _ = grant_id;
|
Err(GrantMutationError::Internal)
|
||||||
todo!()
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[message]
|
#[message]
|
||||||
@@ -217,8 +218,8 @@ impl OperatorSession {
|
|||||||
pub(crate) async fn handle_list_wallet_access(
|
pub(crate) async fn handle_list_wallet_access(
|
||||||
&mut self,
|
&mut self,
|
||||||
) -> Result<Vec<EvmWalletAccess>, Error> {
|
) -> Result<Vec<EvmWalletAccess>, Error> {
|
||||||
let mut conn = self.props.db.get().await?;
|
|
||||||
use crate::db::schema::evm_wallet_access;
|
use crate::db::schema::evm_wallet_access;
|
||||||
|
let mut conn = self.props.db.get().await?;
|
||||||
let access_entries = evm_wallet_access::table
|
let access_entries = evm_wallet_access::table
|
||||||
.select(EvmWalletAccess::as_select())
|
.select(EvmWalletAccess::as_select())
|
||||||
.load::<_>(&mut conn)
|
.load::<_>(&mut conn)
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ impl OperatorSession {
|
|||||||
Self {
|
Self {
|
||||||
props,
|
props,
|
||||||
sender,
|
sender,
|
||||||
pending_client_approvals: Default::default(),
|
pending_client_approvals: HashMap::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ 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},
|
||||||
},
|
},
|
||||||
crypto::integrity::{self},
|
crypto::integrity::{self},
|
||||||
db::DatabasePool,
|
db::DatabasePool,
|
||||||
@@ -17,6 +18,9 @@ use tokio::sync::oneshot;
|
|||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
use x25519_dalek::{EphemeralSecret, PublicKey, SharedSecret};
|
use x25519_dalek::{EphemeralSecret, PublicKey, SharedSecret};
|
||||||
|
|
||||||
|
pub use VaultGateMessage as Inbound;
|
||||||
|
pub use VaultGateMessageReply as Outbound;
|
||||||
|
|
||||||
pub mod state;
|
pub mod state;
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
@@ -118,8 +122,7 @@ impl VaultGate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#[messages(enum)]
|
||||||
#[messages(messages = Inbound, replies = Outbound)]
|
|
||||||
impl VaultGate {
|
impl VaultGate {
|
||||||
#[message]
|
#[message]
|
||||||
pub fn handle_handshake(
|
pub fn handle_handshake(
|
||||||
@@ -234,6 +237,52 @@ impl VaultGate {
|
|||||||
|
|
||||||
Ok(answer)
|
Ok(answer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[message]
|
||||||
|
pub async fn handle_declare_committee(&mut self, count: usize) -> Result<(), Error> {
|
||||||
|
self.actors
|
||||||
|
.vault_coordinator
|
||||||
|
.ask(StartBootstrap {
|
||||||
|
operator_id: self.auth_creds.id,
|
||||||
|
declared_count: count,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::internal("VaultCoordinator unavailable"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[message]
|
||||||
|
pub async fn handle_contribute_bootstrap_passphrase(
|
||||||
|
&mut self,
|
||||||
|
passphrase: Vec<u8>,
|
||||||
|
) -> Result<bool, Error> {
|
||||||
|
use arbiter_crypto::safecell::SafeCell;
|
||||||
|
let passphrase_cell = SafeCell::new(passphrase);
|
||||||
|
self.actors
|
||||||
|
.vault_coordinator
|
||||||
|
.ask(ContributeBootstrap {
|
||||||
|
operator_id: self.auth_creds.id,
|
||||||
|
passphrase: passphrase_cell,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::internal("VaultCoordinator unavailable"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[message]
|
||||||
|
pub async fn handle_contribute_unseal_passphrase(
|
||||||
|
&mut self,
|
||||||
|
passphrase: Vec<u8>,
|
||||||
|
) -> Result<bool, Error> {
|
||||||
|
use arbiter_crypto::safecell::SafeCell;
|
||||||
|
let passphrase_cell = SafeCell::new(passphrase);
|
||||||
|
self.actors
|
||||||
|
.vault_coordinator
|
||||||
|
.ask(ContributeUnseal {
|
||||||
|
operator_id: self.auth_creds.id,
|
||||||
|
passphrase: passphrase_cell,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::internal("VaultCoordinator unavailable"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Message<events::Bootstrapped> for VaultGate {
|
impl Message<events::Bootstrapped> for VaultGate {
|
||||||
|
|||||||
Reference in New Issue
Block a user