Firma cada mensaje WebSocket

TLS demuestra que el canal no se manipuló en la red. No demuestra quién produjo un mensaje, y no sobrevive una vez que el mensaje se registra, persiste o repite. Este tutorial combina sgcWebSockets y sgcSign para que cada frame que emite tu servidor — o cada frame que envía tu cliente — lleve una firma CAdES detached que el otro lado valida contra un certificado firmante conocido.

Mensajes vinculados a identidad
CAdES detached por frame
Sobrevive a replay y logging

TLS no basta

TLS protege los bytes en tránsito entre dos extremos. Una vez que un frame se registra, archiva o se reenvía a través de middleware, la evidencia TLS desaparece — sólo una firma digital sobre el propio payload sobrevive.

No repudio por mensaje

Mesas de trading, casas de apuestas, canales de comandos IoT y flujos de datos clínicos necesitan una pista de auditoría a prueba de manipulación por mensaje. Una firma CAdES detached en cada frame permite al receptor demostrar, horas o años después, exactamente quién emitió qué y cuándo.

Identidad mutua

El mTLS de WebSocket demuestra la identidad del cliente en el momento de conexión. La firma por mensaje demuestra la identidad de la aplicación por frame — útil cuando el gateway WebSocket es un proxy edge compartido y el productor real está aguas abajo.

A prueba de replay

Incluye el id de sesión WebSocket y un nonce monotónico creciente dentro del payload firmado. Un atacante de replay no puede volver a firmar el mensaje con un nonce nuevo sin acceso a la clave privada.

Forma del sobre

Envuelve cada mensaje WebSocket en un pequeño sobre JSON que lleva el payload, un nonce, un timestamp y la firma detached codificada en base64.

Contrato del sobre

El sobre se firma sobre una secuencia de bytes determinista: session_id || nonce || timestamp || payload_sha256. Usamos SHA-256 porque el payload puede ser binario (Protobuf, MessagePack) y queremos una entrada de longitud fija al firmador.

CAdES es la elección correcta aquí — produce firmas CMS/PKCS#7 compactas que codificadas en base64 son lo bastante pequeñas para caber cómodamente junto al payload. PAdES es específico de PDF y XAdES es específico de XML; ninguno encaja en un canal binario-o-JSON en tiempo real.

envelope.json
{
  "sid": "7f3a-b6e1",
  "nonce": 42,
  "ts": "2026-05-26T11:24:09Z",
  "payload": { /* the actual app message */ },
  "sig": "MIIK...base64...detached CAdES..."
}

Incluye las units y carga la clave de firma

sgcWebSockets aporta el transporte, sgcSign aporta el motor de firma. No comparten estado — los conectas en tu código de aplicación.

Cláusula uses

Tanto para cliente como para servidor necesitas los componentes WebSocket, el perfil CAdES, un proveedor de claves y el verificador. Usa un PFX en el lado cliente y un certificado de servicio de larga duración (típicamente del almacén de Windows) en el lado servidor.

uses-clauses.pas
uses
  Classes, SysUtils,
  // sgcWebSockets
  sgcWebSocket, sgcWebSocket_Classes,
  // sgcSign
  sgcSign_KeyProvider_PFX,
  sgcSign_CAdES,
  sgcSign_Profile_CAdES,
  sgcSign_Verifier,
  sgcJSON;

Lado cliente — firma antes de enviar

Engancha el flujo de salida del cliente, calcula el digest del payload junto con los metadatos de sesión, adjunta la firma CAdES y emite el sobre vía WriteData.

Envoltorio del payload

Mantén el firmador y el proveedor de claves como campos de tu form o data module. TsgcSignCAdES.SignDetached acepta un buffer de bytes y devuelve la estructura CMS detached. Construimos la secuencia canónica de bytes en BuildToBeSigned — id de sesión, nonce, timestamp ISO 8601 y SHA-256 de los bytes del payload.

Llamar a WriteData sobre TsgcWebSocketClient envía el sobre como un frame de texto normal. El verificador del lado servidor invertirá este proceso antes de pasar el payload al manejador de la aplicación.

client-send.pas
procedure TForm1.SendSignedMessage(const aPayload: string);
var
  vTBS: TBytes;
  vSig: TBytes;
  vEnvelope: ISuperObject;
begin
  Inc(FNonce);
  vTBS := BuildToBeSigned(FSessionId, FNonce, NowUTC, aPayload);

  vSig := FCAdESSigner.SignDetached(vTBS);  // CMS SignedData, bytes

  vEnvelope := SO();
  vEnvelope.S['sid']     := FSessionId;
  vEnvelope.I['nonce']   := FNonce;
  vEnvelope.S['ts']      := DateToISO8601(NowUTC, True);
  vEnvelope.S['payload'] := aPayload;
  vEnvelope.S['sig']     := TNetEncoding.Base64.EncodeBytesToString(vSig);

  WSClient.WriteData(vEnvelope.AsJSon);
end;

Lado servidor — verifica antes de despachar

Engancha el evento OnMessage del servidor, recalcula los bytes a firmar desde los campos del sobre, verifica la firma detached y sólo entonces reenvía el payload a tu lógica de negocio.

Manejador OnMessage

El manejador parsea el sobre, reconstruye la secuencia canónica de bytes y llama a TsgcSignatureVerifier.VerifyDetached con los bytes de la firma y los bytes a firmar. El verificador devuelve un informe con el sujeto firmante, el estado de la cadena y cualquier metadato de sello de tiempo.

Cachea los certificados de firmantes confiables por huella dentro del servidor — la verificación completa de cadena en cada frame se vuelve cara en conexiones de alta throughput. Tras la primera verificación sólo necesitas confirmar que la firma es válida y que la huella es una de las que ya has confiado.

server-receive.pas
procedure TFormSrv.WSServerMessage(Connection: TsgcWSConnection;
  const Text: string);
var
  vEnv: ISuperObject;
  vSig, vTBS: TBytes;
  vReport: TsgcSignatureReport;
begin
  vEnv := SO(Text);
  vSig := TNetEncoding.Base64.DecodeStringToBytes(vEnv.S['sig']);
  vTBS := BuildToBeSigned(
    vEnv.S['sid'], vEnv.I['nonce'],
    ISO8601ToDate(vEnv.S['ts'], True),
    vEnv.S['payload']);

  vReport := FVerifier.VerifyDetached(vSig, vTBS);
  if vReport.Signatures[0].Status <> svValid then
  begin
    Connection.Disconnect;
    Exit;
  end;

  HandlePayload(Connection, vEnv.S['payload']);
end;

Protección contra replay

Una firma válida no basta — un atacante que capture un sobre firmado puede repetirlo mientras el certificado siga siendo válido. Vincula cada mensaje a la sesión y exige nonces monotónicos.

Ventana de nonce por conexión

Mantén un diccionario en el servidor indexado por (SessionId, SignerThumbprint) con el nonce más alto visto hasta ahora. Rechaza cualquier sobre cuyo nonce no sea estrictamente mayor. Rechaza cualquier sobre cuyo timestamp esté a más de unos pocos segundos del reloj del servidor — acota la ventana de replay incluso si el mismo nonce se reutilizara de algún modo.

Para redes con pérdidas donde el orden no está garantizado, cambia la comprobación estricta de mayor-que por una ventana deslizante: acepta cualquier nonce dentro de las últimas N entradas que no se haya visto, rechazando todo lo más antiguo. El requisito fundamental es que ningún par de frames lleve el mismo (SessionId, Nonce).

replay.pas
function TFormSrv.AcceptNonce(const aSid: string;
  aNonce: Int64): Boolean;
var
  vLast: Int64;
begin
  FNonceLock.Acquire;
  try
    vLast := FNonces.Items[aSid];
    Result := aNonce > vLast;
    if Result then
      FNonces.AddOrSetValue(aSid, aNonce);
  finally
    FNonceLock.Release;
  end;
end;

Lo que suele fallar

Tres modos de fallo que más vemos cuando los desarrolladores integran por primera vez sgcSign con sgcWebSockets.

Canonicalización inconsistente

Si el cliente construye los bytes a firmar desde un string JSON con claves en orden de inserción y el servidor los reconstruye desde un objeto parseado y re-serializado, los digests no coincidirán. Firma siempre una secuencia de bytes determinista que construyas tú — no firmes JSON re-serializado.

Latencia en flujos de alta throughput

Firmar cada mensaje con una clave RSA-2048 añade unos pocos milisegundos por frame. En flujos de 10k msg/s eso se convierte en un cuello de botella. Pasa a claves ECDSA-P256 (un orden de magnitud más rápidas) o firma en lotes agrupando N mensajes en un único sobre.

Olvidar el SubjectKeyIdentifier

CAdES incluye el certificado firmante por defecto, lo que infla cada frame en 1-2 KB. Usa vProfile.IncludeCertificate := False tras el primer frame de una sesión y deja que el verificador coincida por SubjectKeyIdentifier contra su caché de firmantes confiables.

Por dónde seguir

El cifrado de extremo a extremo complementa la firma. sgcSign Server centraliza la custodia de claves para flotas de clientes.

10 proveedores de claves

Usa Azure Trusted Signing, AWS KMS, Google KMS o HashiCorp Vault para mantener las claves privadas completamente fuera del proceso de la aplicación.

Leer más →

sgcSign Server

Servicio de firma REST para que los clientes nunca vean la clave privada. Útil cuando el productor WebSocket es un worker en sandbox.

Leer más →

sgcWebSockets

La librería completa de transporte WebSocket / HTTP2 / HTTP3 / MQTT / AMQP usada en este tutorial.

Leer más →

¿Listo para firmar cada frame WebSocket?

Ambas librerías vienen en la misma versión de prueba. Descarga una vez y construye el canal firmado de extremo a extremo en una tarde.