Firma ogni messaggio WebSocket

TLS dimostra che il canale non è stato manomesso in transito. Non dimostra chi ha prodotto un messaggio e non sopravvive una volta che il messaggio viene loggato, persistito o ritrasmesso. Questo tutorial combina sgcWebSockets e sgcSign in modo che ogni frame emesso dal tuo server — o ogni frame inviato dal tuo client — porti con sé una firma CAdES detached che l'altra parte convalida rispetto a un certificato firmatario noto.

Messaggi legati all'identità
CAdES detached per frame
Sopravvive a replay e logging

TLS non basta

TLS protegge i byte in transito tra due endpoint. Una volta che un frame viene loggato, archiviato o ritrasmesso attraverso del middleware, la prova TLS è persa — sopravvive solo una firma digitale sul payload stesso.

Non ripudio per messaggio

Sale di trading, exchange di scommesse, canali di comando IoT e flussi di dati clinici hanno tutti bisogno di un audit trail a prova di manomissione per ogni messaggio. Una firma CAdES detached su ogni frame consente al ricevente di dimostrare, ore o anni più tardi, esattamente chi ha emesso cosa e quando.

Identità mutua

L'mTLS WebSocket dimostra l'identità del client al momento della connessione. La firma per messaggio dimostra l'identità dell'applicazione per ogni frame — utile quando il gateway WebSocket è un edge proxy condiviso e il vero produttore sta a valle.

A prova di replay

Includi il session id del WebSocket e un nonce monotonicamente crescente all'interno del payload firmato. Un attaccante di tipo replay non può rifirmare il messaggio con un nuovo nonce senza accesso alla chiave privata.

Forma dell'envelope

Avvolgi ogni messaggio WebSocket in un piccolo envelope JSON che porta il payload, un nonce, un timestamp e la firma detached codificata in base64.

Contratto dell'envelope

L'envelope è firmato su una sequenza deterministica di byte: session_id || nonce || timestamp || payload_sha256. Usiamo SHA-256 perché il payload potrebbe essere binario (Protobuf, MessagePack) e vogliamo un input di lunghezza fissa per il firmatario.

CAdES è la scelta giusta qui — produce firme CMS/PKCS#7 compatte che, codificate in base64, restano abbastanza piccole da stare comodamente accanto al payload. PAdES è specifico di PDF e XAdES è specifico di XML; nessuno dei due si adatta a un canale real-time binario o JSON.

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

Includi le unit e carica la chiave di firma

sgcWebSockets fornisce il trasporto, sgcSign fornisce l'engine di firma. Non condividono stato — li colleghi nel codice della tua applicazione.

Clausola uses

Sia per il client che per il server servono i componenti WebSocket, il profilo CAdES, un key provider e il verificatore. Usa un PFX lato client e un certificato di servizio di lunga durata (in genere dal Windows store) lato server.

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

Lato client — firma prima dell'invio

Aggancia il flusso in uscita del client, calcola il digest del payload più i metadati di sessione, allega la firma CAdES ed emetti l'envelope tramite WriteData.

Avvolgi il payload

Tieni il firmatario e il key provider come campi sulla tua form o data module. TsgcSignCAdES.SignDetached accetta un buffer di byte e restituisce la struttura CMS detached. Costruiamo la sequenza canonica di byte in BuildToBeSigned — session id, nonce, timestamp ISO 8601 e SHA-256 dei byte del payload.

Chiamare WriteData su TsgcWebSocketClient invia l'envelope come un normale frame di testo. Il verificatore lato server invertirà questo processo prima di passare il payload all'handler dell'applicazione.

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;

Lato server — verifica prima del dispatch

Aggancia l'evento OnMessage del server, ricalcola i byte da firmare dai campi dell'envelope, verifica la firma detached e solo allora inoltra il payload alla tua logica di business.

Handler OnMessage

L'handler fa il parsing dell'envelope, ricostruisce la sequenza canonica di byte e chiama TsgcSignatureVerifier.VerifyDetached con i byte della firma e i byte da firmare. Il verificatore restituisce un report con il subject del firmatario, lo stato della catena ed eventuali metadati di timestamp.

Memorizza in cache i certificati dei firmatari fidati per thumbprint all'interno del server — la verifica completa della catena per ogni frame diventa costosa su connessioni ad alto throughput. Dopo la prima verifica devi solo confermare che la firma è valida e che il thumbprint è uno di quelli che hai già accettato come fidati.

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;

Protezione dal replay

Una firma valida non basta — un attaccante che cattura un envelope firmato può ritrasmetterlo finché il certificato è ancora valido. Lega ogni messaggio alla sessione e imponi nonce monotoni.

Finestra di nonce per connessione

Mantieni un dizionario lato server indicizzato da (SessionId, SignerThumbprint) con il nonce più alto visto finora. Rifiuta ogni envelope il cui nonce non sia strettamente maggiore. Rifiuta ogni envelope il cui timestamp sia a più di pochi secondi dall'orologio del server — limita la finestra di replay anche se lo stesso nonce venisse in qualche modo riutilizzato.

Per reti lossy in cui l'ordinamento non è garantito, sostituisci il controllo strettamente maggiore con una finestra scorrevole: accetta qualsiasi nonce all'interno delle ultime N voci che non sia già stato visto, rifiutando tutto ciò che è più vecchio. Il requisito fondamentale è che nessun paio di frame porti lo stesso (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;

Cosa va storto di solito

Tre modalità di fallimento che vediamo più spesso quando gli sviluppatori collegano per la prima volta sgcSign a sgcWebSockets.

Canonicalizzazione incoerente

Se il client costruisce i byte da firmare da una stringa JSON con chiavi nell'ordine di inserimento e il server li ricostruisce da un oggetto parsato e riserializzato, i digest non corrisponderanno. Firma sempre una sequenza deterministica di byte che costruisci tu — non firmare JSON riserializzato.

Latenza su stream ad alto throughput

Firmare ogni messaggio con una chiave RSA-2048 aggiunge qualche millisecondo per frame. Su stream da 10k msg/s diventa un collo di bottiglia. Passa a chiavi ECDSA-P256 (un ordine di grandezza più veloci) o firma in batch raggruppando N messaggi in un singolo envelope.

Dimenticare il SubjectKeyIdentifier

CAdES include il certificato del firmatario per default, il che gonfia ogni frame di 1-2 KB. Usa vProfile.IncludeCertificate := False dopo il primo frame di una sessione e lascia che il verificatore corrisponda per SubjectKeyIdentifier rispetto alla sua cache dei firmatari fidati.

Dove andare da qui

La crittografia end-to-end complementa la firma. sgcSign Server centralizza la custodia delle chiavi per flotte di client.

10 key provider

Usa Azure Trusted Signing, AWS KMS, Google KMS o HashiCorp Vault per tenere le chiavi private completamente fuori dal processo dell'applicazione.

Leggi di più →

sgcSign Server

Servizio di firma REST in cui i client non vedono mai la chiave privata. Utile quando il produttore WebSocket è un worker sandboxed.

Leggi di più →

sgcWebSockets

L'intera libreria di trasporto WebSocket / HTTP2 / HTTP3 / MQTT / AMQP usata in questo tutorial.

Leggi di più →

Pronto a firmare ogni frame WebSocket?

Entrambe le librerie sono incluse nella stessa trial. Scarica una volta, costruisci il canale firmato end-to-end in un pomeriggio.