Signer chaque message WebSocket

TLS prouve que le canal n'a pas été altéré sur le fil. Il ne prouve pas qui a produit un message, et il ne survit pas une fois que le message est journalisé, persisté ou rejoué. Ce tutoriel combine sgcWebSockets et sgcSign pour que chaque frame que votre serveur émet — ou chaque frame que votre client envoie — porte une signature CAdES detached que l'autre côté valide contre un certificat de signataire connu.

Messages liés à l'identité
CAdES detached par frame
Survit au replay et à la journalisation

TLS ne suffit pas

TLS protège les octets en vol entre deux endpoints. Une fois qu'une frame est journalisée, archivée ou rejouée à travers un middleware, la preuve TLS a disparu — seule une signature numérique sur le payload lui-même survit.

Non-répudiation par message

Les salles de marché, les bourses de paris, les canaux de commande IoT et les flux de données cliniques ont tous besoin d'une piste d'audit inviolable par message. Une signature CAdES detached sur chaque frame permet au récepteur de prouver, des heures ou des années plus tard, exactement qui a émis quoi et quand.

Identité mutuelle

Le mTLS WebSocket prouve l'identité du client au moment de la connexion. La signature par message prouve l'identité de l'application par frame — utile lorsque la passerelle WebSocket est un proxy edge partagé et que le vrai producteur est en aval.

Anti-replay

Incluez l'id de session WebSocket et un nonce monotone croissant dans le payload signé. Un attaquant en replay ne peut pas re-signer le message avec un nonce frais sans accès à la clé privée.

Forme de l'enveloppe

Encapsulez chaque message WebSocket dans une petite enveloppe JSON qui porte le payload, un nonce, un timestamp et la signature detached encodée en base64.

Contrat d'enveloppe

L'enveloppe est signée sur une séquence d'octets déterministe : session_id || nonce || timestamp || payload_sha256. Nous utilisons SHA-256 parce que le payload peut être binaire (Protobuf, MessagePack) et nous voulons une entrée de longueur fixe pour le signeur.

CAdES est le bon choix ici — il produit des signatures CMS/PKCS#7 compactes qui s'encodent base64 suffisamment petites pour tenir confortablement à côté du payload. PAdES est spécifique au PDF et XAdES est spécifique au XML ; aucun ne convient à un canal temps réel binaire 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..."
}

Inclure les unités et charger la clé de signature

sgcWebSockets fournit le transport, sgcSign fournit le moteur de signature. Ils ne partagent aucun état — vous les câblez dans votre code applicatif.

Clause uses

Pour le client et le serveur vous avez besoin des composants WebSocket, du profil CAdES, d'un fournisseur de clés et du vérificateur. Utilisez un PFX côté client et un certificat de service long terme (typiquement depuis le magasin Windows) côté serveur.

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

Côté client — signer avant l'envoi

Branchez le flux sortant du client, calculez le digest du payload plus les métadonnées de session, attachez la signature CAdES et émettez l'enveloppe via WriteData.

Encapsuler le payload

Gardez le signeur et le fournisseur de clés comme champs sur votre fiche ou module de données. TsgcSignCAdES.SignDetached accepte un buffer d'octets et retourne la structure CMS detached. Nous construisons la séquence d'octets canonique dans BuildToBeSigned — id de session, nonce, timestamp ISO 8601 et SHA-256 des octets du payload.

Appeler WriteData sur TsgcWebSocketClient envoie l'enveloppe comme une frame texte classique. Le vérificateur côté serveur inversera ce processus avant de passer le payload au handler applicatif.

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;

Côté serveur — vérifier avant la dispatch

Branchez l'événement OnMessage du serveur, recalculez les octets à signer depuis les champs de l'enveloppe, vérifiez la signature detached et seulement alors transférez le payload à votre logique métier.

Handler OnMessage

Le handler parse l'enveloppe, reconstruit la séquence d'octets canonique et appelle TsgcSignatureVerifier.VerifyDetached avec les octets de signature et les octets à signer. Le vérificateur retourne un rapport avec le sujet du signataire, le statut de la chaîne et toute métadonnée d'horodatage.

Mettez en cache les certificats de signataires de confiance par empreinte à l'intérieur du serveur — la vérification complète de la chaîne sur chaque frame devient coûteuse sur des connexions à haut débit. Après la première vérification, vous n'avez qu'à confirmer que la signature est valide et que l'empreinte est une que vous avez déjà approuvée.

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;

Protection contre le replay

Une signature valide ne suffit pas — un attaquant qui capture une enveloppe signée peut la rejouer tant que le certificat est encore valide. Liez chaque message à la session et imposez des nonces monotones.

Fenêtre de nonce par connexion

Maintenez un dictionnaire côté serveur indexé par (SessionId, SignerThumbprint) avec le nonce le plus élevé vu jusqu'à présent. Rejetez toute enveloppe dont le nonce n'est pas strictement plus grand. Rejetez toute enveloppe dont le timestamp est à plus de quelques secondes de l'horloge serveur — bornez la fenêtre de replay même si le même nonce est rejoué d'une manière ou d'une autre.

Pour les réseaux avec perte où l'ordre n'est pas garanti, basculez la vérification stricte plus-grand-que vers une fenêtre glissante : acceptez tout nonce dans les N dernières entrées qui n'a pas été vu auparavant, en rejetant tout ce qui est plus vieux. L'exigence fondamentale est qu'aucune deux frames ne portent le même (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;

Ce qui se passe mal habituellement

Trois modes d'échec que nous voyons le plus souvent quand des développeurs câblent sgcSign dans sgcWebSockets pour la première fois.

Canonicalisation incohérente

Si le client construit les octets à signer depuis une chaîne JSON avec des clés dans l'ordre d'insertion et que le serveur les reconstruit depuis un objet parsé et re-sérialisé, les digests ne correspondront pas. Signez toujours une séquence d'octets déterministe que vous construisez vous-même — ne signez pas du JSON re-sérialisé.

Latence sur les flux à haut débit

Signer chaque message avec une clé RSA-2048 ajoute quelques millisecondes par frame. Sur des flux de 10k msg/s cela devient un goulot d'étranglement. Basculez vers des clés ECDSA-P256 (un ordre de grandeur plus rapide) ou signez par lots en regroupant N messages dans une seule enveloppe.

Oublier le SubjectKeyIdentifier

CAdES inclut le certificat du signataire par défaut, ce qui gonfle chaque frame de 1 à 2 Ko. Utilisez vProfile.IncludeCertificate := False après la première frame d'une session et laissez le vérificateur faire correspondre par SubjectKeyIdentifier contre son cache de signataires de confiance.

Où aller à partir d'ici

Le chiffrement de bout en bout complète la signature. Le serveur sgcSign centralise la garde des clés pour des flottes de clients.

10 fournisseurs de clés

Utilisez Azure Trusted Signing, AWS KMS, Google KMS ou HashiCorp Vault pour garder les clés privées hors du processus applicatif entièrement.

En savoir plus →

Serveur sgcSign

Service de signature REST pour que les clients ne voient jamais la clé privée. Utile quand le producteur WebSocket est un worker en sandbox.

En savoir plus →

sgcWebSockets

La bibliothèque de transport complète WebSocket / HTTP2 / HTTP3 / MQTT / AMQP utilisée dans ce tutoriel.

En savoir plus →

Prêt à signer chaque frame WebSocket ?

Les deux bibliothèques sont livrées dans le même essai. Téléchargez une fois, construisez le canal signé de bout en bout en un après-midi.