Sign Every WebSocket Message

TLS proves the channel was not tampered with on the wire. It does not prove who produced a message, and it does not survive once the message is logged, persisted or replayed. This tutorial combines sgcWebSockets and sgcSign so every frame your server emits — or every frame your client sends — carries a detached CAdES signature that the other side validates against a known signer certificate.

Identity-bound messages
Detached CAdES per frame
Survives replay and logging

TLS is not enough

TLS protects bytes in flight between two endpoints. Once a frame is logged, archived or replayed through middleware, the TLS evidence is gone — only a digital signature on the payload itself survives.

Per-message non-repudiation

Trading floors, betting exchanges, IoT command channels and clinical data feeds all need a tamper-proof audit trail per message. A detached CAdES signature on each frame lets the receiver prove, hours or years later, exactly who emitted what and when.

Mutual identity

WebSocket mTLS proves the client identity at connect time. Per-message signing proves the application identity per frame — useful when the WebSocket gateway is a shared edge proxy and the real producer is downstream.

Replay-safe

Include the WebSocket session id and a monotonically increasing nonce inside the signed payload. A replay attacker cannot re-sign the message with a fresh nonce without access to the private key.

Envelope shape

Wrap every WebSocket message in a small JSON envelope that carries the payload, a nonce, a timestamp and the base64-encoded detached signature.

Envelope contract

The envelope is signed over a deterministic byte sequence: session_id || nonce || timestamp || payload_sha256. We use SHA-256 because the payload may be binary (Protobuf, MessagePack) and we want a fixed-length input to the signer.

CAdES is the right choice here — it produces compact CMS/PKCS#7 signatures that base64-encode small enough to fit comfortably alongside the payload. PAdES is PDF-specific and XAdES is XML-specific; neither fits a binary-or-JSON real-time channel.

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

Pull in the units and load the signing key

sgcWebSockets supplies the transport, sgcSign supplies the signature engine. They share no state — you wire them up in your application code.

Uses clause

For both client and server you need the WebSocket components, the CAdES profile, a key provider, and the verifier. Use a PFX for the client side and a long-lived service certificate (typically from Windows store) for the server side.

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

Client side — sign before send

Hook the client's outbound flow, compute the digest of the payload plus session metadata, attach the CAdES signature and emit the envelope through WriteData.

Wrap the payload

Keep the signer and key provider as fields on your form or data module. TsgcSignCAdES.SignDetached accepts a byte buffer and returns the detached CMS structure. We build the canonical byte sequence in BuildToBeSigned — session id, nonce, ISO 8601 timestamp and SHA-256 of the payload bytes.

Calling WriteData on TsgcWebSocketClient sends the envelope as a regular text frame. The server-side verifier will reverse this process before passing the payload to the application handler.

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;

Server side — verify before dispatch

Hook the server's OnMessage event, re-compute the to-be-signed bytes from the envelope fields, verify the detached signature and only then forward the payload to your business logic.

OnMessage handler

The handler parses the envelope, rebuilds the canonical byte sequence and calls TsgcSignatureVerifier.VerifyDetached with the signature bytes and the to-be-signed bytes. The verifier returns a report with the signer subject, the chain status and any timestamp metadata.

Cache trusted signer certificates by thumbprint inside the server — full chain verification on every frame becomes expensive on high-throughput connections. After the first verification you only need to confirm the signature is valid and the thumbprint is one you have already trusted.

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;

Replay protection

A valid signature is not enough — an attacker who captures a signed envelope can replay it as long as the certificate is still valid. Bind each message to the session and enforce monotonic nonces.

Per-connection nonce window

Keep a server-side dictionary keyed by (SessionId, SignerThumbprint) with the highest nonce seen so far. Reject any envelope whose nonce is not strictly greater. Reject any envelope whose timestamp is more than a few seconds away from the server clock — bound the replay window even if the same nonce is somehow reused.

For lossy networks where ordering is not guaranteed, switch the strict greater-than check to a sliding window: accept any nonce within the last N entries that has not been seen before, rejecting anything older. The fundamental requirement is that no two frames carry the same (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;

What usually goes wrong

Three failure modes we see most often when developers first wire sgcSign into sgcWebSockets.

Inconsistent canonicalisation

If the client builds the to-be-signed bytes from a JSON string with insertion-order keys and the server rebuilds them from a parsed and re-serialised object, the digests will not match. Always sign a deterministic byte sequence you construct yourself — do not sign re-serialised JSON.

Latency on high-throughput streams

Signing every message with an RSA-2048 key adds a few milliseconds per frame. On 10k msg/s streams that becomes a bottleneck. Switch to ECDSA-P256 keys (an order of magnitude faster) or sign in batches by grouping N messages into a single envelope.

Forgetting the SubjectKeyIdentifier

CAdES includes the signer certificate by default, which inflates every frame by 1-2 KB. Use vProfile.IncludeCertificate := False after the first frame in a session and let the verifier match by SubjectKeyIdentifier against its trusted-signer cache.

Where to go from here

End-to-end encryption complements signing. The sgcSign Server centralises key custody for fleets of clients.

10 Key Providers

Use Azure Trusted Signing, AWS KMS, Google KMS or HashiCorp Vault to keep private keys out of the application process entirely.

Read more →

sgcSign Server

REST signing service so clients never see the private key. Useful when the WebSocket producer is a sandboxed worker.

Read more →

sgcWebSockets

The full WebSocket / HTTP2 / HTTP3 / MQTT / AMQP transport library used in this tutorial.

Read more →

Ready to sign every WebSocket frame?

Both libraries ship in the same trial. Download once, build the end-to-end signed channel in an afternoon.