すべての WebSocket メッセージに署名する

TLS は通信路上で改ざんされていないことを証明します。誰がメッセージを生成したかは証明せず、メッセージがログ記録、永続化、リプレイされた後では生き残りません。本チュートリアルでは sgcWebSockets と sgcSign を組み合わせ、サーバーが送出するすべてのフレーム — またはクライアントが送るすべてのフレーム — が、相手側が既知の署名者証明書に対して検証する detached CAdES 署名を運ぶようにします。

身元結合メッセージ
フレームごとの detached CAdES
リプレイとロギングに耐える

TLS だけでは不十分

TLS は 2 つのエンドポイント間の通信中のバイトを保護します。フレームがログ記録、アーカイブ、ミドルウェアを通じてリプレイされると、TLS の証拠は消えます — ペイロード自体のデジタル署名のみが生き残ります。

メッセージごとの否認防止

取引フロア、賭博取引所、IoT コマンドチャネル、臨床データフィードはすべて、メッセージごとの改ざん防止監査証跡を必要とします。各フレームの detached CAdES 署名により、受信者は数時間後、または数年後に、誰が何をいつ送出したかを正確に証明できます。

相互身元

WebSocket mTLS は接続時にクライアントの身元を証明します。メッセージごとの署名はフレームごとにアプリケーションの身元を証明します — WebSocket ゲートウェイが共有エッジプロキシで、実際の生成者が下流にいる場合に有用です。

リプレイ安全

WebSocket セッション ID と単調増加のノンスを署名済みペイロード内に含めます。リプレイ攻撃者は秘密鍵にアクセスせずに新しいノンスでメッセージを再署名することはできません。

エンベロープの形

すべての WebSocket メッセージを、ペイロード、ノンス、タイムスタンプ、base64 エンコードされた detached 署名を運ぶ小さな JSON エンベロープでラップします。

エンベロープ契約

エンベロープは決定論的なバイト列に対して署名されます: session_id || nonce || timestamp || payload_sha256。ペイロードはバイナリ(Protobuf、MessagePack)の可能性があり、署名者への固定長入力が欲しいため SHA-256 を使用します。

ここでは CAdES が正しい選択です — コンパクトな CMS/PKCS#7 署名を生成し、base64 エンコードしてもペイロードと一緒に快適に収まる程度に小さいです。PAdES は PDF 専用、XAdES は XML 専用で、どちらもバイナリまたは JSON のリアルタイムチャネルには適合しません。

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

ユニットを取り込み、署名鍵を読み込む

sgcWebSockets がトランスポートを、sgcSign が署名エンジンを提供します。両者は状態を共有しません — アプリケーションコードで結線します。

Uses 句

クライアントとサーバーの両方で、WebSocket コンポーネント、CAdES プロファイル、鍵プロバイダー、検証者が必要です。クライアント側には PFX、サーバー側には長寿命のサービス証明書(通常 Windows ストアから)を使用してください。

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

クライアント側 — 送信前に署名

クライアントの送信フローにフックし、ペイロードとセッションメタデータのダイジェストを計算し、CAdES 署名を添付し、WriteData 経由でエンベロープを送出します。

ペイロードをラップ

署名者と鍵プロバイダーをフォームまたはデータモジュールのフィールドとして保持してください。TsgcSignCAdES.SignDetached はバイトバッファを受け取り、detached CMS 構造を返します。BuildToBeSigned で正規バイト列 — セッション ID、ノンス、ISO 8601 タイムスタンプ、ペイロードバイトの SHA-256 — を構築します。

TsgcWebSocketClientWriteData を呼ぶと、エンベロープが通常のテキストフレームとして送信されます。サーバー側の検証者は、アプリケーションハンドラーにペイロードを渡す前にこのプロセスを逆転します。

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;

サーバー側 — ディスパッチ前に検証

サーバーの OnMessage イベントにフックし、エンベロープフィールドから to-be-signed バイトを再計算し、detached 署名を検証し、その後初めてビジネスロジックにペイロードを転送します。

OnMessage ハンドラー

ハンドラーはエンベロープをパースし、正規バイト列を再構築し、署名バイトと to-be-signed バイトで TsgcSignatureVerifier.VerifyDetached を呼びます。検証者は署名者サブジェクト、チェーンステータス、タイムスタンプメタデータを持つレポートを返します。

信頼された署名者証明書をサーバー内でサムプリント別にキャッシュしてください — 各フレームでの完全なチェーン検証は、高スループット接続では高価になります。最初の検証後は、署名が有効でサムプリントがすでに信頼したものであることを確認するだけで十分です。

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;

リプレイ保護

有効な署名だけでは不十分です — 署名済みエンベロープをキャプチャした攻撃者は、証明書が有効である限りリプレイできます。各メッセージをセッションに結びつけ、単調なノンスを強制してください。

接続ごとのノンスウィンドウ

サーバー側で (SessionId, SignerThumbprint) をキーとし、これまで見た最高のノンスを持つ辞書を保持してください。ノンスが厳密に大きくないエンベロープを拒否してください。タイムスタンプがサーバークロックから数秒以上離れたエンベロープも拒否してください — 同じノンスが何らかの理由で再利用されてもリプレイウィンドウを制限します。

順序が保証されないロッシーネットワークでは、厳密な大なり比較をスライディングウィンドウに切り替えてください: 過去 N エントリ内の見たことのない任意のノンスを受け入れ、それより古いものは拒否。基本要件は 2 つのフレームが同じ (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;

通常何がうまくいかないか

開発者が最初に sgcSign を sgcWebSockets に組み込むときに最もよく見る 3 つの失敗モード。

正規化の不一致

クライアントが挿入順キーの JSON 文字列から to-be-signed バイトを構築し、サーバーがパースして再シリアル化したオブジェクトから再構築すると、ダイジェストは一致しません。常に自分で構築する決定論的バイト列に署名してください — 再シリアル化された JSON に署名しないでください。

高スループットストリームでのレイテンシ

RSA-2048 鍵で各メッセージに署名するとフレームあたり数ミリ秒追加されます。10k msg/s ストリームでは、これがボトルネックになります。ECDSA-P256 鍵(桁違いに高速)に切り替えるか、N メッセージを単一エンベロープにグループ化してバッチで署名してください。

SubjectKeyIdentifier を忘れる

CAdES はデフォルトで署名者証明書を含めるため、各フレームを 1-2 KB 膨張させます。セッション内の最初のフレーム後に vProfile.IncludeCertificate := False を使用し、検証者にその信頼署名者キャッシュに対して SubjectKeyIdentifier で照合させてください。

ここからどこへ

エンド・ツー・エンド暗号化は署名を補完します。sgcSign Server はクライアント群の鍵管理を集約します。

10 の鍵プロバイダー

Azure Trusted Signing、AWS KMS、Google KMS、HashiCorp Vault を使用して、秘密鍵を完全にアプリケーションプロセスの外に保ちます。

続きを読む →

sgcSign Server

クライアントが秘密鍵を見ることがない REST 署名サービス。WebSocket 生成者がサンドボックス化されたワーカーの場合に有用。

続きを読む →

sgcWebSockets

本チュートリアルで使用される完全な WebSocket / HTTP2 / HTTP3 / MQTT / AMQP トランスポートライブラリ。

続きを読む →

すべての WebSocket フレームに署名する準備はできましたか?

両ライブラリは同じトライアルに同梱されます。1 度ダウンロードし、午後の間にエンド・ツー・エンドの署名済みチャネルを構築してください。