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:
Sergio
2026-05-09 14:50:04 +00:00
parent 2059af4fb9
commit c164e9f422
10 changed files with 541 additions and 27 deletions
+94
View File
@@ -6,6 +6,100 @@ ratio/diff ver `git show <sha>`.
## 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
Tercer paso del plan "el encuentro entre Entes no se restringe a
local". Cuando un Init local acepta una sesión cuya Card declara