Compare commits
5 Commits
022003ac5e
...
terminate-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a30bef11da | ||
| 357726bc5d | |||
|
|
23827c613e | ||
|
|
8381b68a52 | ||
|
|
b843105533 |
@@ -11,7 +11,9 @@ use kameo::{
|
|||||||
prelude::{ActorId, ActorRef, ActorStopReason, Context, WeakActorRef},
|
prelude::{ActorId, ActorRef, ActorStopReason, Context, WeakActorRef},
|
||||||
reply::ReplySender,
|
reply::ReplySender,
|
||||||
};
|
};
|
||||||
use std::ops::ControlFlow;
|
use std::{ops::ControlFlow, time::Duration};
|
||||||
|
|
||||||
|
const APPROVAL_TIMEOUT: Duration = Duration::from_secs(30);
|
||||||
|
|
||||||
pub struct Args {
|
pub struct Args {
|
||||||
pub client: ClientProfile,
|
pub client: ClientProfile,
|
||||||
@@ -64,6 +66,14 @@ impl Actor for ClientApprovalController {
|
|||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let weak = actor_ref.downgrade();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
tokio::time::sleep(APPROVAL_TIMEOUT).await;
|
||||||
|
if let Some(r) = weak.upgrade() {
|
||||||
|
let _ = r.tell(OnApprovalTimeout {}).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
Ok(this)
|
Ok(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,4 +114,14 @@ impl ClientApprovalController {
|
|||||||
ctx.stop();
|
ctx.stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fired after `APPROVAL_TIMEOUT` elapses. Any operator that hasn't responded
|
||||||
|
/// by then is treated as a denial to prevent zombie sessions from blocking the flow.
|
||||||
|
#[message(ctx)]
|
||||||
|
pub fn on_approval_timeout(&mut self, ctx: &mut Context<Self, ()>) {
|
||||||
|
if self.pending > 0 {
|
||||||
|
self.send_reply(Ok(false));
|
||||||
|
ctx.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
use super::{OutOfBand, OperatorConnection};
|
use super::{OutOfBand, OperatorConnection};
|
||||||
use crate::{
|
use crate::{
|
||||||
actors::{
|
actors::{
|
||||||
flow_coordinator::{GetConnectedClientIds, client_connect_approval::ClientApprovalController},
|
flow_coordinator::{GetConnectedClientIds, client_connect_approval::{ClientApprovalAnswer, ClientApprovalController}}, operator_registry::ConnectOperator,
|
||||||
operator_registry::ConnectOperator,
|
}, peers::client::ClientProfile,
|
||||||
},
|
|
||||||
peers::client::ClientProfile,
|
|
||||||
};
|
};
|
||||||
use arbiter_crypto::authn;
|
use arbiter_crypto::authn;
|
||||||
use arbiter_proto::transport::Sender;
|
use arbiter_proto::transport::Sender;
|
||||||
@@ -93,6 +91,7 @@ impl OperatorSession {
|
|||||||
actor = "operator",
|
actor = "operator",
|
||||||
event = "failed to announce new client connection"
|
event = "failed to announce new client connection"
|
||||||
);
|
);
|
||||||
|
let _ = controller.tell(ClientApprovalAnswer { approved: false }).await;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
|
|||||||
use state::State;
|
use state::State;
|
||||||
|
|
||||||
use chacha20poly1305::{AeadInPlace, KeyInit as _, XChaCha20Poly1305, XNonce};
|
use chacha20poly1305::{AeadInPlace, KeyInit as _, XChaCha20Poly1305, XNonce};
|
||||||
use kameo::{Actor, error::SendError, messages, prelude::Message};
|
use kameo::{Actor, error::SendError, messages, prelude::{Context, Message}};
|
||||||
use kameo_actors::message_bus::Register;
|
use kameo_actors::message_bus::Register;
|
||||||
use tokio::sync::oneshot;
|
use tokio::sync::oneshot;
|
||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
@@ -143,12 +143,13 @@ impl VaultGate {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[message]
|
#[message(ctx)]
|
||||||
pub async fn handle_unseal_encrypted_key(
|
pub async fn handle_unseal_encrypted_key(
|
||||||
&mut self,
|
&mut self,
|
||||||
nonce: Vec<u8>,
|
nonce: Vec<u8>,
|
||||||
ciphertext: Vec<u8>,
|
ciphertext: Vec<u8>,
|
||||||
associated_data: Vec<u8>,
|
associated_data: Vec<u8>,
|
||||||
|
ctx: &mut Context<Self, Result<(), Error>>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let State::ReadyForExchange { secret, .. } = &self.state else {
|
let State::ReadyForExchange { secret, .. } = &self.state else {
|
||||||
return Err(Error::State);
|
return Err(Error::State);
|
||||||
@@ -172,7 +173,12 @@ impl VaultGate {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(SendError::HandlerError(vault::Error::InvalidKey)) => Err(Error::InvalidKey),
|
Err(SendError::HandlerError(vault::Error::InvalidKey)) => Err(Error::InvalidKey),
|
||||||
Err(SendError::HandlerError(vault::Error::LockedOut)) => Err(Error::LockedOut),
|
Err(SendError::HandlerError(vault::Error::LockedOut)) => {
|
||||||
|
// Vault is permanently locked — terminate this gate so the
|
||||||
|
// run_vault_gate loop breaks and the connection is closed.
|
||||||
|
ctx.stop();
|
||||||
|
Err(Error::LockedOut)
|
||||||
|
}
|
||||||
Err(SendError::HandlerError(err)) => {
|
Err(SendError::HandlerError(err)) => {
|
||||||
error!(?err, "Vault failed to unseal key");
|
error!(?err, "Vault failed to unseal key");
|
||||||
Err(Error::InvalidKey)
|
Err(Error::InvalidKey)
|
||||||
@@ -245,7 +251,7 @@ impl Message<events::Bootstrapped> for VaultGate {
|
|||||||
async fn handle(
|
async fn handle(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: events::Bootstrapped,
|
_: events::Bootstrapped,
|
||||||
ctx: &mut kameo::prelude::Context<Self, Self::Reply>,
|
ctx: &mut Context<Self, Self::Reply>,
|
||||||
) -> Self::Reply {
|
) -> Self::Reply {
|
||||||
let result = async {
|
let result = async {
|
||||||
let mut conn = self
|
let mut conn = self
|
||||||
@@ -281,7 +287,7 @@ impl Message<events::Unsealed> for VaultGate {
|
|||||||
async fn handle(
|
async fn handle(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: events::Unsealed,
|
_: events::Unsealed,
|
||||||
ctx: &mut kameo::prelude::Context<Self, Self::Reply>,
|
ctx: &mut Context<Self, Self::Reply>,
|
||||||
) -> Self::Reply {
|
) -> Self::Reply {
|
||||||
if let Some(tx) = self.promotion_tx.take() {
|
if let Some(tx) = self.promotion_tx.take() {
|
||||||
let _ = tx.send(Ok(()));
|
let _ = tx.send(Ok(()));
|
||||||
|
|||||||
@@ -266,7 +266,7 @@ pub async fn metadata_unchanged_does_not_append_history() {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[test_log::test]
|
#[test_log::test]
|
||||||
pub async fn metadata_change_appends_history_and_repoints_binding() {
|
pub async fn metadata_frozen_after_approval_ignores_reconnect_changes() {
|
||||||
let db = db::create_test_pool().await;
|
let db = db::create_test_pool().await;
|
||||||
let actors = spawn_test_actors(&db).await;
|
let actors = spawn_test_actors(&db).await;
|
||||||
let new_key = MlDsa87::key_gen(&mut rand::rng());
|
let new_key = MlDsa87::key_gen(&mut rand::rng());
|
||||||
@@ -287,6 +287,7 @@ pub async fn metadata_change_appends_history_and_repoints_binding() {
|
|||||||
connect_client(props, &mut server_transport).await;
|
connect_client(props, &mut server_transport).await;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Reconnect presenting different metadata — must be silently ignored.
|
||||||
test_transport
|
test_transport
|
||||||
.send(auth::Inbound::AuthChallengeRequest {
|
.send(auth::Inbound::AuthChallengeRequest {
|
||||||
pubkey: verifying_key(&new_key).into(),
|
pubkey: verifying_key(&new_key).into(),
|
||||||
@@ -313,6 +314,7 @@ pub async fn metadata_change_appends_history_and_repoints_binding() {
|
|||||||
client_metadata, client_metadata_history, program_client,
|
client_metadata, client_metadata_history, program_client,
|
||||||
};
|
};
|
||||||
let mut conn = db.get().await.unwrap();
|
let mut conn = db.get().await.unwrap();
|
||||||
|
// Metadata is frozen: no new row, no history entry.
|
||||||
let metadata_count: i64 = client_metadata::table
|
let metadata_count: i64 = client_metadata::table
|
||||||
.count()
|
.count()
|
||||||
.get_result(&mut conn)
|
.get_result(&mut conn)
|
||||||
@@ -338,15 +340,16 @@ pub async fn metadata_change_appends_history_and_repoints_binding() {
|
|||||||
.first::<(String, Option<String>, Option<String>)>(&mut conn)
|
.first::<(String, Option<String>, Option<String>)>(&mut conn)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(metadata_count, 2);
|
assert_eq!(metadata_count, 1, "frozen: no new metadata row on reconnect");
|
||||||
assert_eq!(history_count, 1);
|
assert_eq!(history_count, 0, "frozen: no history entry on reconnect");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
current,
|
current,
|
||||||
(
|
(
|
||||||
"client".to_owned(),
|
"client".to_owned(),
|
||||||
Some("new".to_owned()),
|
Some("old".to_owned()),
|
||||||
Some("2.0.0".to_owned())
|
Some("1.0.0".to_owned())
|
||||||
)
|
),
|
||||||
|
"frozen: original metadata must be preserved"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,6 +140,47 @@ pub async fn unseal_corrupted_ciphertext() {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// After MAX_UNSEAL_ATTEMPTS wrong keys, the vault locks and the VaultGate
|
||||||
|
/// must stop itself so the connection is dropped.
|
||||||
|
#[tokio::test]
|
||||||
|
#[test_log::test]
|
||||||
|
pub async fn lockout_stops_vault_gate() {
|
||||||
|
use kameo::error::SendError;
|
||||||
|
|
||||||
|
let seal_key = b"real-seal-key";
|
||||||
|
let (_db, gate, _promotion_rx) = setup_sealed_gate(seal_key).await;
|
||||||
|
|
||||||
|
// Exhaust all MAX_UNSEAL_ATTEMPTS (5) with wrong keys; each returns InvalidKey.
|
||||||
|
for _ in 0..5 {
|
||||||
|
let encrypted_key = client_dh_encrypt(&gate, b"wrong-key").await;
|
||||||
|
assert!(matches!(
|
||||||
|
gate.ask(encrypted_key).await,
|
||||||
|
Err(SendError::HandlerError(VaultGateError::InvalidKey))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sixth attempt: vault is now locked, returns LockedOut, and the gate stops itself.
|
||||||
|
let encrypted_key = client_dh_encrypt(&gate, b"wrong-key").await;
|
||||||
|
assert!(matches!(
|
||||||
|
gate.ask(encrypted_key).await,
|
||||||
|
Err(SendError::HandlerError(VaultGateError::LockedOut))
|
||||||
|
));
|
||||||
|
|
||||||
|
// Give the actor scheduler time to process the stop signal.
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
|
||||||
|
|
||||||
|
// Any subsequent message must be rejected because the gate is stopped.
|
||||||
|
let client_secret = EphemeralSecret::random();
|
||||||
|
let client_public = PublicKey::from(&client_secret);
|
||||||
|
assert!(
|
||||||
|
matches!(
|
||||||
|
gate.ask(HandleHandshake { client_pubkey: client_public }).await,
|
||||||
|
Err(SendError::ActorNotRunning(_))
|
||||||
|
),
|
||||||
|
"VaultGate must be stopped after LockedOut"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[test_log::test]
|
#[test_log::test]
|
||||||
pub async fn unseal_retry_after_invalid_key() {
|
pub async fn unseal_retry_after_invalid_key() {
|
||||||
|
|||||||
Reference in New Issue
Block a user