- 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>
538 lines
20 KiB
Markdown
538 lines
20 KiB
Markdown
# 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
|
|
```
|