feat(brahman-handshake): Fase 3 — trust remoto via firma Ed25519
Cuarto paso del plan "el encuentro entre Entes no se restringe a
local". Cierra la falla de seguridad que dejaba la red P2P abierta:
hasta ahora un atacante que pudiera dial-ar al multiaddr del Init
podia registrar cualquier Card con cualquier label/flow. Fase 3
exige que el Hello via libp2p venga firmado con la misma keypair
Ed25519 que produce el peer_id autenticado por Noise.
Modelo de trust:
- Local Unix: SO_PEERCRED del kernel autentica. Firma opcional pero
verificada si presente (defensa en profundidad).
- Remoto libp2p: firma obligatoria; public key del Hello debe derivar
al peer_id autenticado por Noise. Si falta o no coincide ->
HandshakeError::Unauthorized.
Wire:
- Hello.signature: Option<HelloSignature> (default None, backwards
compat para path Unix).
- HelloSignature { public_key: Vec<u8>, signature: Vec<u8> } —
public_key en formato canonico libp2p (encode_protobuf), firma
Ed25519 sobre (SIGNATURE_VERSION, WireCard, Option<WitInterface>)
serializado postcard.
Nuevo modulo signature:
- sign_hello / verify_hello con SignatureError tipado.
- 4 unit tests: roundtrip, peer mismatch, card tampered, sig flipped.
Server:
- Session<S> gana expected_peer: Option<PeerId>.
- session_from_libp2p_stream(stream, peer) para path remoto;
session_from_stream sin peer para Unix.
- do_handshake exige firma + verifica peer match si expected_peer.
Client:
- connect_with_stream_signed(stream, card, wit, &Keypair) (nuevo).
- connect_libp2p ahora requiere &Keypair (breaking change).
BrahmanNet:
- Almacena Arc<Keypair> internamente; expose keypair() accessor.
Truco: ed25519::Keypair SI es Clone, se duplica para que swarm
(Noise) y caller (signing) compartan identidad sin copiar bytes.
- with_keypair rechaza no-Ed25519.
Tests: 4 unit signature + 1 E2E negativo nuevo
(libp2p_handshake_rejects_mismatched_signing_key) + E2E positivo
ya pasaba con keypair correcta. 90+ tests verdes en
brahman-handshake/brahman-net/brahman-card/minga-p2p.
Lo que cierra: la cadena completa de discovery + connect + trust
funciona cross-machine sin paths hardcodeados ni confianza
implicita. Brahman-net es una malla publicamente dial-able CON
autenticacion criptografica end-to-end.
Pendientes futuros: stop_providing en cleanup, wire de Arje con
ServerConfig.net configurado, allowlist/denylist de peers,
persistencia de la keypair entre reboots.
This commit is contained in:
@@ -125,6 +125,7 @@ async fn server_rejects_protocol_mismatch() {
|
||||
protocol_version: "999.0.0".into(),
|
||||
card: sample_card("future-module").into(),
|
||||
wit: None,
|
||||
signature: None,
|
||||
};
|
||||
write_frame(&mut stream, &Frame::Hello(hello)).await.unwrap();
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ use brahman_card::{
|
||||
};
|
||||
use brahman_handshake::network::{connect_libp2p, run_libp2p_accept_loop};
|
||||
use brahman_handshake::server::{Server, ServerConfig};
|
||||
use brahman_net::{BrahmanNet, Multiaddr, PeerId, Protocol};
|
||||
use brahman_net::{BrahmanNet, Keypair, Multiaddr, PeerId, Protocol};
|
||||
use tempfile::TempDir;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
@@ -86,7 +86,8 @@ async fn libp2p_handshake_roundtrip() {
|
||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||
|
||||
let card = sample_card("test.remote_ente");
|
||||
let mut client = connect_libp2p(&client_net, server_peer_id, card, None)
|
||||
let client_kp = client_net.keypair();
|
||||
let mut client = connect_libp2p(&client_net, server_peer_id, card, None, &client_kp)
|
||||
.await
|
||||
.expect("handshake remoto debería completar");
|
||||
|
||||
@@ -116,3 +117,54 @@ async fn libp2p_handshake_roundtrip() {
|
||||
// peer_id no usado aquí, pero validamos que la API existe.
|
||||
let _ = PeerId::random();
|
||||
}
|
||||
|
||||
/// Fase 3 negativo: el cliente intenta firmar el Hello con una keypair
|
||||
/// distinta a la del peer libp2p. El server (que verifica que la
|
||||
/// public key del Hello derive al peer_id autenticado por Noise) debe
|
||||
/// rechazar con `Unauthorized`.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||||
async fn libp2p_handshake_rejects_mismatched_signing_key() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let unix_socket = tmp.path().join("brahman-init.sock");
|
||||
|
||||
let server = Arc::new(
|
||||
Server::bind(
|
||||
&unix_socket,
|
||||
ServerConfig {
|
||||
init_attached: true,
|
||||
broker: None,
|
||||
net: None,
|
||||
},
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
let sessions = server.sessions();
|
||||
|
||||
let server_net = Arc::new(BrahmanNet::new().unwrap());
|
||||
let server_peer = server_net.peer_id;
|
||||
let listen_addr: Multiaddr = "/ip4/127.0.0.1/tcp/0".parse().unwrap();
|
||||
let actual = server_net.listen(listen_addr).await;
|
||||
let mut full = actual.clone();
|
||||
full.push(Protocol::P2p(server_peer));
|
||||
|
||||
tokio::spawn(run_libp2p_accept_loop(server.clone(), server_net.clone()));
|
||||
|
||||
let client_net = BrahmanNet::new().unwrap();
|
||||
client_net.dial(full);
|
||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||
|
||||
// Keypair fraudulenta: NO es la del client_net.
|
||||
let evil_keypair = Keypair::generate_ed25519();
|
||||
|
||||
let card = sample_card("test.evil");
|
||||
let result = connect_libp2p(&client_net, server_peer, card, None, &evil_keypair).await;
|
||||
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"handshake con keypair fraudulenta debe fallar"
|
||||
);
|
||||
|
||||
// Sanidad: ninguna sesión registrada.
|
||||
let s = sessions.lock().await;
|
||||
assert_eq!(s.len(), 0, "no debería haber sesión registrada");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user