PWR Túnel
Proxy cifrado post-cuantico para trafico HTTP entre sedes. Cifra peticiones y respuestas con doble capa criptografica (ChaCha20-Poly1305 + AES-256-GCM) y handshake ML-KEM-768 (Kyber) para resistencia ante ordenadores cuanticos. PHP puro, sin dependencias externas.
Instalacion
Túnel es un unico archivo PHP. Copialo a tu proyecto e incluyelo donde necesites cifrar trafico HTTP entre sedes.
require_once __DIR__ . '/core/tunel.php';
Requisitos minimos
- PHP 7.4 o superior
- Extension
sodium(incluida por defecto en PHP 7.2+) - Extension
opensslcon soporte AES-256-GCM - Opcional:
ext-ffi+ liboqs para ML-KEM-768 nativo
Uso rapido
Flujo completo en 5 pasos: generar claves, handshake, crear sesion, empaquetar request, desempaquetar en destino.
require_once 'core/tunel.php';
// 1. Cada sede genera sus claves
$sede_a = pwr_tunel_generar_claves();
$sede_b = pwr_tunel_generar_claves();
// 2. Handshake: A encapsula con pk de B
$hs = pwr_tunel_handshake($sede_b['pk']);
// 3. B decapsula y obtiene el mismo shared_secret
$dec = pwr_tunel_decapsular($sede_b['sk'], $hs['ciphertext']);
// 4. Ambos crean sesion con el shared_secret
$sesion = pwr_tunel_crear_sesion($hs['shared_secret']);
// 5. A envia request cifrado a B
$paquete = pwr_tunel_empaquetar_request(
'POST',
'https://sede-b.ejemplo.es/api/datos',
['Content-Type' => 'application/json'],
'{"expediente":"2026/001"}',
$sesion
);
// B desempaqueta
$request = pwr_tunel_desempaquetar_request(
$paquete['paquete'],
$sesion
);
Generar claves
Cada sede genera su propio par de claves (publica y secreta). La clave publica se comparte con la otra sede; la secreta se guarda en local.
pwr_tunel_generar_claves()
pwr_tunel_generar_claves(): array
Retorna:
| Clave | Tipo | Descripcion |
|---|---|---|
ok | bool | Siempre true |
pk | string | Clave publica (base64) |
sk | string | Clave secreta (base64) |
modo | string | ml_kem_768 o simulado_x25519 |
$claves = pwr_tunel_generar_claves();
// Guardar la pk para enviarla a la otra sede
file_put_contents('mi_pk.b64', $claves['pk']);
// Guardar la sk en lugar seguro (nunca compartir)
file_put_contents('mi_sk.b64', $claves['sk']);
sk) nunca debe salir del servidor.
Solo comparte la clave publica (pk) con la sede remota.
Handshake PQC
El handshake establece un secreto compartido entre ambas sedes.
Utiliza ML-KEM-768 (Kyber) si liboqs esta disponible, o X25519 + HKDF como fallback.
En ambos casos, el resultado es un shared_secret de 32 bytes.
pwr_tunel_handshake()
La sede origen encapsula un secreto con la clave publica del destino.
pwr_tunel_handshake(string $pk_destino): array
| Clave | Tipo | Descripcion |
|---|---|---|
ok | bool | Exito o fallo |
shared_secret | string | Secreto compartido (base64, 32 bytes) |
ciphertext | string | Ciphertext para enviar al destino (base64) |
modo | string | Algoritmo utilizado |
pwr_tunel_decapsular()
La sede destino recupera el secreto compartido con su clave secreta.
pwr_tunel_decapsular(string $sk, string $ciphertext): array
// Sede A: encapsula con pk de B
$hs = pwr_tunel_handshake($pk_sede_b);
// Enviar $hs['ciphertext'] a la sede B
// Sede B: decapsula con su sk
$dec = pwr_tunel_decapsular($sk_sede_b, $hs['ciphertext']);
// Ambos tienen el mismo shared_secret
// $hs['shared_secret'] === $dec['shared_secret']
Sesiones
Una sesion encapsula las claves simetricas derivadas del handshake, junto con metadatos de control (TTL, contadores, estado).
pwr_tunel_crear_sesion()
pwr_tunel_crear_sesion(string $shared_secret, array $opciones = []): array
| Opcion | Tipo | Defecto | Descripcion |
|---|---|---|---|
ttl | int | 3600 | Tiempo de vida en segundos |
id | string | aleatorio | Identificador de sesion |
La sesion contiene:
clave_chacha— Clave para la capa ChaCha20-Poly1305clave_aes— Clave para la capa AES-256-GCMsal— Sal utilizada en la derivacioncreada,expira,ttl— Control temporalbytes_enviados,bytes_recibidos,paquetes— Contadoresrotaciones— Numero de rotaciones de claves realizadas
pwr_tunel_estado()
Consulta el estado actual de una sesion.
pwr_tunel_estado(array $sesion): array
Retorna: activa (bool), razon (string), ttl_restante (int), bytes_total (int), paquetes (int), rotaciones (int).
Cifrar / Descifrar
El cifrado usa doble capa: primero ChaCha20-Poly1305 (libsodium), despues AES-256-GCM (OpenSSL). El descifrado es en orden inverso. Cada capa usa su propia clave, nonce/IV y datos asociados (AD) para autenticacion AEAD.
pwr_tunel_cifrar()
pwr_tunel_cifrar(string $datos, array &$sesion): array
pwr_tunel_descifrar()
pwr_tunel_descifrar(array $datos_cifrados, array &$sesion): array
$texto = 'Datos sensibles del expediente municipal';
// Cifrar con doble capa
$cifrado = pwr_tunel_cifrar($texto, $sesion);
// Descifrar
$descifrado = pwr_tunel_descifrar($cifrado, $sesion);
// $descifrado['datos'] === $texto
Empaquetado HTTP
Las funciones de empaquetado serializan peticiones y respuestas HTTP completas (metodo, URL, headers, body) en un paquete cifrado que puede transmitirse por cualquier canal.
pwr_tunel_empaquetar_request()
pwr_tunel_empaquetar_request(
string $method,
string $url,
array $headers,
string $body,
array &$sesion
): array
pwr_tunel_desempaquetar_request()
pwr_tunel_desempaquetar_request(string $paquete, array &$sesion): array
pwr_tunel_empaquetar_response()
pwr_tunel_empaquetar_response(
int $status,
array $headers,
string $body,
array &$sesion
): array
pwr_tunel_desempaquetar_response()
pwr_tunel_desempaquetar_response(string $paquete, array &$sesion): array
// Sede A envia request
$req = pwr_tunel_empaquetar_request(
'POST',
'https://sede-b.palma.es/api/registro',
['Content-Type' => 'application/json'],
'{"ciudadano":"12345678A"}',
$sesion
);
// Enviar $req['paquete'] por HTTPS a la sede B
// Sede B recibe y desempaqueta
$request = pwr_tunel_desempaquetar_request($paquete_recibido, $sesion);
// $request['method'] === 'POST'
// $request['body'] === '{"ciudadano":"12345678A"}'
// Sede B responde
$res = pwr_tunel_empaquetar_response(
201,
['Content-Type' => 'application/json'],
'{"ok":true,"id":"REG-001"}',
$sesion
);
Rotacion de claves
La rotacion de claves permite renovar las claves simetricas de la sesion sin necesidad de repetir el handshake. Deriva nuevas claves a partir del shared_secret original y una sal fresca, garantizando forward secrecy parcial dentro de la sesion.
pwr_tunel_rotar_claves()
pwr_tunel_rotar_claves(array &$sesion): array
// Rotar claves cada N mensajes o periodicamente
if ($sesion['paquetes'] % 100 === 0) {
$rot = pwr_tunel_rotar_claves($sesion);
// $rot['rotacion'] indica el numero de rotacion
}
rotate)
para coordinar la rotacion.
API de funciones
Resumen de todas las funciones publicas del modulo Túnel.
| Funcion | Descripcion |
|---|---|
pwr_tunel_info() | Informacion del modulo: version, capacidades, algoritmos |
pwr_tunel_generar_claves() | Genera par de claves ML-KEM-768 (o X25519 fallback) |
pwr_tunel_handshake($pk) | Encapsula shared_secret con clave publica destino |
pwr_tunel_decapsular($sk, $ct) | Recupera shared_secret con clave secreta propia |
pwr_tunel_crear_sesion($ss, $opts) | Crea sesion con claves derivadas del shared_secret |
pwr_tunel_cifrar($datos, &$sesion) | Cifra con doble capa ChaCha20 + AES-256 |
pwr_tunel_descifrar($cifrado, &$sesion) | Descifra en orden inverso AES-256 + ChaCha20 |
pwr_tunel_empaquetar_request(...) | Empaqueta y cifra peticion HTTP completa |
pwr_tunel_desempaquetar_request(...) | Descifra y desempaqueta peticion HTTP |
pwr_tunel_empaquetar_response(...) | Empaqueta y cifra respuesta HTTP completa |
pwr_tunel_desempaquetar_response(...) | Descifra y desempaqueta respuesta HTTP |
pwr_tunel_estado($sesion) | Verifica estado de sesion (activa, expirada, bytes) |
pwr_tunel_rotar_claves(&$sesion) | Rota claves simetricas sin repetir handshake |
Arquitectura
PWR Túnel implementa un modelo de proxy cifrado punto a punto entre sedes, con tres capas de seguridad independientes.
Flujo de datos
Sede A Sede B
| |
| 1. Generar claves (pk_a, sk_a) 1. Generar claves (pk_b, sk_b)
| |
| 2. Intercambiar pks |
| ─────── pk_a ───────────────> |
| <─────── pk_b ─────────────── |
| |
| 3. Handshake PQC |
| encapsular(pk_b) → ss + ct |
| ─────── ct ─────────────────> |
| decapsular(sk_b, ct) → ss
| |
| 4. Crear sesion(ss) 4. Crear sesion(ss)
| |
| 5. Empaquetar request |
| [HTTP req] → cifrar(2 capas) |
| ─── paquete cifrado ────────> |
| desempaquetar → [HTTP req]
| |
| 6. Empaquetar response
| descifrar ← [paquete] ── |
| |
Capas de cifrado
| Capa | Algoritmo | Libreria | Funcion |
|---|---|---|---|
| 1 (interna) | ChaCha20-Poly1305 IETF | libsodium | Cifrado autenticado AEAD con nonce de 12 bytes |
| 2 (externa) | AES-256-GCM | OpenSSL | Cifrado autenticado AEAD con IV de 12 bytes + tag de 16 bytes |
| KEM | ML-KEM-768 / X25519 | liboqs / libsodium | Intercambio de claves post-cuantico |
Derivacion de claves
Las claves simetricas se derivan del shared_secret mediante
HKDF (RFC 5869) con SHA-256, usando contextos independientes para cada capa:
pwr_tunel_chacha_v1— Contexto para la clave ChaCha20-Poly1305pwr_tunel_aes_v1— Contexto para la clave AES-256-GCM