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:
@@ -44,7 +44,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use brahman_card::{Card, TypeRef, WitInterface};
|
||||
use brahman_net::{BrahmanNet, OpenStreamError, PeerId, Stream, StreamProtocol};
|
||||
use brahman_net::{BrahmanNet, Keypair, OpenStreamError, PeerId, Stream, StreamProtocol};
|
||||
use futures::StreamExt;
|
||||
use tokio_util::compat::{Compat, FuturesAsyncReadCompatExt};
|
||||
use tracing::{debug, warn};
|
||||
@@ -107,7 +107,9 @@ async fn handle_libp2p_session(
|
||||
stream: Stream,
|
||||
peer: PeerId,
|
||||
) {
|
||||
let session = server.session_from_stream(stream.compat());
|
||||
// session_from_libp2p_stream propaga el peer_id al `do_handshake`,
|
||||
// que exige firma del Hello cuya public key derive a este peer.
|
||||
let session = server.session_from_libp2p_stream(stream.compat(), peer);
|
||||
if let Err(e) = session.handle().await {
|
||||
warn!(
|
||||
target: "brahman_handshake::network",
|
||||
@@ -119,8 +121,14 @@ async fn handle_libp2p_session(
|
||||
}
|
||||
|
||||
/// Conecta como cliente a un Ente remoto vía libp2p y completa el
|
||||
/// handshake. Requiere que `net` ya tenga conexión (o pueda dial-ar)
|
||||
/// al `peer`; típicamente el caller hace `net.dial(multiaddr)` antes.
|
||||
/// handshake **firmado** con `keypair`. Requiere que `net` ya tenga
|
||||
/// conexión (o pueda dial-ar) al `peer`; típicamente el caller hace
|
||||
/// `net.dial(multiaddr)` antes.
|
||||
///
|
||||
/// La `keypair` debe ser la misma que la del nodo libp2p (la que
|
||||
/// pasaste a [`brahman_net::BrahmanNet::with_keypair`]). Si no coincide
|
||||
/// con el `peer_id` autenticado por Noise, el server rechaza el Hello
|
||||
/// con `Unauthorized`.
|
||||
///
|
||||
/// Devuelve un `Client` típico — los métodos `ping`, `await_event`,
|
||||
/// `farewell` funcionan idéntico al path Unix. El stream subyacente
|
||||
@@ -130,12 +138,13 @@ pub async fn connect_libp2p(
|
||||
peer: PeerId,
|
||||
card: Card,
|
||||
wit: Option<WitInterface>,
|
||||
keypair: &Keypair,
|
||||
) -> Result<Client<LibP2pHandshakeStream>, NetworkError> {
|
||||
let mut control = net.control.clone();
|
||||
let stream = control
|
||||
.open_stream(peer, BRAHMAN_HANDSHAKE_PROTOCOL)
|
||||
.await?;
|
||||
let client = Client::connect_with_stream(stream.compat(), card, wit).await?;
|
||||
let client = Client::connect_with_stream_signed(stream.compat(), card, wit, keypair).await?;
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user