모든 WebSocket 메시지 서명하기
TLS는 채널이 전송 중에 변조되지 않았음을 증명해요. 누가 메시지를 생성했는지는 증명하지 않으며, 메시지가 기록, 저장 또는 재생되면 살아남지 못해요. 이 튜토리얼은 sgcWebSockets와 sgcSign을 결합하여 서버가 내보내는 모든 프레임 — 또는 클라이언트가 보내는 모든 프레임 — 이 상대방이 알려진 서명자 인증서에 대해 검증하는 디태치드 CAdES 서명을 운반하도록 해요.
TLS는 채널이 전송 중에 변조되지 않았음을 증명해요. 누가 메시지를 생성했는지는 증명하지 않으며, 메시지가 기록, 저장 또는 재생되면 살아남지 못해요. 이 튜토리얼은 sgcWebSockets와 sgcSign을 결합하여 서버가 내보내는 모든 프레임 — 또는 클라이언트가 보내는 모든 프레임 — 이 상대방이 알려진 서명자 인증서에 대해 검증하는 디태치드 CAdES 서명을 운반하도록 해요.
TLS는 두 엔드포인트 사이를 비행하는 바이트를 보호해요. 프레임이 기록, 아카이브 또는 미들웨어를 통해 재생되면 TLS 증거는 사라져요 — 페이로드 자체의 디지털 서명만 살아남아요.
거래 플로어, 베팅 거래소, IoT 명령 채널 및 임상 데이터 피드는 모두 메시지당 변조 방지 감사 추적이 필요해요. 각 프레임의 디태치드 CAdES 서명을 통해 수신자는 몇 시간 또는 몇 년 후에 정확히 누가 무엇을 언제 내보냈는지 증명할 수 있어요.
WebSocket mTLS는 연결 시간에 클라이언트 신원을 증명해요. 메시지당 서명은 프레임당 애플리케이션 신원을 증명해요 — WebSocket 게이트웨이가 공유 에지 프록시이고 실제 생산자가 다운스트림에 있을 때 유용해요.
서명된 페이로드 내에 WebSocket 세션 ID와 단조 증가하는 논스를 포함하세요. 재생 공격자는 개인 키에 접근하지 않고는 새 논스로 메시지를 다시 서명할 수 없어요.
모든 WebSocket 메시지를 페이로드, 논스, 타임스탬프 및 base64로 인코딩된 디태치드 서명을 운반하는 작은 JSON 엔벨로프로 감싸세요.
엔벨로프는 결정론적 바이트 시퀀스에 서명돼요: session_id || nonce || timestamp || payload_sha256. 페이로드가 이진(Protobuf, MessagePack)일 수 있고 서명자에 대한 고정 길이 입력을 원하기 때문에 SHA-256을 사용해요.
여기서는 CAdES가 올바른 선택이에요 — 페이로드와 함께 편안하게 들어갈 만큼 작게 base64로 인코딩되는 컴팩트한 CMS/PKCS#7 서명을 생성해요. 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는 바이트 버퍼를 받아 디태치드 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 이벤트에 후크하고, 엔벨로프 필드에서 서명될 바이트를 다시 계산하고, 디태치드 서명을 검증한 다음에만 페이로드를 비즈니스 로직으로 전달하세요.
핸들러는 엔벨로프를 파싱하고, 표준 바이트 시퀀스를 재구축하고, 서명 바이트와 서명될 바이트로 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개 항목 내의 모든 논스를 수락하고, 더 오래된 것은 거부하세요. 근본적인 요구 사항은 두 프레임이 동일한 (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에 연결할 때 가장 자주 보는 세 가지 실패 모드.
클라이언트가 삽입 순서 키가 있는 JSON 문자열에서 서명될 바이트를 빌드하고 서버가 파싱되고 다시 직렬화된 객체에서 다시 빌드하면 다이제스트가 일치하지 않아요. 항상 직접 구축한 결정론적 바이트 시퀀스에 서명하세요 — 다시 직렬화된 JSON에 서명하지 마세요.
RSA-2048 키로 모든 메시지에 서명하면 프레임당 몇 밀리초가 추가돼요. 10k msg/s 스트림에서는 이것이 병목 현상이 돼요. ECDSA-P256 키(한 자릿수 더 빠름)로 전환하거나 N개의 메시지를 단일 엔벨로프로 그룹화하여 일괄적으로 서명하세요.
CAdES는 기본적으로 서명자 인증서를 포함하므로 모든 프레임이 1-2 KB만큼 부풀려져요. 세션의 첫 프레임 후에 vProfile.IncludeCertificate := False를 사용하고 검증자가 신뢰할 수 있는 서명자 캐시에 대해 SubjectKeyIdentifier로 일치시키도록 하세요.
종단 간 암호화는 서명을 보완해요. sgcSign Server는 클라이언트 플릿에 대한 키 보관을 중앙 집중화해요.
Azure Trusted Signing, AWS KMS, Google KMS 또는 HashiCorp Vault를 사용하여 개인 키를 애플리케이션 프로세스 외부에 완전히 유지하세요.
더 읽기 →