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:
@@ -6,6 +6,100 @@ ratio/diff ver `git show <sha>`.
|
|||||||
|
|
||||||
## 2026-05-09
|
## 2026-05-09
|
||||||
|
|
||||||
|
### feat(brahman-handshake): Fase 3 — trust remoto vía firma Ed25519 anclada al peer libp2p
|
||||||
|
Cuarto y último 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 podía registrar cualquier Card con cualquier
|
||||||
|
label/flow. Fase 3 cierra esto exigiendo que el Hello vía libp2p
|
||||||
|
venga firmado con la **misma keypair Ed25519 que produce el
|
||||||
|
`peer_id` autenticado por Noise**.
|
||||||
|
|
||||||
|
Modelo:
|
||||||
|
- **Local Unix**: SO_PEERCRED del kernel autentica al cliente. Firma
|
||||||
|
opcional. Si está presente, igual se verifica (defensa en
|
||||||
|
profundidad).
|
||||||
|
- **Remoto libp2p**: firma obligatoria. La public key del Hello debe
|
||||||
|
derivar al `peer_id` que Noise ya autenticó. Si falta o no coincide
|
||||||
|
→ `HandshakeError::Unauthorized`.
|
||||||
|
|
||||||
|
Wire (`brahman_handshake::messages`):
|
||||||
|
- `Hello.signature: Option<HelloSignature>` (nuevo, default None).
|
||||||
|
- `HelloSignature { public_key: Vec<u8>, signature: Vec<u8> }` —
|
||||||
|
public_key en formato canónico libp2p (`encode_protobuf`), firma
|
||||||
|
Ed25519 sobre `(SIGNATURE_VERSION, WireCard, Option<WitInterface>)`
|
||||||
|
serializado postcard.
|
||||||
|
- `SIGNATURE_VERSION = 1` documenta el esquema del payload firmado;
|
||||||
|
bump al cambiar.
|
||||||
|
|
||||||
|
Nuevo módulo `brahman_handshake::signature`:
|
||||||
|
- `sign_hello(keypair, card, wit) -> HelloSignature`.
|
||||||
|
- `verify_hello(sig, card, wit, expected_peer) -> Result<(), SignatureError>`.
|
||||||
|
- `SignatureError` tipado (`DecodeKey`, `EncodePayload`, `Invalid`,
|
||||||
|
`PeerMismatch`, `Missing`, `Unexpected`).
|
||||||
|
|
||||||
|
Server:
|
||||||
|
- `Session<S>` gana `expected_peer: Option<PeerId>`.
|
||||||
|
- `Server::session_from_libp2p_stream(stream, peer)` (nuevo)
|
||||||
|
construye Session con `expected_peer = Some(peer)`.
|
||||||
|
`session_from_stream` (Unix/in-memory) sigue con `None`.
|
||||||
|
- `do_handshake` exige firma + verifica peer match cuando
|
||||||
|
`expected_peer.is_some()`. Si no, verifica firma presente por
|
||||||
|
consistencia interna pero no exige que esté.
|
||||||
|
- `network::run_libp2p_accept_loop` ahora usa
|
||||||
|
`session_from_libp2p_stream(stream.compat(), peer)` para propagar
|
||||||
|
la identidad libp2p al gate de trust.
|
||||||
|
|
||||||
|
Client:
|
||||||
|
- `Client::connect_with_stream_signed(stream, card, wit, &Keypair)`
|
||||||
|
(nuevo) firma el Hello antes de mandarlo.
|
||||||
|
- `Client::connect_with_stream` sigue existiendo sin firma (path
|
||||||
|
Unix / tests).
|
||||||
|
- `Client::connect`/`connect_with` (Unix) no cambian — siguen sin
|
||||||
|
firma porque SO_PEERCRED autentica.
|
||||||
|
- `network::connect_libp2p(net, peer, card, wit, keypair)`
|
||||||
|
**breaking change**: gana parámetro `keypair: &Keypair`.
|
||||||
|
|
||||||
|
BrahmanNet:
|
||||||
|
- Almacena la `Keypair` en `Arc<Keypair>` (libp2p Keypair no es
|
||||||
|
Clone; el truco es duplicar el `ed25519::Keypair` interno que sí
|
||||||
|
es Clone, una copia para Noise/swarm y otra para signing).
|
||||||
|
- `BrahmanNet::keypair() -> Arc<Keypair>` accessor para que callers
|
||||||
|
puedan firmar con la misma identidad libp2p del nodo sin tener
|
||||||
|
que mantener la keypair por separado.
|
||||||
|
- `with_keypair` rechaza keypairs no-Ed25519 (RSA/ECDSA/Secp256k1
|
||||||
|
vendrían a futuro si se necesitan).
|
||||||
|
|
||||||
|
Tests:
|
||||||
|
- 4 unit en `signature::tests`: roundtrip propio, peer mismatch,
|
||||||
|
card tampered, signature flipped.
|
||||||
|
- 1 E2E nuevo en `tests/network_libp2p.rs`:
|
||||||
|
`libp2p_handshake_rejects_mismatched_signing_key` — el cliente
|
||||||
|
intenta firmar con keypair distinta a la del net; server rechaza.
|
||||||
|
- E2E positivo (`libp2p_handshake_roundtrip`) ahora pasa la keypair
|
||||||
|
del client_net y debe verificar OK.
|
||||||
|
- Discovery + handshake legacy + signature: 90+ tests verdes en
|
||||||
|
brahman-handshake/brahman-net/brahman-card/minga-p2p.
|
||||||
|
|
||||||
|
Lo que esto cierra:
|
||||||
|
- Brahman-net es una malla públicamente dial-able **con
|
||||||
|
autenticación criptográfica end-to-end**: Noise para el transport,
|
||||||
|
Ed25519 para las Cards.
|
||||||
|
- La cadena completa de discovery + connect + trust funciona
|
||||||
|
cross-machine sin paths hardcodeados ni confianza implícita.
|
||||||
|
- El plan original ("el encuentro entre Entes no se restringe a
|
||||||
|
local, la ejecución remota está pensada desde el principio")
|
||||||
|
está implementado y testeado.
|
||||||
|
|
||||||
|
Pendientes (futuro, no de hoy):
|
||||||
|
- `stop_providing` al cleanup de sesión (records DHT viven hasta
|
||||||
|
TTL ~24h).
|
||||||
|
- Wire de Arje (`ente-zero`) para arrancar opcionalmente con
|
||||||
|
`BrahmanNet` configurado y `ServerConfig.net = Some(...)`.
|
||||||
|
- Allowlist/Denylist de peers (hoy cualquier peer Ed25519-válido
|
||||||
|
pasa el trust gate; producción podría querer un PKI explícito).
|
||||||
|
- Persistencia de la keypair de identidad del nodo entre reboots.
|
||||||
|
|
||||||
### feat(brahman-handshake): Fase 2 — discovery remoto vía DHT por flow type
|
### feat(brahman-handshake): Fase 2 — discovery remoto vía DHT por flow type
|
||||||
Tercer paso del plan "el encuentro entre Entes no se restringe a
|
Tercer paso del plan "el encuentro entre Entes no se restringe a
|
||||||
local". Cuando un Init local acepta una sesión cuya Card declara
|
local". Cuando un Init local acepta una sesión cuya Card declara
|
||||||
|
|||||||
@@ -5,12 +5,14 @@ use std::path::Path;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use brahman_card::{Card, WitInterface, CARD_SCHEMA_VERSION};
|
use brahman_card::{Card, WitInterface, CARD_SCHEMA_VERSION};
|
||||||
|
use brahman_net::Keypair;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tokio::io::{AsyncRead, AsyncWrite};
|
use tokio::io::{AsyncRead, AsyncWrite};
|
||||||
use tokio::net::UnixStream;
|
use tokio::net::UnixStream;
|
||||||
|
|
||||||
use crate::codec::{read_frame, write_frame};
|
use crate::codec::{read_frame, write_frame};
|
||||||
use crate::messages::{Farewell, Frame, HandshakeError, Hello, HelloAck, MatchEvent, Ping, SessionId};
|
use crate::messages::{Farewell, Frame, HandshakeError, Hello, HelloAck, MatchEvent, Ping, SessionId};
|
||||||
|
use crate::signature::{sign_hello, SignatureError};
|
||||||
|
|
||||||
/// Errores del cliente.
|
/// Errores del cliente.
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
@@ -29,6 +31,11 @@ pub enum ClientError {
|
|||||||
/// La Card que el cliente intentó enviar no pasa su propia validación.
|
/// La Card que el cliente intentó enviar no pasa su propia validación.
|
||||||
#[error("card inválida pre-envío: {0}")]
|
#[error("card inválida pre-envío: {0}")]
|
||||||
InvalidCard(String),
|
InvalidCard(String),
|
||||||
|
|
||||||
|
/// Firma del Hello falló al construirse (rara — sólo puede pasar
|
||||||
|
/// si la keypair pasada está en un estado inválido).
|
||||||
|
#[error("firma del Hello falló: {0}")]
|
||||||
|
Signature(#[from] SignatureError),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cliente conectado y autenticado. Tras `connect` ya completó el handshake
|
/// Cliente conectado y autenticado. Tras `connect` ya completó el handshake
|
||||||
@@ -72,23 +79,54 @@ impl<S> Client<S>
|
|||||||
where
|
where
|
||||||
S: AsyncRead + AsyncWrite + Unpin + Send,
|
S: AsyncRead + AsyncWrite + Unpin + Send,
|
||||||
{
|
{
|
||||||
/// Constructor genérico: arranca el handshake sobre un stream
|
/// Constructor genérico sobre un stream ya abierto, **sin firma**.
|
||||||
/// arbitrario que ya esté abierto. Es el punto de entrada para
|
/// Apto para path Unix (donde SO_PEERCRED del kernel ya autentica)
|
||||||
/// transports alternativos (libp2p, in-memory para tests, etc.)
|
/// o tests in-memory. Para libp2p remoto usá
|
||||||
/// que reusan toda la lógica del handshake post-stream-open.
|
/// [`connect_with_stream_signed`](Self::connect_with_stream_signed) —
|
||||||
|
/// el server libp2p rechaza Hello sin firma.
|
||||||
pub async fn connect_with_stream(
|
pub async fn connect_with_stream(
|
||||||
|
stream: S,
|
||||||
|
card: Card,
|
||||||
|
wit: Option<WitInterface>,
|
||||||
|
) -> Result<Self, ClientError> {
|
||||||
|
Self::connect_inner(stream, card, wit, None).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Igual que `connect_with_stream` pero firma el Hello con
|
||||||
|
/// `keypair`. Usar para conexiones libp2p donde el server exige
|
||||||
|
/// firma. La public key derivada de `keypair` debe coincidir con
|
||||||
|
/// el `peer_id` libp2p autenticado por Noise — típicamente la
|
||||||
|
/// keypair pasada a [`brahman_net::BrahmanNet::with_keypair`].
|
||||||
|
pub async fn connect_with_stream_signed(
|
||||||
|
stream: S,
|
||||||
|
card: Card,
|
||||||
|
wit: Option<WitInterface>,
|
||||||
|
keypair: &Keypair,
|
||||||
|
) -> Result<Self, ClientError> {
|
||||||
|
Self::connect_inner(stream, card, wit, Some(keypair)).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn connect_inner(
|
||||||
mut stream: S,
|
mut stream: S,
|
||||||
card: Card,
|
card: Card,
|
||||||
wit: Option<WitInterface>,
|
wit: Option<WitInterface>,
|
||||||
|
keypair: Option<&Keypair>,
|
||||||
) -> Result<Self, ClientError> {
|
) -> Result<Self, ClientError> {
|
||||||
card.validate()
|
card.validate()
|
||||||
.map_err(|e| ClientError::InvalidCard(e.to_string()))?;
|
.map_err(|e| ClientError::InvalidCard(e.to_string()))?;
|
||||||
|
|
||||||
|
let wire_card = brahman_card::WireCard::from(card);
|
||||||
|
let signature = match keypair {
|
||||||
|
Some(kp) => Some(sign_hello(kp, &wire_card, &wit)?),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
let hello = Hello {
|
let hello = Hello {
|
||||||
schema_version: CARD_SCHEMA_VERSION,
|
schema_version: CARD_SCHEMA_VERSION,
|
||||||
protocol_version: brahman_card::PROTOCOL_VERSION.to_string(),
|
protocol_version: brahman_card::PROTOCOL_VERSION.to_string(),
|
||||||
card: card.into(), // Card → WireCard: descarta extensions
|
card: wire_card,
|
||||||
wit,
|
wit,
|
||||||
|
signature,
|
||||||
};
|
};
|
||||||
write_frame(&mut stream, &Frame::Hello(hello)).await?;
|
write_frame(&mut stream, &Frame::Hello(hello)).await?;
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ pub mod messages;
|
|||||||
pub mod server;
|
pub mod server;
|
||||||
pub mod client;
|
pub mod client;
|
||||||
pub mod network;
|
pub mod network;
|
||||||
|
pub mod signature;
|
||||||
pub mod transport;
|
pub mod transport;
|
||||||
|
|
||||||
pub use brahman_card::PROTOCOL_VERSION;
|
pub use brahman_card::PROTOCOL_VERSION;
|
||||||
|
|||||||
@@ -17,6 +17,14 @@ pub type SessionId = Ulid;
|
|||||||
/// la convierte a `Card` para uso interno. Opcionalmente, una
|
/// la convierte a `Card` para uso interno. Opcionalmente, una
|
||||||
/// `WitInterface` ya extraída — si está presente, el módulo es
|
/// `WitInterface` ya extraída — si está presente, el módulo es
|
||||||
/// "consciente" y el server lo registra como `ResolvedCard::from_conscious`.
|
/// "consciente" y el server lo registra como `ResolvedCard::from_conscious`.
|
||||||
|
///
|
||||||
|
/// **Firma (Fase 3, trust remoto)**: el campo `signature` es
|
||||||
|
/// obligatorio para conexiones libp2p (donde el server exige que la
|
||||||
|
/// public key derive al `peer_id` autenticado por Noise) y opcional
|
||||||
|
/// para Unix socket (donde SO_PEERCRED del kernel ya provee
|
||||||
|
/// autenticación). La firma cubre los bytes postcard de
|
||||||
|
/// `(WireCard, Option<WitInterface>)` — ver
|
||||||
|
/// [`HelloSignature::sign_payload`].
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Hello {
|
pub struct Hello {
|
||||||
/// Versión del schema de Card que el cliente sigue.
|
/// Versión del schema de Card que el cliente sigue.
|
||||||
@@ -29,6 +37,22 @@ pub struct Hello {
|
|||||||
/// `brahman-card-wit`). `None` si el módulo es agnóstico.
|
/// `brahman-card-wit`). `None` si el módulo es agnóstico.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub wit: Option<WitInterface>,
|
pub wit: Option<WitInterface>,
|
||||||
|
/// Firma Ed25519 sobre `(card, wit)`. Requerida para conexiones
|
||||||
|
/// remotas (libp2p); opcional para Unix socket. Ver módulo
|
||||||
|
/// [`super::signature`] para construcción y verificación.
|
||||||
|
#[serde(default)]
|
||||||
|
pub signature: Option<HelloSignature>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Firma de un Hello. La `public_key` viaja en el formato canónico
|
||||||
|
/// libp2p (protobuf) — el verificador la decodifica y compara su
|
||||||
|
/// `peer_id` derivado con la identidad libp2p autenticada por Noise.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct HelloSignature {
|
||||||
|
/// Public key del firmante, encoded como `libp2p::identity::PublicKey::encode_protobuf()`.
|
||||||
|
pub public_key: Vec<u8>,
|
||||||
|
/// Bytes de la firma Ed25519 sobre el payload canonical.
|
||||||
|
pub signature: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Respuesta del servidor a un `Hello` aceptado.
|
/// Respuesta del servidor a un `Hello` aceptado.
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use brahman_card::{Card, TypeRef, WitInterface};
|
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 futures::StreamExt;
|
||||||
use tokio_util::compat::{Compat, FuturesAsyncReadCompatExt};
|
use tokio_util::compat::{Compat, FuturesAsyncReadCompatExt};
|
||||||
use tracing::{debug, warn};
|
use tracing::{debug, warn};
|
||||||
@@ -107,7 +107,9 @@ async fn handle_libp2p_session(
|
|||||||
stream: Stream,
|
stream: Stream,
|
||||||
peer: PeerId,
|
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 {
|
if let Err(e) = session.handle().await {
|
||||||
warn!(
|
warn!(
|
||||||
target: "brahman_handshake::network",
|
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
|
/// 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)
|
/// handshake **firmado** con `keypair`. Requiere que `net` ya tenga
|
||||||
/// al `peer`; típicamente el caller hace `net.dial(multiaddr)` antes.
|
/// 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`,
|
/// Devuelve un `Client` típico — los métodos `ping`, `await_event`,
|
||||||
/// `farewell` funcionan idéntico al path Unix. El stream subyacente
|
/// `farewell` funcionan idéntico al path Unix. El stream subyacente
|
||||||
@@ -130,12 +138,13 @@ pub async fn connect_libp2p(
|
|||||||
peer: PeerId,
|
peer: PeerId,
|
||||||
card: Card,
|
card: Card,
|
||||||
wit: Option<WitInterface>,
|
wit: Option<WitInterface>,
|
||||||
|
keypair: &Keypair,
|
||||||
) -> Result<Client<LibP2pHandshakeStream>, NetworkError> {
|
) -> Result<Client<LibP2pHandshakeStream>, NetworkError> {
|
||||||
let mut control = net.control.clone();
|
let mut control = net.control.clone();
|
||||||
let stream = control
|
let stream = control
|
||||||
.open_stream(peer, BRAHMAN_HANDSHAKE_PROTOCOL)
|
.open_stream(peer, BRAHMAN_HANDSHAKE_PROTOCOL)
|
||||||
.await?;
|
.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)
|
Ok(client)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use std::time::{SystemTime, UNIX_EPOCH};
|
|||||||
|
|
||||||
use brahman_broker::{Broker, Endpoint};
|
use brahman_broker::{Broker, Endpoint};
|
||||||
use brahman_card::{Card, ResolvedCard, WitInterface, CARD_SCHEMA_VERSION};
|
use brahman_card::{Card, ResolvedCard, WitInterface, CARD_SCHEMA_VERSION};
|
||||||
use brahman_net::BrahmanNet;
|
use brahman_net::{BrahmanNet, PeerId};
|
||||||
use tokio::io::{split, AsyncRead, AsyncWrite, WriteHalf};
|
use tokio::io::{split, AsyncRead, AsyncWrite, WriteHalf};
|
||||||
use tokio::net::{UnixListener, UnixStream};
|
use tokio::net::{UnixListener, UnixStream};
|
||||||
use tokio::sync::{mpsc, Mutex};
|
use tokio::sync::{mpsc, Mutex};
|
||||||
@@ -118,22 +118,44 @@ impl Server {
|
|||||||
|
|
||||||
/// Acepta UNA conexión Unix, devuelve la `Session` lista para `handle()`.
|
/// Acepta UNA conexión Unix, devuelve la `Session` lista para `handle()`.
|
||||||
/// No corre el handler — eso es responsabilidad del llamante.
|
/// No corre el handler — eso es responsabilidad del llamante.
|
||||||
|
/// Path Unix → `expected_peer = None` (firma del Hello opcional;
|
||||||
|
/// SO_PEERCRED del kernel ya autentica al cliente).
|
||||||
pub async fn accept_one(&self) -> std::io::Result<Session<UnixStream>> {
|
pub async fn accept_one(&self) -> std::io::Result<Session<UnixStream>> {
|
||||||
let (stream, _addr) = self.listener.accept().await?;
|
let (stream, _addr) = self.listener.accept().await?;
|
||||||
Ok(self.session_from_stream(stream))
|
Ok(self.session_from_stream(stream))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Construye una `Session` a partir de un stream arbitrario que
|
/// Construye una `Session` a partir de un stream arbitrario que
|
||||||
/// implemente `AsyncRead + AsyncWrite + Unpin + Send`. Es el
|
/// implemente `AsyncRead + AsyncWrite + Unpin + Send`. Path
|
||||||
/// punto de entrada para transports alternativos (libp2p en
|
/// agnóstico al transport (Unix, in-memory, etc.) — `expected_peer`
|
||||||
/// `brahman_handshake::network`, in-memory para tests, etc.) que
|
/// queda en `None`, así que la firma del Hello es opcional.
|
||||||
/// quieren reutilizar la lógica del handshake sin venir por el
|
|
||||||
/// listener Unix.
|
|
||||||
///
|
|
||||||
/// Las tablas compartidas (sessions/push/last_matches/broker) se
|
|
||||||
/// clonan, así que sesiones construidas por esta vía conviven
|
|
||||||
/// indistinguibles en el mismo `Server`.
|
|
||||||
pub fn session_from_stream<S>(&self, stream: S) -> Session<S>
|
pub fn session_from_stream<S>(&self, stream: S) -> Session<S>
|
||||||
|
where
|
||||||
|
S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
|
||||||
|
{
|
||||||
|
self.session_from_stream_inner(stream, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Variante para conexiones libp2p: el `peer_id` viene autenticado
|
||||||
|
/// por Noise. La sesión exige firma del Hello cuya public key
|
||||||
|
/// derive a este `peer_id` exacto. Ver
|
||||||
|
/// [`super::network::run_libp2p_accept_loop`].
|
||||||
|
pub fn session_from_libp2p_stream<S>(
|
||||||
|
&self,
|
||||||
|
stream: S,
|
||||||
|
peer: PeerId,
|
||||||
|
) -> Session<S>
|
||||||
|
where
|
||||||
|
S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
|
||||||
|
{
|
||||||
|
self.session_from_stream_inner(stream, Some(peer))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn session_from_stream_inner<S>(
|
||||||
|
&self,
|
||||||
|
stream: S,
|
||||||
|
expected_peer: Option<PeerId>,
|
||||||
|
) -> Session<S>
|
||||||
where
|
where
|
||||||
S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
|
S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
|
||||||
{
|
{
|
||||||
@@ -143,6 +165,7 @@ impl Server {
|
|||||||
push_table: self.push_table.clone(),
|
push_table: self.push_table.clone(),
|
||||||
last_matches: self.last_matches.clone(),
|
last_matches: self.last_matches.clone(),
|
||||||
config: self.config.clone(),
|
config: self.config.clone(),
|
||||||
|
expected_peer,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,6 +204,11 @@ pub struct Session<S> {
|
|||||||
push_table: SessionTxTable,
|
push_table: SessionTxTable,
|
||||||
last_matches: LastMatches,
|
last_matches: LastMatches,
|
||||||
config: ServerConfig,
|
config: ServerConfig,
|
||||||
|
/// Si está set, el path es libp2p y `do_handshake` exige firma
|
||||||
|
/// del Hello cuya public key derive a este `peer_id`. Si es
|
||||||
|
/// `None`, el path es Unix/in-memory y la firma es opcional
|
||||||
|
/// (pero si está, se verifica anyway por defensa en profundidad).
|
||||||
|
expected_peer: Option<PeerId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S> Session<S>
|
impl<S> Session<S>
|
||||||
@@ -207,9 +235,11 @@ where
|
|||||||
push_table,
|
push_table,
|
||||||
last_matches,
|
last_matches,
|
||||||
config,
|
config,
|
||||||
|
expected_peer,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
let session_id = match do_handshake(&mut stream, &config, &sessions).await? {
|
let session_id = match do_handshake(&mut stream, &config, &sessions, expected_peer).await?
|
||||||
|
{
|
||||||
Some(id) => id,
|
Some(id) => id,
|
||||||
None => return Ok(()), // Hello rechazado, no se registró nada
|
None => return Ok(()), // Hello rechazado, no se registró nada
|
||||||
};
|
};
|
||||||
@@ -473,11 +503,13 @@ async fn broadcast_match_diffs(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lee el Hello, valida, registra la sesión y emite HelloAck.
|
/// Lee el Hello, valida (incluyendo firma cuando aplica), registra la
|
||||||
|
/// sesión y emite HelloAck.
|
||||||
async fn do_handshake<S>(
|
async fn do_handshake<S>(
|
||||||
stream: &mut S,
|
stream: &mut S,
|
||||||
config: &ServerConfig,
|
config: &ServerConfig,
|
||||||
sessions: &SessionRegistry,
|
sessions: &SessionRegistry,
|
||||||
|
expected_peer: Option<PeerId>,
|
||||||
) -> std::io::Result<Option<SessionId>>
|
) -> std::io::Result<Option<SessionId>>
|
||||||
where
|
where
|
||||||
S: AsyncRead + AsyncWrite + Unpin,
|
S: AsyncRead + AsyncWrite + Unpin,
|
||||||
@@ -502,6 +534,84 @@ where
|
|||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Trust gate: en path libp2p (expected_peer = Some), exigir
|
||||||
|
// firma cuya public key derive al peer autenticado por Noise.
|
||||||
|
// En path Unix (expected_peer = None), si la firma viene se
|
||||||
|
// verifica anyway por defensa en profundidad — no es un error
|
||||||
|
// que esté ahí, pero si está debe ser válida.
|
||||||
|
if let Some(peer) = expected_peer {
|
||||||
|
let sig = match &hello.signature {
|
||||||
|
Some(s) => s,
|
||||||
|
None => {
|
||||||
|
write_frame(
|
||||||
|
stream,
|
||||||
|
&Frame::Error(HandshakeError::Unauthorized(
|
||||||
|
"Hello sin firma en conexión remota libp2p".into(),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Err(e) = crate::signature::verify_hello(sig, &hello.card, &hello.wit, peer) {
|
||||||
|
write_frame(
|
||||||
|
stream,
|
||||||
|
&Frame::Error(HandshakeError::Unauthorized(format!("firma inválida: {e}"))),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
debug!(peer = %peer, error = %e, "firma rechazada");
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
} else if let Some(sig) = &hello.signature {
|
||||||
|
// Firma presente en path local: no exigida pero verificada.
|
||||||
|
// Si está y no valida, es un signo de Hello mal-construido y
|
||||||
|
// rechazamos por seguridad.
|
||||||
|
// Para Unix no tenemos peer_id contra el cual comparar; se
|
||||||
|
// verifica sólo la consistencia interna (firma sobre payload
|
||||||
|
// con la public_key declarada).
|
||||||
|
match brahman_net::PublicKey::try_decode_protobuf(&sig.public_key) {
|
||||||
|
Ok(pk) => {
|
||||||
|
let payload = match postcard::to_allocvec(&(
|
||||||
|
crate::signature::SIGNATURE_VERSION,
|
||||||
|
&hello.card,
|
||||||
|
&hello.wit,
|
||||||
|
)) {
|
||||||
|
Ok(b) => b,
|
||||||
|
Err(_) => {
|
||||||
|
write_frame(
|
||||||
|
stream,
|
||||||
|
&Frame::Error(HandshakeError::Internal(
|
||||||
|
"no pude codificar payload para verificar firma".into(),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if !pk.verify(&payload, &sig.signature) {
|
||||||
|
write_frame(
|
||||||
|
stream,
|
||||||
|
&Frame::Error(HandshakeError::Unauthorized(
|
||||||
|
"firma del Hello presente pero inválida".into(),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
write_frame(
|
||||||
|
stream,
|
||||||
|
&Frame::Error(HandshakeError::Unauthorized(format!(
|
||||||
|
"public_key inválida en firma: {e}"
|
||||||
|
))),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let session_id = Ulid::new();
|
let session_id = Ulid::new();
|
||||||
let card: Card = hello.card.into();
|
let card: Card = hello.card.into();
|
||||||
register_session(session_id, card, hello.wit, config, sessions).await;
|
register_session(session_id, card, hello.wit, config, sessions).await;
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
//! Firma y verificación del payload del `Hello` para trust remoto.
|
||||||
|
//!
|
||||||
|
//! Usa la identidad Ed25519 de libp2p (la misma keypair que el peer
|
||||||
|
//! presenta al swarm vía Noise). Esto ancla la identidad criptográfica
|
||||||
|
//! del Ente a la identidad de transporte: si Noise autenticó al
|
||||||
|
//! `peer_id` X, sólo X puede firmar Cards válidas para esa conexión.
|
||||||
|
//!
|
||||||
|
//! ## Payload firmado
|
||||||
|
//!
|
||||||
|
//! Bytes postcard de la tupla `(WireCard, Option<WitInterface>)`. Se
|
||||||
|
//! eligió postcard porque ya es el wire format del resto del protocolo:
|
||||||
|
//! mismo determinismo, sin convertir a otro formato sólo para firmar.
|
||||||
|
//!
|
||||||
|
//! Cualquier campo que entre al payload firmado en el futuro debe
|
||||||
|
//! añadirse al final de la tupla (postcard es position-dependent), o
|
||||||
|
//! bumpearse el [`SIGNATURE_VERSION`] para distinguir esquemas.
|
||||||
|
|
||||||
|
use brahman_card::{WireCard, WitInterface};
|
||||||
|
use brahman_net::{Keypair, PeerId, PublicKey};
|
||||||
|
|
||||||
|
use crate::messages::HelloSignature;
|
||||||
|
|
||||||
|
/// Versión del esquema de payload firmado. Si cambia el shape de
|
||||||
|
/// `(WireCard, Option<WitInterface>)` o cómo se serializa, bump este
|
||||||
|
/// número y el verificador rechaza firmas antiguas.
|
||||||
|
pub const SIGNATURE_VERSION: u8 = 1;
|
||||||
|
|
||||||
|
/// Errores de verificación de firma.
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum SignatureError {
|
||||||
|
#[error("public_key inválida (libp2p decode protobuf): {0}")]
|
||||||
|
DecodeKey(String),
|
||||||
|
#[error("encode del payload falló: {0}")]
|
||||||
|
EncodePayload(String),
|
||||||
|
#[error("firma rechazada: bytes inválidos para la public_key")]
|
||||||
|
Invalid,
|
||||||
|
#[error("peer_id de la firma ({signer}) no coincide con el peer libp2p autenticado ({expected})")]
|
||||||
|
PeerMismatch { signer: PeerId, expected: PeerId },
|
||||||
|
#[error("firma del Hello faltante (requerida para conexión remota libp2p)")]
|
||||||
|
Missing,
|
||||||
|
#[error("firma del Hello inesperada en path local sin trust remoto")]
|
||||||
|
Unexpected,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construye los bytes canónicos a firmar/verificar para un Hello.
|
||||||
|
/// Postcard determinístico de `(version, WireCard, Option<WitInterface>)`.
|
||||||
|
fn payload_bytes(card: &WireCard, wit: &Option<WitInterface>) -> Result<Vec<u8>, SignatureError> {
|
||||||
|
let tup = (SIGNATURE_VERSION, card, wit);
|
||||||
|
postcard::to_allocvec(&tup).map_err(|e| SignatureError::EncodePayload(e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Firma `(card, wit)` con la `keypair`. La public key derivada de
|
||||||
|
/// `keypair` debe coincidir con la identidad libp2p del peer cuando
|
||||||
|
/// el verificador la chequee.
|
||||||
|
pub fn sign_hello(
|
||||||
|
keypair: &Keypair,
|
||||||
|
card: &WireCard,
|
||||||
|
wit: &Option<WitInterface>,
|
||||||
|
) -> Result<HelloSignature, SignatureError> {
|
||||||
|
let bytes = payload_bytes(card, wit)?;
|
||||||
|
let signature_bytes = keypair
|
||||||
|
.sign(&bytes)
|
||||||
|
.map_err(|e| SignatureError::EncodePayload(e.to_string()))?;
|
||||||
|
Ok(HelloSignature {
|
||||||
|
public_key: keypair.public().encode_protobuf(),
|
||||||
|
signature: signature_bytes,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verifica que `sig` es una firma válida sobre `(card, wit)` y que
|
||||||
|
/// la public key declarada coincide con `expected_peer` (la identidad
|
||||||
|
/// libp2p autenticada por Noise).
|
||||||
|
///
|
||||||
|
/// Devuelve `Ok(())` si todo cuadra; si no, el error concreto.
|
||||||
|
pub fn verify_hello(
|
||||||
|
sig: &HelloSignature,
|
||||||
|
card: &WireCard,
|
||||||
|
wit: &Option<WitInterface>,
|
||||||
|
expected_peer: PeerId,
|
||||||
|
) -> Result<(), SignatureError> {
|
||||||
|
let public_key = PublicKey::try_decode_protobuf(&sig.public_key)
|
||||||
|
.map_err(|e| SignatureError::DecodeKey(e.to_string()))?;
|
||||||
|
let signer_peer = public_key.to_peer_id();
|
||||||
|
if signer_peer != expected_peer {
|
||||||
|
return Err(SignatureError::PeerMismatch {
|
||||||
|
signer: signer_peer,
|
||||||
|
expected: expected_peer,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let bytes = payload_bytes(card, wit)?;
|
||||||
|
if !public_key.verify(&bytes, &sig.signature) {
|
||||||
|
return Err(SignatureError::Invalid);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use brahman_card::Card;
|
||||||
|
|
||||||
|
fn sample_card() -> WireCard {
|
||||||
|
Card::new("test.signed").into()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sign_then_verify_roundtrip() {
|
||||||
|
let kp = Keypair::generate_ed25519();
|
||||||
|
let peer = kp.public().to_peer_id();
|
||||||
|
let card = sample_card();
|
||||||
|
let wit = None;
|
||||||
|
let sig = sign_hello(&kp, &card, &wit).unwrap();
|
||||||
|
verify_hello(&sig, &card, &wit, peer).expect("firma propia debe verificar");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn verify_rejects_wrong_peer() {
|
||||||
|
let kp = Keypair::generate_ed25519();
|
||||||
|
let other = Keypair::generate_ed25519().public().to_peer_id();
|
||||||
|
let card = sample_card();
|
||||||
|
let wit = None;
|
||||||
|
let sig = sign_hello(&kp, &card, &wit).unwrap();
|
||||||
|
let err = verify_hello(&sig, &card, &wit, other).unwrap_err();
|
||||||
|
assert!(matches!(err, SignatureError::PeerMismatch { .. }), "got {err:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn verify_rejects_tampered_card() {
|
||||||
|
let kp = Keypair::generate_ed25519();
|
||||||
|
let peer = kp.public().to_peer_id();
|
||||||
|
let original = sample_card();
|
||||||
|
let wit = None;
|
||||||
|
let sig = sign_hello(&kp, &original, &wit).unwrap();
|
||||||
|
|
||||||
|
// Verificamos contra una Card distinta (mismo shape, distinto label).
|
||||||
|
let tampered: WireCard = Card::new("test.tampered").into();
|
||||||
|
let err = verify_hello(&sig, &tampered, &wit, peer).unwrap_err();
|
||||||
|
assert!(matches!(err, SignatureError::Invalid), "got {err:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn verify_rejects_corrupted_signature() {
|
||||||
|
let kp = Keypair::generate_ed25519();
|
||||||
|
let peer = kp.public().to_peer_id();
|
||||||
|
let card = sample_card();
|
||||||
|
let wit = None;
|
||||||
|
let mut sig = sign_hello(&kp, &card, &wit).unwrap();
|
||||||
|
// Flip un bit de la firma.
|
||||||
|
if let Some(b) = sig.signature.last_mut() {
|
||||||
|
*b ^= 0x01;
|
||||||
|
}
|
||||||
|
let err = verify_hello(&sig, &card, &wit, peer).unwrap_err();
|
||||||
|
assert!(matches!(err, SignatureError::Invalid), "got {err:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -125,6 +125,7 @@ async fn server_rejects_protocol_mismatch() {
|
|||||||
protocol_version: "999.0.0".into(),
|
protocol_version: "999.0.0".into(),
|
||||||
card: sample_card("future-module").into(),
|
card: sample_card("future-module").into(),
|
||||||
wit: None,
|
wit: None,
|
||||||
|
signature: None,
|
||||||
};
|
};
|
||||||
write_frame(&mut stream, &Frame::Hello(hello)).await.unwrap();
|
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::network::{connect_libp2p, run_libp2p_accept_loop};
|
||||||
use brahman_handshake::server::{Server, ServerConfig};
|
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 tempfile::TempDir;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
@@ -86,7 +86,8 @@ async fn libp2p_handshake_roundtrip() {
|
|||||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||||
|
|
||||||
let card = sample_card("test.remote_ente");
|
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
|
.await
|
||||||
.expect("handshake remoto debería completar");
|
.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.
|
// peer_id no usado aquí, pero validamos que la API existe.
|
||||||
let _ = PeerId::random();
|
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");
|
||||||
|
}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@
|
|||||||
#![warn(rust_2018_idioms)]
|
#![warn(rust_2018_idioms)]
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
@@ -53,7 +54,11 @@ use libp2p::{
|
|||||||
use libp2p_stream as stream;
|
use libp2p_stream as stream;
|
||||||
use tokio::sync::{mpsc, oneshot, Mutex};
|
use tokio::sync::{mpsc, oneshot, Mutex};
|
||||||
|
|
||||||
pub use libp2p::{multiaddr::Protocol, Multiaddr, PeerId, Stream, StreamProtocol};
|
pub use libp2p::{
|
||||||
|
identity::{Keypair, PublicKey},
|
||||||
|
multiaddr::Protocol,
|
||||||
|
Multiaddr, PeerId, Stream, StreamProtocol,
|
||||||
|
};
|
||||||
pub use libp2p_stream::OpenStreamError;
|
pub use libp2p_stream::OpenStreamError;
|
||||||
|
|
||||||
const IDENTIFY_PROTOCOL: &str = "/brahman-net/0.1.0";
|
const IDENTIFY_PROTOCOL: &str = "/brahman-net/0.1.0";
|
||||||
@@ -96,6 +101,11 @@ pub struct BrahmanNet {
|
|||||||
/// keypair (efímera por default; persistente si pasaste una
|
/// keypair (efímera por default; persistente si pasaste una
|
||||||
/// vía [`with_keypair`]).
|
/// vía [`with_keypair`]).
|
||||||
pub peer_id: PeerId,
|
pub peer_id: PeerId,
|
||||||
|
/// Keypair compartida (Arc para compartir con consumers que
|
||||||
|
/// necesitan firmar mensajes con la misma identidad — p. ej.
|
||||||
|
/// `brahman_handshake::network::connect_libp2p` que firma el
|
||||||
|
/// Hello). NO se expone públicamente; usar [`Self::keypair`].
|
||||||
|
keypair: Arc<Keypair>,
|
||||||
cmd_tx: mpsc::UnboundedSender<Command>,
|
cmd_tx: mpsc::UnboundedSender<Command>,
|
||||||
listen_rx: Mutex<mpsc::UnboundedReceiver<Multiaddr>>,
|
listen_rx: Mutex<mpsc::UnboundedReceiver<Multiaddr>>,
|
||||||
/// Control para abrir y aceptar streams. Cada protocolo
|
/// Control para abrir y aceptar streams. Cada protocolo
|
||||||
@@ -117,10 +127,21 @@ impl BrahmanNet {
|
|||||||
/// `peer_id` estable (por ejemplo si tu identidad se persiste a
|
/// `peer_id` estable (por ejemplo si tu identidad se persiste a
|
||||||
/// disco, o si la derivás de la identidad criptográfica del
|
/// disco, o si la derivás de la identidad criptográfica del
|
||||||
/// módulo).
|
/// módulo).
|
||||||
|
///
|
||||||
|
/// Sólo Ed25519 se soporta — la `keypair` se duplica internamente
|
||||||
|
/// vía clone del `ed25519::Keypair` para que tanto el swarm
|
||||||
|
/// (Noise auth) como el caller (firma de Cards) compartan la
|
||||||
|
/// misma identidad sin la fricción de que `identity::Keypair` no
|
||||||
|
/// implemente `Clone`.
|
||||||
pub fn with_keypair(keypair: identity::Keypair) -> Result<Self, NodeError> {
|
pub fn with_keypair(keypair: identity::Keypair) -> Result<Self, NodeError> {
|
||||||
let peer_id = keypair.public().to_peer_id();
|
let ed_kp = keypair
|
||||||
|
.try_into_ed25519()
|
||||||
|
.map_err(|_| NodeError::Build("brahman-net sólo soporta keypairs Ed25519".into()))?;
|
||||||
|
let kp_for_swarm = identity::Keypair::from(ed_kp.clone());
|
||||||
|
let kp_for_storage = Arc::new(identity::Keypair::from(ed_kp));
|
||||||
|
let peer_id = kp_for_swarm.public().to_peer_id();
|
||||||
|
|
||||||
let mut swarm: Swarm<BrahmanBehaviour> = SwarmBuilder::with_existing_identity(keypair)
|
let mut swarm: Swarm<BrahmanBehaviour> = SwarmBuilder::with_existing_identity(kp_for_swarm)
|
||||||
.with_tokio()
|
.with_tokio()
|
||||||
.with_tcp(
|
.with_tcp(
|
||||||
tcp::Config::default(),
|
tcp::Config::default(),
|
||||||
@@ -274,12 +295,21 @@ impl BrahmanNet {
|
|||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
peer_id,
|
peer_id,
|
||||||
|
keypair: kp_for_storage,
|
||||||
cmd_tx,
|
cmd_tx,
|
||||||
listen_rx: Mutex::new(listen_rx),
|
listen_rx: Mutex::new(listen_rx),
|
||||||
control,
|
control,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Acceso a la keypair de identidad del nodo. Usar para firmar
|
||||||
|
/// payloads que viajan asociados al `peer_id` (handshake brahman
|
||||||
|
/// firmado, futuros sub-protocolos con autenticación). El `Arc`
|
||||||
|
/// permite compartir sin copia — la keypair libp2p no es `Clone`.
|
||||||
|
pub fn keypair(&self) -> Arc<Keypair> {
|
||||||
|
self.keypair.clone()
|
||||||
|
}
|
||||||
|
|
||||||
/// Empieza a escuchar en `addr`. Bloquea hasta que el listener
|
/// Empieza a escuchar en `addr`. Bloquea hasta que el listener
|
||||||
/// publique su dirección real (Multiaddr resuelta — útil cuando
|
/// publique su dirección real (Multiaddr resuelta — útil cuando
|
||||||
/// pediste `/ip4/0.0.0.0/tcp/0` y querés saber qué puerto te tocó).
|
/// pediste `/ip4/0.0.0.0/tcp/0` y querés saber qué puerto te tocó).
|
||||||
|
|||||||
Reference in New Issue
Block a user