이 글은 조만간 출판될 Game Programming Gems 3 한국어판(류광 역, 정보문화사)의 일부입니다. 이 글에 대한 모든 권리는 정보문화사가 가지고 있으며, 사전 허락 없이는 웹 사이트 게시를 비롯한 어떠한 형태의 재배포도 금지되어 있습니다.

이 글은 최종 교정 전의 상태이므로 오타나 오역이 있을 수 있습니다. 또한 웹 페이지의 한계 상, 실제로 종이에 인쇄된 형태와는 다를 수 있습니다. 실제 책에서는 표나 수식이 좀 더 정확한 형태로 표시될 것이며 그림/도표 안의 영문도 적절히 한글화될 것입니다.

Game Programming Gem 3 한국어판에 대한 정보는 GPG 스터디(http://www.gpgstudy.com/)에서 얻을 수 있습니다.

5.6 보안 소켓

Pete Isensee, Microsoft Corporation
pkisensee_at_msn.com

멀티플레이어 게임에서의 치팅과 해킹은 게임 체험을 망치는 흔한 문제이다. 치팅을 방지하는 한 가지 방법은 암호화 기술을 이용해서 네트웍 전송을 암호화하고 인증하는 것이다. 이 글에서는 인터넷 보안 프로토콜(Internet Protocol Security, IPSec) 표준을 소개한 다음, 멀티플레이어 게임에서 네트웍 패킷을 보호하고 스푸핑(spoofing), 스니핑(sniffing), 리플레이 공격(replay attack)을 방지하기 위해 IPSec 표준의 일부를 활용하는 방법에 대해 설명한다.

IPSec

IPSec은 IETF(Internet Engineering Task Force)가 학계 및 정부의 연구에 기반해서 개발한 하나의 인터넷 표준이다. 이 표준은 [RFC2401]에서 [RFC2411]까지의 문서들로 발표되었으며, [IPSec2]로도 발표되었다. IPSec는 인증(authentication), 무결성(integrity), 기밀성(confidentiality)을 위한 서비스들을 제공하며, 가상 사설망(virtual private network, VPN)의 구현에 널리 쓰인다. IPSec은 네트웍 계층(네트웍 스택)에서 구현되도록 설계되었으며, 일반적으로 응용 프로그램으로부터는 투명하게 은폐된다. 그러나 게임 플랫폼들 중 IPSec을 제대로 사용할 수 있을만한 것은 별로 없다. 주어진 OS에서 IPSec을 사용할 수 있다고 해도(예를 들면 Windows 2000), 기본적으로 사용할 수 있도록 설정되어 있는 것도 아니고 응용 프로그램이 반드시 그것을 지원하도록 되어 있는 것도 아니다. 이 글을 쓰는 현재, IPSec을 투명하게 지원하며 모든 게임들이 IPSec만을 사용하도록 강조하는 플랫폼은 Microsoft Xbox 뿐이다. 그렇다고는 해도, 게임 프로그래머는 IPSec으로부터 많은 것을 배울 수 있으며, IPSec의 많은 기능들을 게임 네트웍 코드의 문맥 하에서 구현할 수 있다.

인증은 메시지를 보낸 것이 누구인지를 확인하기 위한 수단이다. 무결성은 그 메시지가 변조되지 않았음을 보장하는 수단이다. IPSec은 MD5 [RFC1321]과 SHA-1 [RFC3174] 같은 키 기반 암호 해시를 통해서 인증과 무결성을 구현한다. “기밀성”은 데이터를 노출시키지 않게 하는 수단으로, 암묵적으로 암호화를 수반한다. IPSec은 DES [RFC2405]와 AES [AESDraft] 같은 대칭적 암호화 알고리즘을 이용해서 기밀성을 제공한다. 게임 데이터 전송의 경우 일반적으로 속도가 빠른 알고리즘을 선택하게 된다. 고수준의 암호화를 요구하는 데이터(플레이어 피해치 패킷, 토너먼트 결과 등)라면 느리지만 더 강력한 알고리즘이 필요할 수도 있다(알고리즘들의 성능에 대해서는 [WeiDai01]에 잘 요약되어 있다).

암호화 기술 자체로는 리플레이 공격을 방지할 수 없다. 이미 인증되고 암호화된 메시지를 가로채서 다시 보내는 식의 리플레이 공격은 인증이나 암호화 차원으로는 해결되지 않는다. 이를 위해, IPSec에는 일련번호들의 조합과 슬라이딩 리플레이 윈도우를 이용해서 리플레이 공격을 막는 메커니즘이 포함되어 있다.

몇 가지 가정들

보안 시스템의 구축에서 가장 중요한 것들 중 하나는 키의 관리이다. 키 교환 기법이나 키를 생성, 만료, 갱신하는 적절한 방법들은 이 글에서 이야기하지 않겠다. 여기서는 연결의 양 끝이 동일한 키들을 가지고 있으며 그 키들이 안전한 방식으로 설치, 교환된다고 가정한다. 보안 시스템에서 중요한 또 다른 절차는, 키들이나 공유된 비밀들, 초기화 벡터들에 대해 진정으로 무작위적인 비트들을 적절히 생성하는 것이다[Isensee01]. 이 글은 키들이 암호화에 충분할 정도로 무작위적인 값들을 사용한다고 가정한다.

IPSec 표준은 구현자들이 IP 헤더의 복사본을 패킷의 페이로드(데이터 부분)에 내장해야 하며 그 복사본에도 암호화와 인증이 적용되어야 한다고 요구한다. 이는 실제 IP가 변경되지 않았는지를 수신자가 확인할 수 있도록 하기 위한 것이다. 이런 수준의 인증을 구현하는 것은 복잡할 뿐더러 꼭 필요하지 않은 경우도 많다. 왜냐하면 페이로드 자체의 인증으로도 그 패킷이 적절한 인증 키를 알고 있는 누군가로부터 왔다는 점을 확신할 수 있기 때문이다. 그래서 이 글에서는 IP 인증을 다루지 않는다.

IPSec 인증들은 IP 프로토콜의 단편화(fragmentation) 시스템과 밀접하게 결합되어 있다. 단편화에 대한 처리 문제를 피하기 위해, 이 글에서는 모든 패킷들이 TCP가 아니라 단편화되지 않는 UDP 패킷으로 전송된다고 가정한다. 어차피 대부분의 게임들은 UDP를 사용할 것이므로, 이러한 가정이 큰 제약은 아닐 것이다. 이 글은 또한 모든 메시지들이 최대 전송 단위(maximum transmission unit, MTU)보다 작다고 가정한다. MTU보다 큰 메시지들에 대해서는 응용 프로그램 자신이 적절한 결합 메커니즘을 구현해야 한다.

보안 연결

보안 연결(security association, SA. 또는 보안 협상)은 보안을 목적으로 만들어진 하나의 논리적인 ‘연결(connection)’이다. SA는 한 노드에서 다른 노드로의 네트웍 전송이 어떻게 보안되는지를 정의한다. 하나의 SA에는 암호 및 인증 키들, 모드들, 알고리즘들, 그리고 패킷 형식을 정의하는 기타 정보가 포함된다. 이 글의 SecurityAssociation 클래스에는 표 5.6.1에 나온 정보가 포함된다.

SA들은 거의 항상 쌍으로 발생한다. 송신자의 SA와 수신자의 SA는 정확히 일치해야 한다. 클라이언트/서버 게임이라면 서버는 각 클라이언트의 SA를 원소로 하는 하나의 목록을 가지고 각 클라이언트는 그 목록의 해당 SA와 일치하는 SA를 가지는 식으로 하면 될 것이다. 동급간(peer-to-peer) 게임이라면 각 컴퓨터마다 SA들의 목록을 가져야 할 것이다. 한 호스트가 관리하는 SA들의 목록을 “보안 연결 데이터베이스(SAD)”라고 부른다. 게임의 경우, 이 ‘데이터베이스’는 일반적으로 게임 또는 게임 서버의 메모리 안에 존재하게 된다.

보안 데이터를 보내거나 받기 전에, 통신의 양 쪽은 하나의 보안 연결을 맺어야 한다. 이 글에서는 수신자와 송신자가 SA 데이터를 주고 받는 수단에 대해서는 이야기하지 않겠다. 알고리즘 같은 일부 데이터는 코드 자체에 내장시킬 수도 있다. 키 등 그 외의 데이터의 경우 최대한의 보안을 위해서는 데이터를 주기적으로 변경해야 하며, EKE, Kerberos, Diffie-Hellman [Schneier96] 같은 보안 프로토콜을 통해서 교환되어야 한다. 또한 최대의 보안을 위해서는 인증과 암호화에 대해 서로 다른 키들을 사용해야 한다는 점도 잊어서는 안 된다.

표 5.6.1 보안 연결 데이터

 데이터  설명 
 인증 알고리즘  인증에 쓰이는 암호화 해싱 알고리즘(MD5, SHA-1 등). 
 인증 키  인증에 쓰이는 대칭적 키 데이터. 
 암호화 알고리즘  패킷의 암호화/복호화에 쓰이는 대칭적 암호화 알고리즘(DES, AES 등). 
 암호화 키  암호화 알고리즘에 쓰이는 대칭적 키 데이터. 
 일련번호  이 SA에 대해 전송되는 패킷에 쓰일 일련번호. 
 최근 일련번호  이 SA가 받은 가장 큰 일련번호. 
 리플레이 윈도우  리플레이 공격을 거부하기 위한 슬라이딩 윈도우에 쓰이는 비트마스크 
 IV 크기  암호화 초기화 벡터의 크기(2-8 바이트). 
 ICV 크기  무결성 점검 값의 크기(8-12 바이트). 
 최대 채움 블럭  추가적인 무작위 채움(padding) 블럭들의 최대 개수(0-4 블럭). 블럭 자체의 크기는 암호화 알고리즘에 의해 결정된다. 

패킷 형식

보안 소켓을 구현한다는 것은 데이터를 보낼 때에는 먼저 데이터를 암호화하고 해시를 찍는 것, 그리고 데이터를 받았을 때에는 인증을 하고 데이터를 해독하는 것을 말한다. 테이블 5.6.2는 보안 패킷의 형식이다. 특히 패킷의 어떤 정보에 인증과 암호화가 필요한 지를 주목해서 보기 바란다.

표 5.6.2 보안 패킷 형식

 데이터  크기(바이트)  기본 크기(바이트)  인증됨  암호화됨 
 보안 매개변수 색인(SPI)  1-4  2  예  아니오 
 일련번호  4  4  예  아니오 
 초기화 벡터 (IV)  2-8  4  예  아니오 
 페이로드  가변  가변  예  예 
 채움  0-255  0-255  예  예 
 채움 길이  1  1  예  예 
 무결성 점검 값(ICV)  8-12  10  아니오  아니오 

보안 매개변수 색인

보안 매개변수 색인(Security Parameters Index, SPI)은 보안 연결 데이터베이스 안의 한 보안 연결을 고유하게 식별하는 하나의 핸들이다. SPI는 모든 보안 패킷에 포함되며, 수신자는 그 SPI로 패킷을 처리할 SA를 선택한다. SPI의 크기는 게임에 따라 다르며, 주어진 한 시점에서 활성화될 수 있는 최대 SA 개수에 의해 결정된다. CD-ROM의 예제 구현은 무작위적이고 고유한 2 바이트의 SPI들을 생성한다. SPI는 암호화/해독에 필요한 알고리즘과 키들을 정의하는 SA를 선택하는 데 쓰이므로 암호화되지 않으며 암호화될 수도 없다. SPI는 항상 인증되므로, 공격자가 SPI를 수정하면 패킷 유효성 점검이 실패한다.

일련번호

일련번호는 계속 증가하는 카운터 값이다. 주어진 하나의 보안 연결 하에서 전송되는 첫 번째 패킷의 일련번호는 1이며, 그 다음 패킷은 2, 그 다음은 3이 되는 식이다. 수신자는 일련번호를 통해서 중복된 패킷들이나 ‘이전의’ 패킷들을 걸러낸다. 일련번호가 다시 0으로 되돌아갈 가능성이 있는 경우, 연결의 양 끝에서 새로운 보안 연결 쌍을 생성하고 0으로 되돌아가기 전에 기존 SA들을 만료시키는 일은 송신자가 책임져야 한다. 일련번호는 암호화되지 않으므로 패킷을 해독하지 않고도 리플레이 공격을 검출할 수 있다. 이는 또한 서비스 거부(denialo-of-service, DoS) 공격의 효과를 줄이기도 한다. 일련번호는 항상 인증되므로, 공격자가 일련변호를 변조하면 패킷 유효성 점검이 실패한다.

초기화 벡터

초기화 벡터(initialization vector, IV)는 대칭적 암호화 알고리즘의 초기화에 쓰이는 무작위적인 이진 데이터(BLOB)이다. IV 때문에, 동일한 평문(plaintext)이 여러 번 전송된다 해도 항상 다른 암호문(ciphertext)으로 암호화된다. IV는 송신자에 의해 무작위적인 데이터로 초기화되어서 암호화 알고리즘에 전달된다. 수신자는 그와 동일한 IV를 이용해서 자신의 해독 루틴을 초기화한다. 각 암호화 알고리즘들은 표준 IV 길이를 정의하고 있으며, 대부분의 대칭적 알고리즘들에서 IV는 8 바이트이다. 보안을 조금 희생하더라도 대역폭을 절약할 필요가 있다면 더 적은 바이트들을 사용해도 될 것이다. CD-ROM의 예제 구현은 기본적으로 4 바이트의 IV를 사용한다. IV의 나머지는 암호화나 해독 단계 도중 IV가 쓰일 때 일련번호에 설정된다.

페이로드

페이로드(payload)는 가공되지 않은 원래의 평문 데이터를 뜻한다. 이 데이터는 네트웍으로 전송될 패킷 안에 암호화되어서 들어간다.

채움

채움(padding)은 암호화 이전에 페이로드에 추가된다. 채움이 필요한 이유는 두 가지이다. 우선, 암호화 알고리즘은 특정한 크기의 블럭 단위로 수행된다. 예를 들어서 많은 대칭적 암호화 알고리즘들은 8 바이트 블럭을 사용한다. 두 번째로, 무작위적인 길이 데이터를 페이로드에 채움으로써 페이로드의 진짜 크기를 숨길 수 있다. 채움 바이트들은 일련의 정수 값들(1부터 시작)로 초기화되며, 수신자 측에서 이를 확인함으로써 추가적인 수준의 보안을 얻을 수 있다.

채움 길이

채움 길이는 패킷 안의 채움 바이트들의 개수를 저장하는 1 바이트 필드이다.

무결성 점검 값

무결성 점검 값(integrity check value, ICV)은 잘려진(truncated), 해시 기반 메시지 인증 코드(hash-based message authentication code, HMAC)이다. HMAC는 표준 암호 해시 알고리즘과 비공개 대칭 키[RFC2104]의 조합을 사용하는 키 기반 해싱 알고리즘이다. 지문이 사람을 고유하게 식별하듯이, HMAC는 하나의 보안 패킷을 고유하게 식별한다. HMAC를 자르는 것은 잘 알려진 보안 기법이다. CD-ROM의 예제 구현은 해시를 기본적으로 8 바이트로 자른다.

송신자는 암호화된 패킷 데이터를 SA의 인증 키로 해싱해서 ICV를 계산한다. 수신자는 동일한 계산을 수행하고 그 결과로 나온 ICV를 패킷에 있는 ICV와 비교한다. 그 둘이 일치한다면 패킷은 유효한 것이다. 여기서 유효하다는 것은, 그 패킷이 자신의 SA와 일치하는 SA를 가진 누군가에 의해 전송되었으며(인증되었음) 또한 전송 과정에서 수정되지 않았음(무결성)을 의미한다. 일치하지 않는다면 패킷은 유효하지 않으며, 따라서 폐기해야 한다.

데이터 전송

데이터 전송에서 중요한 것은 페이로드를 암호화하고 전송할 데이터에 대한 암호 해시를 생성하는 것이다. 그럼 예제 구현에서 쓰인 방법들을 자세히 살펴보자.

보안 연결 맺기

보안 데이터를 보내기 전에, 송신자는 수신자와 하나의 보안 연결을 맺어야 한다. SA와 SPI가 설정되었다면 연결의 양 끝은 서로 주고받을 데이터를 암호화, 해독, 인증하는 데 필요한 모든 준비가 끝난 것이다.

헤더 만들기

헤더에는 SPI, 일련번호, 암호화 IV가 포함된다. SPI는 페이로드를 암호화하고 인증하는 데 쓰이는 SA에 해당한다. 일단 일련번호가 쓰였다면, 일련번호는 SA 안에서 증가된다. 암호화 IV는 무작위적으로 생성된다. SA 암호화 알고리즘이 패킷으로 보낸 IV보다 더 큰 IV를 요구한다면 나머지 바이트들은 일련번호로 설정된다.

채움 만들기

채움은 원래의 페이로드의 크기와 SA 암호화 알고리즘에 기반해서 만들어진다. 페이로드의 진짜 크기를 숨기기 위해 무작위적으로 채움을 더 추가할 수도 있다. 무작위적인 채움의 크기는 설정 가능하다. 채움 바이트들은 1에서 시작하는 일련의 정수들로 초기화된다.

페이로드의 암호화

패킷의 정보 중 암호화되는 부분에는 원래의 페이로드, 채움, 채움 길이 바이트가 포함된다. 위에서 생성된 IV로 SA 대칭적 암호화 알고리즘을 초기화하고, 그 알고리즘과 SA 대칭적 암호화 키로 페이로드를 암호화한다. 그 결과로 생긴 암호문은 헤더 뒤에 추가된다.

인증 코드 만들기

패킷을 보내기 전에 마지막으로 할 일은 ICV를 생성하는 것이다. SA 암호 해시 알고리즘과 인증 키로 HMAC 알고리즘을 초기화한다. 그런 다음 해시 알고리즘을 패킷 헤더와 암호화된 페이로드/채움에 적용한다. 그 결과로 생긴 해시 값을 암호문에 추가한다. 해시 값은 암호화하지 않는다.

보안 패킷의 전송

이제 보안 패킷을 담은 버퍼를 게임이 사용하는 UDP 소켓 메커니즘을 이용해서(send() 등) 보내면 된다.

데이터 수신

데이터를 받는 쪽에서는 작업 순서가 보안과 성능에 매우 중요하다. 수신자는 패킷 SPI, ICV, 그리고 일련번호의 유효성을 반드시 점검해야 한다. 수신자는 패킷을 해독하고 채움에 대한 유효성을 점검할 수도 있다. 모든 유효성 점검이 완료된 후에만 패킷 페이로드를 처리해야 한다. 유효성 점검 중 하나라도 실패한 것이 있으면 그 패킷은 폐기해야 한다. 수신자가 게임 서버라면 치터나 해커를 검출, 추적하기 위해 유효성 점검에 실패한 패킷들을 따로 보관해둘 필요도 있을 것이다.

보안 소켓 받기

보안 패킷은 게임이 사용하는 UDP 소켓 메커니즘으로 받으면 된다(recv() 등).

패킷 유효성 점검

가장 간단한 점검은 SPI를 점검하는 것이다. SPI가 수신자의 보안 연결 데이터베이스의 해당 보안 연결과 일치하지 않는다면 패킷은 유효하지 않은 것으로 간주되어야 한다. SA가 일치한다면, 보안 패킷 안에 반드시 포함되어야 하는 데이터 최소량에 기반해서 블럭 크기를 간단히 점검한다.

그 다음으로는 ICV를 대해 점검한다. SA 암호 해시 알고리즘과 키로 HMAC 알고리즘을 초기한다. 그리고 그 해시 알고리즘을 패킷 헤더와 암호화된 페이로드에 적용한다. 그 결과로 생긴 해시 값을 잘라내고 받은 ICV와 비교한다. 일치한다면 인증된 패킷인 것이다.

패킷을 해독하기 전에 먼저 일련번호를 점검한다. 패킷이 리플레이 공격에 의한 것이거나 너무 예전 것이라면 해독할 필요가 없기 때문이다. 여기서 중요한 것은 리플레이 공격을 거부하려다가 유효하지만 순서가 잘못된 패킷(UDP에서 있을 수 있는 일이다)까지 거부하는 일은 없어야 한다는 것인데, 이를 위해 예제 구현은 64 비트 슬라이딩 윈도우를 사용한다. 윈도우의 오른쪽 가장자리는 해당 SA에서 받을 수 있는 가장 높은 유효한 일련번호이다. 패킷의 일련번호가 윈도우의 오른쪽 가장자리보다 낮다면 그 패킷은 너무 오래된 것이므로 폐기한다. 패킷이 윈도우 범위 안에 있다면, 이전에 받은 패킷들의 목록과 비교해본다. 만일 지금 받은 패킷과 동일한 일련번호를 가진 패킷이 존재한다면 리플레이 공격이므로 폐기한다. 그렇지 않다면 유효한 패킷으로 간주하고 패킷의 일련번호로 윈도우의 해당 비트를 설정한다. 일련번호가 오른쪽 가장자리보다 더 크다면 윈도우를 그 만큼 오른쪽으로 이동시킨다.

페이로드의 해독

해독 과정의 첫 번째 단계는 IV를 추출하는 것이다. IV를 추출하고(필요하다면 일련번호로 확장한다), 추출한 IV로 해독 알고리즘을 초기화한다. 그리고 SA 비대칭 해독 키로 페이로드와 채움 바이트들을 풀어낸다. 그 결과로 얻은 평문(plaintext)에는 원래의 페이로드, 채움 바이트들, 그리고 채움 크기가 포함된다.

채움의 점검

채움 크기가 적절한지 점검한다. 그리고 채움 바이트들이 일련의 정수들인지 점검한다. 두 점검 모두 통과했다면 평문에서 모든 채움 바이트들을 제거한다. 그러면 원래의 페이로드가 된다.

예제 구현

CD-ROM의 예제 코드에는 암호화 관련 기능, 보안 연결, 보안 버퍼를 감싸는 C++ 클래스들이 포함되어 있다. 표 5.6.3은 그 클래스들을 정리한 것이다.

핵심적인 구현 코드는 대부분 SecurityAssociation과 SecureBuffer에 들어 있다. 그럼 게임에서 보안 데이터를 전송하거나 받을 때 그 클래스들을 사용하는 방법에 대해 간략히 살펴보자.

표 5.6.3 보안 소켓 클래스들

 클래스 이름  설명 
 CryptContext  Win32 CryptoAPI 암호 서비스 공급자 
 Key  암호 키 및 알고리즘들 
 Cipher  암호화/해독 함수들 
 Hash  해싱 알고리즘들 
 Buffer  std::string<unsigned char> 
 SecurityAssociation  보안 연결 데이터 
 SADatabase  std::map<SpiType, SecurityAssociation> 
 SecureBuffer  Functions for encrypting, decrypting, and authenticating payloads 

보안 연결 만들기

보안 연결을 하나 생성하고 보안 연결 데이터베이스(SAD)에 추가한다.

 // SAD에서 SecureBuffer들로의 연관을 설정.
 SADatabase sad;
 SecureBuffer::SetSADatabase( &sad );
 // 무작위 키들을 생성. 이 예는 암호화에는 DES,
 // 해싱에는 MD5를 사용한다.
 Key keyAuth( CALG_DES );
 Key keyCrypt( CALG_DES );
 // 주어진 키와 알고리즘들로 새 SA를 생성한다.
 // 다른 것들은 기본값들이 쓰인다.
 SecurityAssociation sa( keyAuth, keyCrypt, CALG_MD5 );
 // 새 SPI를 생성하고 SA와 SPI 쌍을 SAD에 추가한다.
 SpiType nSPI = sad.GenNewSPI();
 sad.Insert( nSPI, sa );
 // 연결의 다른 쪽과 안전한 방식으로 nSPI와 키들을 교환한다...

보안 패킷의 전송

보안 패킷을 생성하고 전송한다.

 // SPI를 통해서 SecureBuffer에 SA를 연결시킨다.
 SecureBuffer sb( nSPI );

// 암호화되고 인증된 버퍼를 생성한다.

 // 여기서 암호화 및 해싱이 일어난다.
 sb.Create( “payload”, 7 );
 // 보안 패킷을 전송한다.
 send( sock, sb.GetDataPtr(), sb.GetSize(), 0 );

보안 패킷 받기

보안 패킷을 받아서 처리한다.

 // 보안 패킷을 받는다.
 char pData[ 1024 ];
 int n = recv( sock, pData, 1024, 0 );
 if( n == 0 || n == SOCKET_ERROR )
    return false;
 // 패킷의 유효성 검사
 SecureBuffer sb( pData, n );
 if( !sb.IsAuthentic() )
    return false;
 // 원래의 페이로드를 추출
 if( !sb.GetPayload( pData, &n ) )
    return false;
 // 리플레이 윈도우를 조정한다.
 sb.SetAccepted();
 return true;

CryptoAPI

이 클래스들은 암호 알고리즘들과 키 관리를 위해 Windows CryptoAPI를 사용한다. 모든 암호 관련 코드는 crypto.cpp 파일로 모듈화되어 있으므로 저수준 암호 기능을 다른 구현들(예를 들면 Crypto++, [WeiDai01])로 대체하는 것도 가능하다. CryptoAPI에서 좀 아쉬운 부분은, 키 데이터에 대해 직접 접근하는 수단을 제공하지 않는다는 점이다. CryptoAPI에는 키를 직접 설정하거나 얻는 API가 없다. 다행히 Microsoft KnowledgeBase 아티클 Q228786[KB228786]에 키 데이터에 직접 접근하는 방법이 나와 있으며, Key 클래스도 그 방법을 사용한다.

CryptoAPI는 암호 서비스 공급자를 사용하는 식으로 설계되었기 때문에 Windows의 버전에 따라 사용할 수 있는 알고리즘들이나 키 길이가 다를 수 있다. 따라서 실패의 경우에 대비해서 반환 코드를 꼭 점검해야 한다.

성능

보안 소켓을 사용할 때 성능에 악영향을 미치는 요인이 두 가지 존재한다. 첫 번째는 대역폭의 증가이다. 기본 설정들과 8 바이트 블럭 암호화 알고리즘(DES)을 사용하는 예제 구현의 경우 패킷 크기의 증가치는 다음과 같다.

 2 (SPI) + 4 (일련번호) + 4 (IV) + 0-15 (채움)
 + 1 (채움길이 필드) + 10 (ICV) = 21-36 bytes

표준 UDP 패킷이 21에서 36 바이트 만큼 늘어나게 된다. SA 매개변수들을 조절해서 증가치를 줄일 수는 있으나, 줄인 만큼 보안이 떨어지게 된다. 이러한 증가의 영향을 줄이기 위해서는, 작은 페이로드들을 여러 번 자주 보내는 것보다 커다란 페이로드를 가끔 보내는 쪽으로 고려해 보는 것이 좋다. 페이로드의 크기 + 1이 SA 암호문의 블럭 크기로 균등하게 나눠질 수 있도록 페이로드 크기를 잡으면(+1은 채움 길이 바이트를 위한 것임) 불필요한 채움을 피할 수 있다.

두 번째 요인은 암호화/해독과 인증에 걸리는 시간이다. 표 5.6.4는 1 킬로바이트의 페이로드 데이터를 생성하고, 인증하고, 해독하는 데 걸리는 비용을 정리한 것이다. 표를 보면 보안 소켓이 성능에 미치는 영향이 그리 크지 않음을 알 수 있다. 성능에서 가장 중요한 요인은 알고리즘들의 속도와 개별 패킷의 크기이다. 주어진 응용 프로그램이 요구하는 보안 수준과 성능 양쪽을 모두 만족시키는 적절한 알고리즘과 키 길이를 선택해야 할 것이다.

표 5.6.4의 결과는 Pentium III 866 MHz, Windows 2000에서 기본 SA 설정들과 DES 암호화(64 비트 키) 알고리즘 및 MD5 해싱을 이용해서 얻은 것이다. “생성” 열은 주어진 페이로드 크기를 이용해서 전체 페이로드 데이터에 대해 SecureBuffer::Create를 반복적으로 호출하는 데 걸린 밀리초 단위의 시간이다. “인증” 열은 SecrueBuffer::IsAuthentic이 소비한 시간이고, “해독” 열은 SecureBuffer::GetPayload가 소비한 시간이다. 페이로드 크기가 커짐에 따라 성능이 증가하는데, 이는 패킷이 클수록 패킷 크기 증가의 상대적 부담이 작아지기 때문이다.

표 5.6.4 성능

 총 1 킬로바이트의 페이로드 데이터를 처리하는 데 걸리는 시간 
 페이로드 크기(바이트)  생성 (ms)  인증 (ms)  해독 (ms) 
 64  2.07  0.90  1.07 
 128  1.06  0.45  0.57 
 256  0.59  0.24  0.34 
 512  0.35  0.14  0.23 
 1024  0.23  0.09  0.16 

보안

이 글의 의도는 패킷의 추가 부담은 크지만 거의 완벽한 보안과 최소의 패킷 추가 부담 + 적당한 보안 사이의 타협선상에 있는 한 구현을 소개하는 것이다. 보안 소켓 구현이 제공하는 보안 품질은 기본적으로 구현에 쓰이는 알고리즘의 강력함에 의존한다. XOR를 암호화나 인증 알고리즘으로 써서는 안 된다. XOR 같은 단순한 방법으로 패킷이 보호되길 기대하는 것은 헛된 일이다. 암호화에 대해서는 DES, Triple-DES, Blowfish, AES, 해싱에 대해서는 MD5, SHA-1 등 잘 알려진 표준 알고리즘들을 견실하게 구현한 코드를 사용해야 한다. 그리고 알고리즘이 지원한다면 좀 더 큰 키들(128 비트)을 사용하는 것이 좋다. 그리고 키를 자주 바꿔야 한다. IV 크기는 4 바이트 이상, ICV 크기는 8 바이트 이상으로 하지 않으면 안전성이 크게 떨어진다.

기술적으로는 암호화를 생략하고 인증만 사용하는 것이 가능하나, 암호화 없이 인증만 해서는 스니핑 공격을 방어할 수 없다. 스니핑 공격 역시 치팅 목적으로 쓰이며, 스푸핑 만큼이나 나쁜 결과를 초래한다(그리고 검출하기는 더 어렵다).

결론

게임들이 토너먼트나 경쟁 시합 같은 기능을 지원함에 따라, 보안은 단지 치팅을 방지하는 차원이 아니라 게임 자체의 성패를 결정하는 핵심적인 요인이 되고 있다. IPSec 표준에는 네트웍 게임에서 즉시 써먹을 수 있을만한 보안 요소들이 포함되어 있다. 패킷 추가부담이 없는 것은 아니지만 성능이 아주 나빠지지는 않으며, 무엇보다도 보안 통신이 주는 장점이 단점을 상쇄하고도 남는다.

참고자료