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
|
||||
|
||||
### 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
|
||||
|
||||
Reference in New Issue
Block a user