Jede WebSocket-Nachricht signieren

TLS beweist, dass der Kanal auf der Leitung nicht manipuliert wurde. Es beweist nicht, wer eine Nachricht erzeugt hat, und es überlebt nicht, sobald die Nachricht protokolliert, gespeichert oder wiederholt wird. Dieses Tutorial kombiniert sgcWebSockets und sgcSign, sodass jeder Frame, den dein Server sendet — oder jeder Frame, den dein Client sendet — eine Detached-CAdES-Signatur trägt, die die Gegenseite gegen ein bekanntes Signaturzertifikat prüft.

Identitätsgebundene Nachrichten
Detached CAdES pro Frame
Überlebt Replay und Logging

TLS reicht nicht

TLS schützt Bytes auf dem Weg zwischen zwei Endpunkten. Sobald ein Frame protokolliert, archiviert oder durch Middleware wiederholt wird, ist der TLS-Nachweis verloren — nur eine digitale Signatur auf dem Payload selbst überlebt.

Nichtabstreitbarkeit pro Nachricht

Trading-Floors, Wettbörsen, IoT-Befehlskanäle und klinische Datenströme brauchen alle einen manipulationssicheren Audit-Trail pro Nachricht. Eine Detached-CAdES-Signatur auf jedem Frame erlaubt dem Empfänger, Stunden oder Jahre später nachzuweisen, wer was wann gesendet hat.

Beidseitige Identität

WebSocket-mTLS belegt die Client-Identität zur Verbindungszeit. Per-Message-Signing belegt die Anwendungsidentität pro Frame — nützlich, wenn das WebSocket-Gateway ein gemeinsam genutzter Edge-Proxy ist und der eigentliche Produzent dahintersteht.

Replay-sicher

Bette die WebSocket-Session-ID und eine monoton wachsende Nonce in den signierten Payload ein. Ein Replay-Angreifer kann die Nachricht nicht mit einer neuen Nonce neu signieren, ohne Zugriff auf den privaten Schlüssel zu haben.

Form des Umschlags

Verpacke jede WebSocket-Nachricht in einen kleinen JSON-Umschlag, der den Payload, eine Nonce, einen Zeitstempel und die Base64-kodierte Detached-Signatur trägt.

Umschlag-Kontrakt

Der Umschlag wird über eine deterministische Bytefolge signiert: session_id || nonce || timestamp || payload_sha256. Wir verwenden SHA-256, weil der Payload binär sein kann (Protobuf, MessagePack) und wir eine Eingabe fester Länge für den Signer wollen.

CAdES ist hier die richtige Wahl — es erzeugt kompakte CMS/PKCS#7-Signaturen, die base64-kodiert klein genug sind, um bequem neben dem Payload Platz zu finden. PAdES ist PDF-spezifisch und XAdES ist XML-spezifisch; keines passt zu einem binären oder JSON-basierten Echtzeit-Kanal.

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

Units einbinden und Signaturschlüssel laden

sgcWebSockets liefert den Transport, sgcSign liefert die Signatur-Engine. Sie teilen keinen Zustand — du verdrahtest sie in deinem Anwendungscode.

Uses-Klausel

Für Client und Server brauchst du die WebSocket-Komponenten, das CAdES-Profil, einen Key Provider und den Verifier. Verwende eine PFX-Datei auf Client-Seite und ein langlebiges Service-Zertifikat (üblicherweise aus dem Windows-Store) auf Server-Seite.

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

Client-Seite — vor dem Senden signieren

Hänge dich in den ausgehenden Fluss des Clients ein, berechne den Digest aus Payload und Session-Metadaten, hänge die CAdES-Signatur an und sende den Umschlag über WriteData.

Den Payload verpacken

Halte Signer und Key Provider als Felder auf deinem Formular oder Datenmodul. TsgcSignCAdES.SignDetached akzeptiert einen Byte-Puffer und liefert die Detached-CMS-Struktur zurück. Die kanonische Bytefolge bauen wir in BuildToBeSigned — Session-ID, Nonce, ISO-8601-Zeitstempel und SHA-256 der Payload-Bytes.

Der Aufruf von WriteData auf TsgcWebSocketClient sendet den Umschlag als normalen Text-Frame. Der serverseitige Verifier kehrt diesen Prozess um, bevor er den Payload an den Anwendungs-Handler weitergibt.

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-Seite — vor der Weiterverarbeitung verifizieren

Hänge dich in das OnMessage-Event des Servers, baue die zu signierenden Bytes aus den Umschlagfeldern neu auf, verifiziere die Detached-Signatur und leite den Payload erst danach an deine Geschäftslogik weiter.

OnMessage-Handler

Der Handler parst den Umschlag, baut die kanonische Bytefolge neu auf und ruft TsgcSignatureVerifier.VerifyDetached mit den Signatur-Bytes und den zu signierenden Bytes auf. Der Verifier liefert einen Bericht mit dem Subject des Signierenden, dem Kettenstatus und etwaigen Zeitstempel-Metadaten.

Cache vertrauenswürdige Signaturzertifikate per Thumbprint im Server — volle Kettenprüfung pro Frame wird auf Hochdurchsatz-Verbindungen teuer. Nach der ersten Verifizierung musst du nur noch bestätigen, dass die Signatur gültig ist und der Thumbprint einer ist, den du bereits als vertrauenswürdig hinterlegt hast.

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-Schutz

Eine gültige Signatur reicht nicht aus — ein Angreifer, der einen signierten Umschlag mitschneidet, kann ihn so lange wiederabspielen, wie das Zertifikat noch gültig ist. Binde jede Nachricht an die Session und erzwinge monotone Nonces.

Nonce-Fenster pro Verbindung

Halte serverseitig ein Dictionary mit Schlüssel (SessionId, SignerThumbprint) und der bisher höchsten gesehenen Nonce. Lehne jeden Umschlag ab, dessen Nonce nicht strikt größer ist. Lehne jeden Umschlag ab, dessen Zeitstempel mehr als wenige Sekunden von der Serveruhr entfernt ist — begrenze das Replay-Fenster, selbst wenn dieselbe Nonce irgendwie wiederverwendet wird.

Für verlustbehaftete Netzwerke ohne garantierte Reihenfolge ersetze die strikte Größer-Prüfung durch ein Sliding Window: akzeptiere jede Nonce innerhalb der letzten N Einträge, die noch nicht gesehen wurde, und lehne ältere ab. Die fundamentale Anforderung ist, dass keine zwei Frames denselben (SessionId, Nonce) tragen.

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;

Was üblicherweise schiefgeht

Drei Fehlerbilder, die wir am häufigsten sehen, wenn Entwickler sgcSign zum ersten Mal in sgcWebSockets einbinden.

Inkonsistente Kanonikalisierung

Wenn der Client die zu signierenden Bytes aus einem JSON-String mit einfügereihenfolge-basierten Schlüsseln baut und der Server sie aus einem geparsten und neu serialisierten Objekt rekonstruiert, passen die Digests nicht zusammen. Signiere immer eine deterministische Bytefolge, die du selbst erzeugst — signiere kein neu serialisiertes JSON.

Latenz bei Hochdurchsatz-Strömen

Jede Nachricht mit einem RSA-2048-Schlüssel zu signieren, kostet ein paar Millisekunden pro Frame. Bei 10k msg/s wird das zum Flaschenhals. Wechsle auf ECDSA-P256-Schlüssel (eine Größenordnung schneller) oder signiere in Batches, indem du N Nachrichten zu einem Umschlag bündelst.

SubjectKeyIdentifier vergessen

CAdES bindet das Signaturzertifikat standardmäßig ein, was jeden Frame um 1–2 KB aufbläht. Setze nach dem ersten Frame einer Session vProfile.IncludeCertificate := False und lass den Verifier per SubjectKeyIdentifier gegen seinen Cache vertrauenswürdiger Signer auflösen.

Wie es weitergeht

Ende-zu-Ende-Verschlüsselung ergänzt das Signieren. Der sgcSign-Server zentralisiert die Schlüsselverwahrung für Flotten von Clients.

10 Key Provider

Nutze Azure Trusted Signing, AWS KMS, Google KMS oder HashiCorp Vault, um private Schlüssel komplett aus dem Anwendungsprozess herauszuhalten.

Mehr erfahren →

sgcSign-Server

REST-Signaturdienst, sodass Clients den privaten Schlüssel nie sehen. Praktisch, wenn der WebSocket-Producer ein sandboxed Worker ist.

Mehr erfahren →

sgcWebSockets

Die komplette WebSocket-/HTTP2-/HTTP3-/MQTT-/AMQP-Transportbibliothek, die in diesem Tutorial verwendet wird.

Mehr erfahren →

Bereit, jeden WebSocket-Frame zu signieren?

Beide Bibliotheken liegen derselben Testversion bei. Einmal herunterladen, den Ende-zu-Ende signierten Kanal an einem Nachmittag bauen.