Assine cada mensagem WebSocket

O TLS prova que o canal não foi adulterado no fio. Não prova quem produziu uma mensagem e não sobrevive depois que a mensagem é logada, persistida ou reproduzida. Este tutorial combina sgcWebSockets e sgcSign para que cada frame que seu servidor emite — ou cada frame que seu cliente envia — carregue uma assinatura CAdES detached que o outro lado valida contra um certificado de signatário conhecido.

Mensagens atreladas à identidade
CAdES detached por frame
Sobrevive a replay e logging

TLS não é suficiente

O TLS protege bytes em trânsito entre dois endpoints. Uma vez que um frame é logado, arquivado ou reproduzido por middleware, a evidência do TLS some — só uma assinatura digital sobre o próprio payload sobrevive.

Não repúdio por mensagem

Pregões, exchanges de apostas, canais de comando IoT e feeds de dados clínicos precisam de um audit trail à prova de adulteração por mensagem. Uma assinatura CAdES detached em cada frame deixa o receptor provar, horas ou anos depois, exatamente quem emitiu o quê e quando.

Identidade mútua

WebSocket com mTLS prova a identidade do cliente na hora da conexão. A assinatura por mensagem prova a identidade da aplicação por frame — útil quando o gateway WebSocket é um edge proxy compartilhado e o produtor real está mais abaixo na pilha.

Resistente a replay

Inclua o id de sessão do WebSocket e um nonce monotonicamente crescente dentro do payload assinado. Um atacante de replay não consegue reassinar a mensagem com um novo nonce sem acesso à chave privada.

Formato do envelope

Embrulhe cada mensagem WebSocket em um pequeno envelope JSON que carrega o payload, um nonce, um timestamp e a assinatura detached codificada em base64.

Contrato do envelope

O envelope é assinado sobre uma sequência determinística de bytes: session_id || nonce || timestamp || payload_sha256. Usamos SHA-256 porque o payload pode ser binário (Protobuf, MessagePack) e queremos uma entrada de tamanho fixo para o signer.

CAdES é a escolha certa aqui — produz assinaturas CMS/PKCS#7 compactas que, em base64, são pequenas o bastante para caber confortavelmente ao lado do payload. PAdES é específico de PDF e XAdES é específico de XML; nenhum dos dois encaixa em um canal de tempo real binário-ou-JSON.

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

Adicione as units e carregue a chave de assinatura

O sgcWebSockets fornece o transporte, o sgcSign fornece o motor de assinatura. Eles não compartilham estado — você os conecta no seu código de aplicação.

Cláusula uses

Tanto para cliente quanto para servidor, você precisa dos componentes WebSocket, do perfil CAdES, de um provedor de chave e do verificador. Use um PFX no lado cliente e um certificado de serviço de longa duração (tipicamente do Windows store) no 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 — assine antes de enviar

Engate o fluxo de saída do cliente, calcule o digest do payload e dos metadados da sessão, anexe a assinatura CAdES e emita o envelope via WriteData.

Empacote o payload

Mantenha o signer e o provedor de chave como campos no seu form ou data module. TsgcSignCAdES.SignDetached aceita um buffer de bytes e retorna a estrutura CMS detached. Construímos a sequência canônica de bytes em BuildToBeSigned — id de sessão, nonce, timestamp ISO 8601 e SHA-256 dos bytes do payload.

Chamar WriteData no TsgcWebSocketClient envia o envelope como um frame de texto comum. O verificador do lado servidor vai reverter esse processo antes de passar o payload ao handler da aplicação.

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 — verifique antes de despachar

Engate o evento OnMessage do servidor, recalcule os bytes a serem assinados a partir dos campos do envelope, verifique a assinatura detached e só então encaminhe o payload para sua lógica de negócio.

Handler OnMessage

O handler faz parse do envelope, reconstrói a sequência canônica de bytes e chama TsgcSignatureVerifier.VerifyDetached com os bytes da assinatura e os bytes a serem assinados. O verificador retorna um relatório com o subject do signatário, o status da cadeia e quaisquer metadados de timestamp.

Faça cache dos certificados de signatários confiáveis por thumbprint dentro do servidor — verificação completa de cadeia a cada frame fica cara em conexões de alto throughput. Após a primeira verificação, você só precisa confirmar que a assinatura é válida e que o thumbprint é um que você já confiou.

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;

Proteção contra replay

Uma assinatura válida não basta — um atacante que captura um envelope assinado pode replicar enquanto o certificado for válido. Vincule cada mensagem à sessão e force nonces monotônicos.

Janela de nonce por conexão

Mantenha no servidor um dicionário chaveado por (SessionId, SignerThumbprint) com o maior nonce já visto. Rejeite qualquer envelope cujo nonce não seja estritamente maior. Rejeite qualquer envelope cujo timestamp esteja a mais de alguns segundos de distância do relógio do servidor — delimite a janela de replay mesmo que o mesmo nonce, de alguma forma, seja reutilizado.

Para redes com perda em que a ordem não é garantida, troque a checagem “estritamente maior” por uma janela deslizante: aceite qualquer nonce dentro das últimas N entradas que ainda não tenha sido visto, rejeitando qualquer coisa mais antiga. O requisito fundamental é que nenhum par de frames carregue o mesmo (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;

O que costuma dar errado

Três modos de falha que mais vemos quando desenvolvedores ligam pela primeira vez o sgcSign ao sgcWebSockets.

Canonicalização inconsistente

Se o cliente constrói os bytes a serem assinados a partir de uma string JSON com chaves em ordem de inserção e o servidor os reconstrói a partir de um objeto parseado e reserializado, os digests não vão bater. Sempre assine uma sequência determinística de bytes que você mesmo construa — não assine JSON reserializado.

Latência em streams de alto throughput

Assinar cada mensagem com uma chave RSA-2048 adiciona alguns milissegundos por frame. Em streams de 10k msg/s, isso vira gargalo. Troque para chaves ECDSA-P256 (uma ordem de magnitude mais rápidas) ou assine em batches agrupando N mensagens em um único envelope.

Esquecer o SubjectKeyIdentifier

O CAdES inclui o certificado do signatário por padrão, o que infla cada frame em 1-2 KB. Use vProfile.IncludeCertificate := False depois do primeiro frame de uma sessão e deixe o verificador casar pelo SubjectKeyIdentifier contra seu cache de signatários confiáveis.

Para onde ir a partir daqui

Criptografia ponta a ponta complementa a assinatura. O sgcSign Server centraliza a custódia de chaves para frotas de clientes.

10 provedores de chave

Use Azure Trusted Signing, AWS KMS, Google KMS ou HashiCorp Vault para manter as chaves privadas completamente fora do processo da aplicação.

Leia mais →

sgcSign Server

Serviço REST de assinatura para que os clientes nunca vejam a chave privada. Útil quando o produtor WebSocket é um worker em sandbox.

Leia mais →

sgcWebSockets

A biblioteca completa de transporte WebSocket / HTTP2 / HTTP3 / MQTT / AMQP usada neste tutorial.

Leia mais →

Pronto para assinar cada frame WebSocket?

As duas bibliotecas vêm no mesmo trial. Baixe uma vez, construa o canal assinado de ponta a ponta em uma tarde.