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:
@@ -42,6 +42,7 @@
|
||||
#![warn(rust_2018_idioms)]
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use futures::StreamExt;
|
||||
@@ -53,7 +54,11 @@ use libp2p::{
|
||||
use libp2p_stream as stream;
|
||||
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;
|
||||
|
||||
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
|
||||
/// vía [`with_keypair`]).
|
||||
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>,
|
||||
listen_rx: Mutex<mpsc::UnboundedReceiver<Multiaddr>>,
|
||||
/// 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
|
||||
/// disco, o si la derivás de la identidad criptográfica del
|
||||
/// 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> {
|
||||
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_tcp(
|
||||
tcp::Config::default(),
|
||||
@@ -274,12 +295,21 @@ impl BrahmanNet {
|
||||
|
||||
Ok(Self {
|
||||
peer_id,
|
||||
keypair: kp_for_storage,
|
||||
cmd_tx,
|
||||
listen_rx: Mutex::new(listen_rx),
|
||||
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
|
||||
/// 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ó).
|
||||
|
||||
Reference in New Issue
Block a user