zcatp2p/PROTOCOL.md
reugenio 7e5b16ee15 Inicial: biblioteca zcatp2p para comunicación P2P segura
- Especificación completa del protocolo (PROTOCOL.md)
- Referencia de API (API.md)
- Implementación crypto: SHA256, ChaCha20-Poly1305
- Device ID con Base32 y verificación Luhn32
- Framing de mensajes (HELLO, PING, DATA, etc.)
- Discovery local UDP broadcast
- Estructura de conexiones y estados
- Build system para Zig 0.15.2

Pendiente: TLS 1.3, STUN, Global Discovery HTTPS, Relay

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-15 01:06:30 +01:00

20 KiB

Protocolo zcatp2p - Especificación Técnica v1.0

1. Visión General

zcatp2p es un protocolo de comunicación P2P para intercambio seguro de mensajes entre nodos identificados criptográficamente. Diseñado para comunicación directa entre empresas (facturas, certificados) sin intermediarios.

1.1 Principios de Diseño

  1. Seguridad primero: Todo tráfico cifrado E2E con TLS 1.3
  2. Identificación por certificado: Device ID = SHA256(certificado)
  3. Descentralizado: Funciona sin servidor central (LAN)
  4. NAT-friendly: STUN + relay para conectividad universal
  5. Sin dependencias: Implementación completa en Zig puro

1.2 Componentes del Sistema

┌──────────────────────────────────────────────────────────────┐
│                      NODO SIMIFACTU                          │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐  │
│  │  Discovery  │  │    STUN     │  │   Relay Client      │  │
│  │  (local+    │  │   Client    │  │   (fallback)        │  │
│  │   global)   │  │             │  │                     │  │
│  └──────┬──────┘  └──────┬──────┘  └──────────┬──────────┘  │
│         │                │                    │              │
│  ┌──────┴────────────────┴────────────────────┴──────────┐  │
│  │                  Connection Manager                    │  │
│  │  - Mantiene conexiones activas                        │  │
│  │  - Intenta conexión directa primero                   │  │
│  │  - Fallback a relay si falla                          │  │
│  └──────────────────────────┬────────────────────────────┘  │
│                             │                                │
│  ┌──────────────────────────┴────────────────────────────┐  │
│  │                    TLS 1.3 Layer                       │  │
│  │  - Autenticación mutua por certificado                │  │
│  │  - Device ID derivado de certificado                  │  │
│  └──────────────────────────┬────────────────────────────┘  │
│                             │                                │
│  ┌──────────────────────────┴────────────────────────────┐  │
│  │                  Protocol Layer                        │  │
│  │  - Message framing                                    │  │
│  │  - Compresión LZ4 opcional                            │  │
│  │  - Request/Response pattern                           │  │
│  └───────────────────────────────────────────────────────┘  │
│                                                              │
└──────────────────────────────────────────────────────────────┘

2. Identidad de Dispositivo (Device ID)

2.1 Generación

El Device ID es un identificador único de 32 bytes derivado del certificado TLS:

DeviceID = SHA256(DER_encoded_certificate)

2.2 Representación en String

Para facilitar el intercambio, el Device ID se representa como string Base32 con dígitos de verificación Luhn:

Formato: XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX
         └──7──┘ └──7──┘ └──7──┘ └──7──┘ └──7──┘ └──7──┘ └──7──┘ └──7──┘

Cada grupo de 7 caracteres incluye 1 dígito de verificación Luhn.
Total: 56 caracteres + 7 guiones = 63 caracteres

2.3 Algoritmo Luhn para Base32

Alfabeto Base32: ABCDEFGHIJKLMNOPQRSTUVWXYZ234567

function luhn32(s):
    factor = 1
    sum = 0
    n = 32  // base

    for char in reverse(s):
        codepoint = base32_to_int(char)
        addend = factor * codepoint
        factor = (factor == 2) ? 1 : 2
        addend = (addend / n) + (addend % n)
        sum += addend

    remainder = sum % n
    check = (n - remainder) % n
    return int_to_base32(check)

2.4 Certificado Auto-firmado

Cada nodo genera un certificado X.509 auto-firmado al inicializarse:

- Subject: CN=syncthing (o "zcatp2p" para nuestra implementación)
- Key Algorithm: ECDSA P-256 o Ed25519
- Validity: 20 años
- Serial: Random 128 bits

3. Protocolo de Transporte

3.1 Conexión TLS 1.3

Toda comunicación usa TLS 1.3 con autenticación mutua de certificados:

Cliente                                Servidor
   │                                      │
   │──────── ClientHello ────────────────>│
   │         + key_share                  │
   │         + supported_versions         │
   │                                      │
   │<─────── ServerHello ─────────────────│
   │         + key_share                  │
   │         + EncryptedExtensions        │
   │         + CertificateRequest         │
   │         + Certificate                │
   │         + CertificateVerify          │
   │         + Finished                   │
   │                                      │
   │──────── Certificate ────────────────>│
   │         + CertificateVerify          │
   │         + Finished                   │
   │                                      │
   │<═══════ Application Data ═══════════>│
   │                                      │

3.2 Cipher Suites Soportadas

En orden de preferencia:

  1. TLS_CHACHA20_POLY1305_SHA256
  2. TLS_AES_256_GCM_SHA384
  3. TLS_AES_128_GCM_SHA256

3.3 Verificación de Identidad

Después del handshake TLS, ambos extremos:

  1. Calculan SHA256 del certificado del peer
  2. Verifican que coincide con el Device ID esperado
  3. Si no coincide, cierran la conexión

4. Protocolo de Mensajes

4.1 Formato de Trama

Cada mensaje tiene el siguiente formato:

┌────────────────┬────────────────┬─────────────────────────────┐
│  Header (2B)   │  Length (4B)   │       Payload (variable)    │
└────────────────┴────────────────┴─────────────────────────────┘
     │                  │                      │
     │                  │                      └── Datos del mensaje
     │                  └── Longitud del payload en big-endian
     └── Tipo de mensaje (1B) + Flags (1B)

4.2 Tipos de Mensaje

Valor   Nombre              Descripción
─────   ──────              ───────────
0x00    HELLO               Intercambio inicial de información
0x01    PING                Keepalive
0x02    PONG                Respuesta a PING
0x03    DATA                Datos de aplicación
0x04    DATA_ACK            Confirmación de recepción
0x05    CLOSE               Cierre de conexión
0x06    ERROR               Notificación de error

4.3 Flags

Bit     Nombre              Descripción
───     ──────              ───────────
0       COMPRESSED          Payload comprimido con LZ4
1       ENCRYPTED           Payload cifrado (adicional a TLS)
2       REQUEST             Espera respuesta
3       RESPONSE            Es respuesta a REQUEST
4-7     Reserved            Para uso futuro

4.4 Mensaje HELLO

Intercambiado inmediatamente después del handshake TLS:

┌─────────────────────────────────────────────────────────────┐
│  HELLO Message                                              │
├─────────────────────────────────────────────────────────────┤
│  device_name_len: u8        │  Longitud del nombre          │
│  device_name: [N]u8         │  Nombre del dispositivo       │
│  client_name_len: u8        │  Longitud del cliente         │
│  client_name: [N]u8         │  "simifactu" o "zcatp2p"      │
│  client_version_len: u8     │  Longitud de versión          │
│  client_version: [N]u8      │  Ej: "1.0.0"                  │
│  timestamp: i64             │  Unix timestamp               │
│  capabilities: u32          │  Bitmap de capacidades        │
└─────────────────────────────────────────────────────────────┘

Capacidades (bitmap):

Bit     Capacidad
───     ─────────
0       COMPRESSION_LZ4
1       ENCRYPTION_CHACHA20
2       RELAY_SUPPORT
3       IPV6_SUPPORT

4.5 Mensaje DATA

┌─────────────────────────────────────────────────────────────┐
│  DATA Message                                               │
├─────────────────────────────────────────────────────────────┤
│  message_id: u32            │  ID único para correlación    │
│  content_type_len: u8       │  Longitud del tipo            │
│  content_type: [N]u8        │  MIME type                    │
│  data_len: u32              │  Longitud de los datos        │
│  data: [N]u8                │  Datos de aplicación          │
└─────────────────────────────────────────────────────────────┘

Content types sugeridos para Simifactu:

  • application/x-simifactu-invoice - Factura
  • application/x-simifactu-certificate - Certificado
  • application/x-simifactu-verifactu - Datos Verifactu

4.6 Mensaje CLOSE

┌─────────────────────────────────────────────────────────────┐
│  CLOSE Message                                              │
├─────────────────────────────────────────────────────────────┤
│  reason_len: u8             │  Longitud del motivo          │
│  reason: [N]u8              │  Motivo del cierre            │
└─────────────────────────────────────────────────────────────┘

5. Discovery Protocol

5.1 Local Discovery (LAN)

5.1.1 Formato de Anuncio

Enviado via UDP broadcast (IPv4) o multicast (IPv6):

Puerto: 21027
IPv4 Broadcast: 255.255.255.255:21027
IPv6 Multicast: [ff12::8384]:21027

┌─────────────────────────────────────────────────────────────┐
│  Local Announce Packet                                      │
├─────────────────────────────────────────────────────────────┤
│  magic: u32                 │  0x2EA7D90B                   │
│  device_id: [32]u8          │  SHA256 del certificado       │
│  instance_id: i64           │  ID único por ejecución       │
│  num_addresses: u8          │  Número de direcciones        │
│  addresses: [N]Address      │  Lista de direcciones         │
└─────────────────────────────────────────────────────────────┘

Address:
│  addr_len: u8               │  Longitud de la URL           │
│  addr: [N]u8                │  URL (ej: "tcp://192.168.1.5:22000") │

5.1.2 Frecuencia de Anuncios

  • Intervalo normal: 30 segundos
  • Al detectar nuevo dispositivo: inmediatamente
  • Caducidad del cache: 90 segundos

5.2 Global Discovery (Internet)

5.2.1 Servidor de Discovery

El servidor de discovery es un servicio HTTPS que:

  1. Recibe anuncios de dispositivos
  2. Responde consultas sobre dispositivos

Para funcionar en Internet, necesitas al menos un servidor de discovery. Puedes usar servidores públicos de Syncthing o ejecutar uno propio.

5.2.2 Anuncio (POST)

POST https://discovery.example.com/v2/
Content-Type: application/json
Authorization: (certificado TLS cliente)

{
    "addresses": [
        "tcp://203.0.113.45:22000",
        "relay://relay.example.com:22067/?id=XXXXX"
    ]
}

El Device ID se extrae del certificado TLS del cliente.

5.2.3 Consulta (GET)

GET https://discovery.example.com/v2/?device=XXXXXXX-XXXXXXX-...

Response:
{
    "addresses": [
        "tcp://203.0.113.45:22000",
        "relay://relay.example.com:22067/?id=XXXXX"
    ]
}

5.2.4 Headers de Control

Response Headers:
- Reannounce-After: 1800    // Segundos hasta próximo anuncio
- Retry-After: 60           // En caso de error

Status Codes:
- 200: OK
- 400: Bad Request
- 403: Forbidden (sin certificado)
- 404: Device not found
- 429: Too Many Requests

6. NAT Traversal

6.1 Estrategia de Conexión

Orden de intentos para conectar con un peer:

  1. Conexión directa TCP: Si tenemos IP directa
  2. Conexión directa con STUN: Usar IP externa descubierta
  3. Hole punching: Ambos peers intentan simultáneamente
  4. Relay: Último recurso si todo falla

6.2 STUN (Session Traversal Utilities for NAT)

Usamos STUN para descubrir nuestra IP externa y tipo de NAT.

6.2.1 Servidores STUN públicos

stun.l.google.com:19302
stun1.l.google.com:19302
stun.syncthing.net:3478

6.2.2 Tipos de NAT detectables

Tipo                    Hole-punchable?    Descripción
────                    ───────────────    ───────────
Full Cone               Sí                 Puerto externo fijo
Restricted Cone         Sí                 Requiere envío previo
Port Restricted Cone    Sí                 + puerto específico
Symmetric               No                 Puerto diferente por destino

6.3 Relay Protocol

Cuando no es posible conexión directa, usamos un servidor relay.

6.3.1 Conectarse al Relay

1. Cliente se conecta al relay via TLS
2. Envía JoinRelayRequest
3. Relay responde con SessionInvitation
4. Cliente A publica su dirección de relay en discovery
5. Cliente B consulta discovery, ve dirección relay
6. Cliente B conecta al relay, envía ConnectRequest(device_id_A)
7. Relay notifica a A, ambos reciben SessionInvitation
8. Relay hace de proxy transparente

6.3.2 Mensajes del Protocolo Relay

JoinRelayRequest:
│  token_len: u8              │
│  token: [N]u8               │  Token de autenticación (opcional)

ConnectRequest:
│  device_id: [32]u8          │  ID del dispositivo destino

SessionInvitation:
│  from: [32]u8               │  Device ID del peer
│  key: [32]u8                │  Clave de sesión
│  address: [16]u8            │  IP del relay
│  port: u16                  │  Puerto
│  is_server: bool            │  ¿Somos el "servidor"?

Response:
│  code: i32                  │  0=OK, 1=NotFound, 2=AlreadyConnected
│  message_len: u8            │
│  message: [N]u8             │

7. Cifrado Adicional (Opcional)

Además del cifrado TLS, se puede añadir cifrado a nivel de aplicación.

7.1 ChaCha20-Poly1305

Nonce size: 24 bytes (XChaCha20)
Tag size: 16 bytes
Key size: 32 bytes

Encrypted = Nonce || ChaCha20-Poly1305(Key, Nonce, Plaintext)

7.2 Derivación de Claves

Para cifrado de datos específicos:

FileKey = HKDF-SHA256(
    IKM = FolderKey || filename,
    salt = "zcatp2p",
    info = empty
)

8. Compresión

8.1 LZ4

Usamos LZ4 para compresión de mensajes grandes (>128 bytes):

Compressed = u32_be(original_size) || LZ4_compress(data)

Solo comprimimos si el resultado es al menos 3% más pequeño.

9. Keepalive y Timeouts

Intervalo de PING:         90 segundos
Timeout de recepción:     300 segundos (5 min)
Timeout de conexión:       30 segundos
Timeout de close:          10 segundos

10. Puertos por Defecto

Puerto                Uso
─────                ───
22000                Conexiones P2P directas
21027                Local discovery (UDP)
22067                Relay server
22070                Relay status (HTTP)
443                  Global discovery (HTTPS)

11. URLs de Direcciones

Formato                         Ejemplo
───────                         ───────
tcp://host:port                tcp://192.168.1.5:22000
tcp4://host:port               tcp4://192.168.1.5:22000
tcp6://host:port               tcp6://[::1]:22000
relay://host:port/?id=XXX      relay://relay.example.com:22067/?id=ABCDEFG

12. Consideraciones de Seguridad

  1. Certificados: Generar con entropía suficiente
  2. Device ID: Verificar siempre después del handshake TLS
  3. Replay attacks: Usar timestamps y nonces únicos
  4. DoS: Limitar conexiones por IP, rate limiting
  5. Man-in-the-middle: Verificar Device ID conocidos
  6. Relay: El relay ve metadatos pero NO el contenido (TLS end-to-end)

13. Ejemplo de Flujo Completo

Empresa A quiere enviar factura a Empresa B

1. A tiene el Device ID de B (intercambiado previamente, ej: QR)

2. A busca a B:
   a. Busca en cache local
   b. Envía broadcast LAN
   c. Consulta discovery global

3. A obtiene direcciones de B:
   ["tcp://203.0.113.45:22000", "relay://relay.example.com:22067/?id=B"]

4. A intenta conectar:
   a. Intenta TCP directo → falla (NAT)
   b. Intenta relay → éxito

5. Handshake TLS:
   - A presenta certificado
   - B presenta certificado
   - Ambos verifican Device IDs

6. Intercambio HELLO:
   - A envía sus capacidades
   - B responde con las suyas

7. A envía factura:
   DATA {
     message_id: 1,
     content_type: "application/x-simifactu-invoice",
     data: <factura_serializada>
   }

8. B confirma:
   DATA_ACK { message_id: 1 }

9. Cierre:
   A envía CLOSE { reason: "transfer complete" }
   B cierra conexión