すべての WebSocket メッセージに署名する
TLS は通信路上で改ざんされていないことを証明します。誰がメッセージを生成したかは証明せず、メッセージがログ記録、永続化、リプレイされた後では生き残りません。本チュートリアルでは sgcWebSockets と sgcSign を組み合わせ、サーバーが送出するすべてのフレーム — またはクライアントが送るすべてのフレーム — が、相手側が既知の署名者証明書に対して検証する detached CAdES 署名を運ぶようにします。
TLS は通信路上で改ざんされていないことを証明します。誰がメッセージを生成したかは証明せず、メッセージがログ記録、永続化、リプレイされた後では生き残りません。本チュートリアルでは sgcWebSockets と sgcSign を組み合わせ、サーバーが送出するすべてのフレーム — またはクライアントが送るすべてのフレーム — が、相手側が既知の署名者証明書に対して検証する detached CAdES 署名を運ぶようにします。
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 のリアルタイムチャネルには適合しません。
{
"sid": "7f3a-b6e1",
"nonce": 42,
"ts": "2026-05-26T11:24:09Z",
"payload": { /* the actual app message */ },
"sig": "MIIK...base64...detached CAdES..."
}
sgcWebSockets がトランスポートを、sgcSign が署名エンジンを提供します。両者は状態を共有しません — アプリケーションコードで結線します。
クライアントとサーバーの両方で、WebSocket コンポーネント、CAdES プロファイル、鍵プロバイダー、検証者が必要です。クライアント側には PFX、サーバー側には長寿命のサービス証明書(通常 Windows ストアから)を使用してください。
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 — を構築します。
TsgcWebSocketClient の WriteData を呼ぶと、エンベロープが通常のテキストフレームとして送信されます。サーバー側の検証者は、アプリケーションハンドラーにペイロードを渡す前にこのプロセスを逆転します。
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 署名を検証し、その後初めてビジネスロジックにペイロードを転送します。
ハンドラーはエンベロープをパースし、正規バイト列を再構築し、署名バイトと to-be-signed バイトで TsgcSignatureVerifier.VerifyDetached を呼びます。検証者は署名者サブジェクト、チェーンステータス、タイムスタンプメタデータを持つレポートを返します。
信頼された署名者証明書をサーバー内でサムプリント別にキャッシュしてください — 各フレームでの完全なチェーン検証は、高スループット接続では高価になります。最初の検証後は、署名が有効でサムプリントがすでに信頼したものであることを確認するだけで十分です。
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) を運ばないことです。
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 メッセージを単一エンベロープにグループ化してバッチで署名してください。
CAdES はデフォルトで署名者証明書を含めるため、各フレームを 1-2 KB 膨張させます。セッション内の最初のフレーム後に vProfile.IncludeCertificate := False を使用し、検証者にその信頼署名者キャッシュに対して SubjectKeyIdentifier で照合させてください。
エンド・ツー・エンド暗号化は署名を補完します。sgcSign Server はクライアント群の鍵管理を集約します。