WIP: some things
This commit is contained in:
@@ -22,7 +22,7 @@ pub trait SafeCellHandle<T> {
|
||||
fn read(&mut self) -> Self::CellRead<'_>;
|
||||
fn write(&mut self) -> Self::CellWrite<'_>;
|
||||
|
||||
fn new_inline<F>(f: F) -> Self
|
||||
fn new_inline_default<F>(f: F) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
T: Default,
|
||||
@@ -36,6 +36,14 @@ pub trait SafeCellHandle<T> {
|
||||
cell
|
||||
}
|
||||
|
||||
fn new_inline<F>(f: Box<F>) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
F: for<'a> FnOnce() -> T,
|
||||
{
|
||||
Self::new(f())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn read_inline<F, R>(&mut self, f: F) -> R
|
||||
where
|
||||
|
||||
@@ -50,6 +50,7 @@ subtle = "2.6.1"
|
||||
x25519-dalek.workspace = true
|
||||
k256.workspace = true
|
||||
kameo_actors.workspace = true
|
||||
vsss-rs = "5.4.0"
|
||||
|
||||
[dev-dependencies]
|
||||
proptest = "1.11.0"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{
|
||||
crypto::{
|
||||
KeyCell, derive_key,
|
||||
@@ -6,7 +8,7 @@ use crate::{
|
||||
},
|
||||
db::{
|
||||
self,
|
||||
models::{self, RootKeyHistory, RootKeyHistoryId},
|
||||
models::{self, OperatorId, OperatorIdentityId, RootKeyHistory, RootKeyHistoryId},
|
||||
schema::{self},
|
||||
},
|
||||
};
|
||||
@@ -15,10 +17,11 @@ use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
|
||||
use chrono::Utc;
|
||||
use diesel::{
|
||||
ExpressionMethods as _, OptionalExtension, QueryDsl, SelectableHelper,
|
||||
dsl::{insert_into, update},
|
||||
dsl::{count, insert_into, update},
|
||||
select,
|
||||
};
|
||||
use diesel_async::{AsyncConnection, RunQueryDsl};
|
||||
use hmac::{KeyInit as _, Mac as _};
|
||||
use hmac::{KeyInit as _, Mac as _, digest::common};
|
||||
use kameo::{Actor, Reply, actor::ActorRef, messages};
|
||||
use kameo_actors::message_bus::{MessageBus, Publish};
|
||||
use strum::{EnumDiscriminants, IntoDiscriminant};
|
||||
@@ -62,6 +65,15 @@ pub enum Error {
|
||||
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 {
|
||||
root_key_history_id: RootKeyHistoryId,
|
||||
root_key: KeyCell,
|
||||
@@ -73,8 +85,15 @@ enum State {
|
||||
#[default]
|
||||
Unbootstrapped,
|
||||
|
||||
Bootstrapping {
|
||||
declared_operators: u64,
|
||||
current_passphrases: HashMap<OperatorIdentityId, SafeCell<Vec<u8>>>,
|
||||
},
|
||||
|
||||
Sealed {
|
||||
threshold: u64, // basically, quorum size
|
||||
root_key_history_id: RootKeyHistoryId,
|
||||
current_shares: HashMap<OperatorId, SafeCell<Vec<u8>>>,
|
||||
},
|
||||
Unsealed(Unsealed),
|
||||
}
|
||||
@@ -90,7 +109,6 @@ pub struct Vault {
|
||||
events: ActorRef<MessageBus>,
|
||||
}
|
||||
|
||||
#[messages]
|
||||
impl Vault {
|
||||
pub async fn new(db: db::DatabasePool, events: ActorRef<MessageBus>) -> Result<Self, Error> {
|
||||
let state = {
|
||||
@@ -103,9 +121,17 @@ impl Vault {
|
||||
.await?;
|
||||
|
||||
match root_key_history {
|
||||
Some(root_key_history) => State::Sealed {
|
||||
root_key_history_id: root_key_history.id,
|
||||
},
|
||||
Some(root_key_history) => {
|
||||
let operator_count: i64 = schema::operator::table
|
||||
.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,
|
||||
}
|
||||
};
|
||||
@@ -154,19 +180,28 @@ impl Vault {
|
||||
const fn expect_unsealed(state: &mut State) -> Result<&mut Unsealed, Error> {
|
||||
match state {
|
||||
State::Unsealed(unsealed) => Ok(unsealed),
|
||||
State::Bootstrapping { .. } => Err(Error::NotBootstrapped),
|
||||
State::Unbootstrapped => Err(Error::NotBootstrapped),
|
||||
State::Sealed { .. } => Err(Error::Sealed),
|
||||
}
|
||||
}
|
||||
|
||||
#[message]
|
||||
pub async fn bootstrap(&mut self, seal_key_raw: SafeCell<Vec<u8>>) -> Result<(), Error> {
|
||||
if !matches!(self.state, State::Unbootstrapped) {
|
||||
pub async fn finalize_bootstrap(&mut self) -> Result<(), Error> {
|
||||
let State::Bootstrapping {
|
||||
declared_operators,
|
||||
current_passphrases,
|
||||
} = &mut self.state
|
||||
else {
|
||||
return Err(Error::AlreadyBootstrapped);
|
||||
}
|
||||
let salt = v1::generate_salt();
|
||||
let mut seal_key = derive_key(seal_key_raw, &salt);
|
||||
};
|
||||
let mut root_key = KeyCell::new_secure_random();
|
||||
let root_key_salt = v1::generate_salt();
|
||||
|
||||
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
|
||||
let root_key_nonce = Nonce::default();
|
||||
@@ -182,11 +217,21 @@ impl Vault {
|
||||
})
|
||||
})?;
|
||||
|
||||
let data_encryption_nonce_bytes = data_encryption_nonce.to_vec();
|
||||
let mut conn = self.db.get().await?;
|
||||
|
||||
let data_encryption_nonce_bytes = data_encryption_nonce.to_vec();
|
||||
let root_key_history_id = 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)
|
||||
.values(&models::NewRootKeyHistory {
|
||||
ciphertext: root_key_ciphertext.clone(),
|
||||
@@ -194,7 +239,7 @@ impl Vault {
|
||||
root_key_encryption_nonce: root_key_nonce.to_vec(),
|
||||
data_encryption_nonce: data_encryption_nonce_bytes.clone(),
|
||||
schema_version: 1,
|
||||
salt: salt.to_vec(),
|
||||
salt: root_key_salt.to_vec(),
|
||||
})
|
||||
.returning(schema::root_key_history::id)
|
||||
.get_result(&mut *conn)
|
||||
@@ -221,11 +266,59 @@ impl Vault {
|
||||
|
||||
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]
|
||||
pub async fn try_unseal(&mut self, seal_key_raw: SafeCell<Vec<u8>>) -> Result<(), Error> {
|
||||
pub async fn contribute_bootstrap(
|
||||
&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 {
|
||||
root_key_history_id,
|
||||
current_shares,
|
||||
} = &self.state
|
||||
else {
|
||||
return Err(Error::NotBootstrapped);
|
||||
@@ -246,7 +339,7 @@ impl Vault {
|
||||
error!("Broken database: invalid salt for root key");
|
||||
Error::BrokenDatabase
|
||||
})?;
|
||||
let mut seal_key = derive_key(seal_key_raw, &salt);
|
||||
let mut seal_key = derive_key(key_raw, &salt);
|
||||
|
||||
let mut root_key = SafeCell::new(current_key.ciphertext.clone());
|
||||
|
||||
@@ -277,6 +370,25 @@ impl Vault {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[message]
|
||||
pub async fn seal(&mut self) -> Result<(), Error> {
|
||||
let Unsealed {
|
||||
root_key_history_id,
|
||||
..
|
||||
} = Self::expect_unsealed(&mut self.state)?;
|
||||
|
||||
self.state = State::Sealed {
|
||||
root_key_history_id: *root_key_history_id,
|
||||
current_shares: HashMap::new(),
|
||||
};
|
||||
let _ = self.events.tell(Publish(events::VaultResealed)).await;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// Server-side cryptographic operations
|
||||
#[messages]
|
||||
impl Vault {
|
||||
#[message]
|
||||
pub async fn decrypt(&mut self, aead_id: i32) -> Result<SafeCell<Vec<u8>>, Error> {
|
||||
let Unsealed { root_key, .. } = Self::expect_unsealed(&mut self.state)?;
|
||||
@@ -394,26 +506,47 @@ impl Vault {
|
||||
|
||||
Ok(hmac.verify_slice(&expected_mac).is_ok())
|
||||
}
|
||||
}
|
||||
|
||||
#[message]
|
||||
pub async fn seal(&mut self) -> Result<(), Error> {
|
||||
let Unsealed {
|
||||
root_key_history_id,
|
||||
..
|
||||
} = Self::expect_unsealed(&mut self.state)?;
|
||||
|
||||
self.state = State::Sealed {
|
||||
root_key_history_id: *root_key_history_id,
|
||||
};
|
||||
let _ = self.events.tell(Publish(events::VaultResealed)).await;
|
||||
Ok(())
|
||||
/// 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)]
|
||||
mod tests {
|
||||
use crate::actors::GlobalActors;
|
||||
use crate::db::models::RootKeyHistory;
|
||||
use arbiter_crypto::safecell::SafeCellHandle as _;
|
||||
|
||||
use super::*;
|
||||
@@ -423,7 +556,7 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
let seal_key = SafeCell::new(b"test-seal-key".to_vec());
|
||||
actor.bootstrap(seal_key).await.unwrap();
|
||||
actor.finalize_bootstrap(seal_key).await.unwrap();
|
||||
actor
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ impl TryFrom<SafeCell<Vec<u8>>> for KeyCell {
|
||||
if value.len() != size_of::<Key>() {
|
||||
return Err(());
|
||||
}
|
||||
let cell = SafeCell::new_inline(|cell_write: &mut Key| {
|
||||
let cell = SafeCell::new_inline_default(|cell_write: &mut Key| {
|
||||
cell_write.copy_from_slice(&value);
|
||||
});
|
||||
Ok(Self(cell))
|
||||
@@ -37,7 +37,7 @@ impl TryFrom<SafeCell<Vec<u8>>> for KeyCell {
|
||||
|
||||
impl KeyCell {
|
||||
pub fn new_secure_random() -> Self {
|
||||
let key = SafeCell::new_inline(|key_buffer: &mut Key| {
|
||||
let key = SafeCell::new_inline_default(|key_buffer: &mut Key| {
|
||||
let mut rng = StdRng::try_from_rng(&mut SysRng)
|
||||
.expect("Rng failure is unrecoverable and should panic");
|
||||
rng.fill_bytes(key_buffer);
|
||||
@@ -94,7 +94,7 @@ impl KeyCell {
|
||||
}
|
||||
|
||||
/// Derive a fixed-length key from the password using Argon2id, which is designed for password hashing and key derivation.
|
||||
pub fn derive_key(mut password: SafeCell<Vec<u8>>, salt: &Salt) -> KeyCell {
|
||||
pub fn derive_key(password: &mut SafeCell<Vec<u8>>, salt: &Salt) -> KeyCell {
|
||||
let params = {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
|
||||
@@ -44,7 +44,7 @@ impl std::fmt::Debug for SafeSigner {
|
||||
/// Returns the protected key bytes and the derived Ethereum address.
|
||||
pub fn generate(rng: &mut impl rand::Rng) -> (SafeCell<[u8; 32]>, Address) {
|
||||
loop {
|
||||
let mut cell = SafeCell::new_inline(|w: &mut [u8; 32]| {
|
||||
let mut cell = SafeCell::new_inline_default(|w: &mut [u8; 32]| {
|
||||
rng.fill_bytes(w);
|
||||
});
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ pub(super) async fn dispatch(
|
||||
VaultRequestPayload::QueryState(()) => {
|
||||
let state = match actor.ask(HandleQueryVaultState {}).await {
|
||||
Ok(VaultState::Unbootstrapped) => ProtoVaultState::Unbootstrapped,
|
||||
Ok(VaultState::Bootstrapping) => ProtoVaultState::Boostrapping,
|
||||
Ok(VaultState::Sealed) => ProtoVaultState::Sealed,
|
||||
Ok(VaultState::Unsealed) => ProtoVaultState::Unsealed,
|
||||
Err(SendError::HandlerError(Error::Internal)) => ProtoVaultState::Error,
|
||||
|
||||
@@ -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;
|
||||
@@ -47,6 +47,7 @@ async fn handle_query_vault_state(
|
||||
let state = match actor.ask(HandleQueryVaultState {}).await {
|
||||
Ok(VaultState::Unbootstrapped) => ProtoVaultState::Unbootstrapped,
|
||||
Ok(VaultState::Sealed) => ProtoVaultState::Sealed,
|
||||
Ok(VaultState::Bootstrapping) => ProtoVaultState::Boostrapping,
|
||||
Ok(VaultState::Unsealed) => ProtoVaultState::Unsealed,
|
||||
Err(err) => {
|
||||
warn!(error = ?err, "Failed to query vault state");
|
||||
|
||||
@@ -4,7 +4,6 @@ use crate::{
|
||||
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;
|
||||
@@ -46,6 +46,7 @@ impl Convert for VaultState {
|
||||
fn convert(self) -> OperatorResponsePayload {
|
||||
let proto_state = match self {
|
||||
Self::Unbootstrapped => ProtoVaultState::Unbootstrapped,
|
||||
Self::Bootstrapping => ProtoVaultState::Boostrapping,
|
||||
Self::Sealed => ProtoVaultState::Sealed,
|
||||
Self::Unsealed => ProtoVaultState::Unsealed,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user