글을 쓴지 거의 1년이나 지나버렸다. 평소 생활이 바쁜 것도 있지만, 게으른 것도 있으리라. 내 생각이 담긴 글은 아니고.. 보안에 관심은 있는데 관련 업종이 아니니 자주 접하질 못하고, 또 자꾸 잊어먹기도 하여 틈틈이 개념들을 정리해 놓곤 하는데, 그 중 하나인 SSL/TLS과 관련된 글이다. Stackexchange에서 “How does SSL work?” 질의에 대한 답변(특히 Thomas Pornin의 답변)을 보고 정리한 것이다. 오역도 있고 그럴테지만 기본 적인 개념을 쌓는데는 충분하다고 생각되고, 글도 올린지 너무 오래되어 이 글을 올려본다. 그리고 최근(14년 4월 7일) OpenSSL 1.0.1 버전 부터 1.0.1f 버전에 매우 심각한 버그가 있음이 발견되었다. 해당 버전의 OpenSSL을 사용하고 있으면 heartbeat를 이용하여 메모리에 상주하고 있는, 비밀키를 포함한 모든 정보를 가져갈 수 있다. 자세한 내용은 Heartbleed Bug를 참고하고, 현재 사용하고 있다면 빨리 패치해야 한다.
SSL/TLS
최초의 SSL은 Netscape사에서 개발되었다. SSL의 목적은 안전하지 않은 전문들을 암호화 하여 안전하게 전달하기 위해서이다. SSL 버전 1은 실제로 발표되진 않았었고, SSL 버전 2가 이후 공개적으로 발표가 되었는데 많은 보안 취약점이 있었다. 그래서 그것을 보안한 것이 SSL 버전 3이며 공식적으로 표준화 한 이름이 TLS 이다. 이를 같이 묶어서 SSL/TLS 라고 부른다. 좀 더 자세히 말해 보자면 아래와 같이 버전이 대입된다.
- SSL v3: SSL 버전 3.0
- SSL/TLS 1.0: SSL 버전 3.1
- SSL/TLS 1.1: SSL 버전 3.2
- SSL/TLS 1.2: SSL 버전 3.3
TLS는 현재 TLS 1.0, TLS 1.1 그리고 TLS 1.2 버전으로 나뉘어져 있으며, 보안상 TLS 1.2가 가장 강력하다. 하지만 아직까지도(2013년) TLS 1.2를 지원하지 않는 브라우저나 애플리케이션이 존재하기 때문에 TLS 1.2만 쓰지는 않고 TLS 1.1 등을 같이 지원하면서 사용한다. SSL/TLS의 가장 큰 목적은 상호간에 데이터를 매우 안전하게 전달하는 것에 있다. TCP는 패킷 이라는 단위를 통하여 데이터를 전송한다. 이 패킷은 악의적인 공격자에 의하여 쉽게 탈취가 가능하며, 공격자는 탈취한 패킷을 임의의 데이터로 조작하여 재 전송을 할 수 있다. 전송해야 할 데이터가 모두 노출이 되어 있기 때문에 가능한 일이다. 이를 방지하도록 도움을 주는 것이 SSL/TLS 이다. SSL/TLS는 TCP/IP 위에서 동작한다. 그렇기 때문에 몇몇 사람들에 의하여 네트워크의 OSI 레이어 중 TCP/IP 레이어 인지, Application 레이어인지 갑론을박이 벌어지기도 한다. 이 2개의 레이어에 속하도록 하는 것이 아니라 이 2개의 레이어 사이에 있다고 보는게 더 적당한 것 같다. 아무튼 이 SSL/TLS는 TCP/IP 위에서 동작하기 때문에 SSL/TLS의 목적인 보안성과 함께 TCP/IP의 안정성까지 가지고 있다.
Records (레코드)
SSL/TLS의 가장 하위 단은 record protocol이다. TCP가 데이터를 패킷으로 나뉘어 사용하듯이 SSL/TLS은 데이터를 record(이하 레코드) 단위로 나뉘어 사용한다. 레코드는 아래와 같은 형태로 되어 있다.
HH V1V2 L1L2 data
각 필드들은 아래와 같다.
- HH: 1 바이트로 레코드 안에 있는 데이터 타입을 결정한다. 아래와 같은 4개의 타입이 정의되어 있다.
- 20: change_cipher_spec
- 21: alert
- 22: handshake
- 23: application_data
- V1V2: 2바이트로 구성되며 현재 사용되는 SSL/TLS 버전을 뜻 한다. 아래와 같이 대입된다고 보면 된다.
- 0300: SSL 버전 3.0
- 0301: SSL 버전 3.1 (SSL/TLS 1.0)
- 0302: SSL 버전 3.2 (SSL/TLS 1.1)
- 0303: SSL 버전 3.3 (SSL/TLS 1.2)
- L1L2: 전송되는 데이터의 길이를 뜻 한다. 빅 엔디언(big-endian) 표기법이기 때문에 실제 사람이 읽을 수 있는 길이는 256*L2+L1 으로 계산해야 한다. 그렇기 때문에 최대 전송 가능한 길이는 18,432 바이트 이지만 실제 이 길이에 도달할 정도로 레코드가 구성되지는 않는다.
이 이후에는 대칭키 암호로 암호화 된 데이터와 무결성 검사용 데이터들이 최대 18KB가 온다. 복호화 할 때 쓰이는 키와 사용되는 암호화 알고리즘, 그리고 무결성 체크 알고리즘은 이미 상호간에 기 교환된 알고리즘들로 체크하게 된다. 만약 압축 기법을 서로 사용하기로 했다면 상호간에 사용하기로 한 압축 기법으로 해제도 수행한다. 대략적인 과정은 아래와 같다.
- 전송하고자 하는 데이터를 최대 16,384 바이트 잡는다. 18,432 바이트가 아닌 이유는 레코드로 구성시 별도로 추가되는 데이터 때문이다.
- 압축 기법을 사용하기로 했다면 상호간에 사용하기로 한 압축기법으로 압축을 수행한다. 현재에는 사용하지 않거나 “Deflate”를 사용하거나 하는데 “Deflate”를 사용할 경우 CRIME attack에 의하여 취약점이 노출될 수 있어 실제는 압축 기법 사용을 권고하지 않는다. 아무튼 압축 기법 사용시 데이터 길이가 더 길어지기도 하는데 그럴 경우를 대비하여 SSL/TLS는 1KB 정도의 여유 공간을 더 잡아 두고 있다.
- 보안성과 무결성을 유지하기 위해 마지막에는 MAC(여기서는 일반적으로 HMAC)을 붙인다. HMAC을 추가하기 위해 SSL/TLS은 1KB 정도를 HMAC을 위한 공간으로 예약해 두고 있다.
HMAC은 MD5, SHA-1 이나 SHA-256 등을 이용하여 구성하고, 암호화는 block cipher인 경우 CBC 모드를, stream cipher인 경우에는 RC4를 사용한다. HMAC는 전송될 데이터와 연결 순서와의 조합에서 생성하기 때문에 공격자는 레코드를 바꿔치지 못하게 된다.
Handshake
Handshake는 레코드 단위에서 동작되는 행동이며, SSL/TLS의 레코드에 적용할 알고리즘과 키를 교환하는 것에 목적이 있다. 이 handshake를 마치고 나면 오직 둘만이 소통할 수 있는 세션(session)이라는 통로가 생성된다. Handshake는 메시지들로 구성되어 있는데, 각각의 메시지들은 4개의 바이트 헤더를 가지고 있으며 메시지는 레코드 형태로 전송이 된다. 레코드 형식으로 전송되므로 레코드에서 사용하는 헤더도 역시 포함되어 전송이 된다. 헤더의 첫 번째 바이트는 메시지의 타입을 나타내며 3 바이트는 메시지의 길이를 나타낸다. (빅 엔디언 방식으로 표시된다.) 메시지들은 레코드 형식으로 다시 보내지므로 여기서 레코드에서 사용하는 타입은 22를 사용한다. SSL/TLS 를 시작하기 위한 최초의 교신은 암호화와 MAC 없이 시작한다. 최초의 메시지는 ClientHello 라고 불리우며 클라이언트(Client)가 SSL 통신을 시작하기 원한다는 것을 뜻한다. ClientHello가 포함하고 있는 내용은 아래와 같다.
- 클라이언트가 지원하는 최대 SSL/TLS 버전
- Client Random: 총 32바이트로 구성되며 이 중 28 바이트는 Cryptographically Strong Number Generator로 무작위로 생성된다.(아니면 가정된다)
- Session ID: Abbreviated Handshake를 사용시 이용됨. SSL/TLS를 다시 사용하고자 할 경우 사용되는 값.
- Cipher Suites: 클라이언트가 수용할 수 있는 암호화 기법 목록
- Compression Algorithms: 클라이언트가 알고 있는 압축 기법 목록
- Optional Extensions
Cipher suite는 16 비트로 사용할 암호화 기법을 정의한다. 예로 TLS_RSA_WITH_AES_128_CBC_SHA는 0x002F로 표현되며 나타내는 바는 HMAC/SHA-1과 128 비트 키로 AES 암호화를 사용하며, 키 교환은 RSA로 수행하겠다는 뜻이다. ClientHello의 응답으로 서버는 ServerHello를 보낸다. ServerHello는 다음과 같은 내용을 포함하고 있다.
- 클라이언트와 서버가 사용할 SSL/TLS 버전
- Server Random: 총 32바이트로 구성되며 28바이트는 Client Random과 동일함
- Sesion ID: 현 연결에서 사용할 Session ID
- Cipher Suite: 현 연결에서 사용할 Cipher Suite
- Compression Algorithm: 현 연결에서 사용할 Compression Algorithm
- Optional Extensions
전체적인 Handshake는 아래와 같은 형식으로 진행 된다.
Client Server
ClientHello -->
ServerHello
Certificate*
ServerKeyExchange*
CertificateRequest*
<-- ServerHelloDone
Certificate*
ClientKeyExchange
CertificateVerify*
[ChangeCipherSpec]
Finished -->
[ChangeCipherSpec]
<-- Finished
Application Data <-> Application Data
ClientHello와 ServerHello를 마치면 사용하는 cipher suite와 다른 인자 값들에 따라 몇몇의 메시지들을 서버는 추가로 보내게 된다.
- Certificate: 공개키를 담고 있는 서버의 인증서를 보낸다. 클라이언트가 인증서를 보내지 말라고 요청 할 때만 보내지 않으며, 그 외는 모두 보낸다.
- ServerKeyExchange: 키 교환시 인증서만으로 충분하지 않은 경우 사용되는 메시지. 예로 “DHE” 사용시 ephermeral Diffie-Hellman 키 교환을 사용하게 되는데 이 때 이 메시지를 이용한다.
- CertificateRequest: 클라이언트의 인증서를 요청하는 메시지. 서버가 추가 인증을 필요한 경우 요청하는 메시지이며, 이 메시지에는 서버가 믿고 있는 상위 인증서들(Root Certificates) 목록을 포함하고 있다.
- ServerHelloDone: Marker message로 ServerHello가 끝났다는 것을 알린다. 지금 부터는 클라이언트가 응답을 시작해야 함을 알린다.
위와 같은 메시지를 서버에게 받으면 클라이언트는 아래와 같이 응답을 해줘야 한다.
- Certificate: 서버가 인증서를 요청하였다면 클라이언트 인증서를 이 메시지를 통하여 전송한다.
- ClientKeyExchange: 키 교환시 사용할 메시지이다.
- CertificateVerify: 이전의 handshake 메시지들을 가지고 계산된 디지털 서명으로서 서버가 클라이언트의 인증서를 요청하였을 경우 응답하게 되는 메시지이다. 이것을 통하여 클라이언트의 인증서가 진짜임을 알린다.
ChangeCipherSpec은 handshake 메시지가 아니다. 레코드 타입(타입 20번) 중 하나로서 발송되며 cipher suite를 서버와 다시 협의하게 된다. Finished 메시지는 이전의 서버와 클라이언트간 모든 handshake 메시지들을 가지고 암호학적인 체크섬을 계산한 것이다. 서버는 이 값을 받으면 재 계산을 통하여 자신이 계속 똑같은 클라이언트와 교신을 했는지 검증하게 된다. 클라이언트가 ChangeCipherSepc을 요청하였다면 서버는 그에 대한 응답을 해주고, Finished 응답을 해준다. 이것으로 상호간 SSL/TLS을 이용하여 교환하고자 하는 데이터를 전송하게 된다. Session ID를 기억하고 있다면 위와 같은 Full Handshake를 이용하지 않아도 된다. Abbreviated Handshake를 이용하면 되는데 아래와 같이 절차가 매우 간단하다.
Client Server
ClientHello -->
ServerHello
[ChangeCipherSpec]
<-- Finished
[ChangeCipherSpec]
Finished -->
Application Data <-> Application Data
Abbreviated Handshake는 latency를 줄이기 위해 자주 사용된다. 일반적인 웹 브라우저는 SSL 연결을 Full Handshake를 통하여 열고 나머지는 모두 Abbreviated Handshake를 통하여 교신을 한다. 그리고 일반적인 웹 서버들은 약 15초간 아무일을 하지 않으면 연결을 끊지만 session ID를 기억하고 있기 때문에 언제든지 Abbreviated Handshake를 통하여 SSL 연결을 다시 열 수 있다.
Key Exchange (키 교환)
SSL/TLS이 사용할 수 있는 몇몇 키 알고리즘들이 있는데, 대부분의 키 알고리즘들은 서버의 공개키를 이용하여 동작한다. 다음은 많이 쓰이는 키 알고리즘 들이다.
- RSA: 서버의 키…….유형이 RSA 형식인 경우 사용가능하다. 클라이언트는 46바이트의 랜덤 값과 2바이트 버전을 포함한 총 48바이트의 “pre-master secret” 값을 만들어 서버의 공개키로 암호화 하여 전송한다. 이러한 경우 ServerKeyExchange 과정은 없다.
- DHE_RSA: 서버의 키 유형이 RSA 형식인 경우지만, 해당 키는 서명을 하는 경우에만 사용된다. 실제 키 교환은 Diffie-Hellman 알고리즘을 이용하여 교환하는데, 이 경우 서버는 DH 인자값들(modulus, generator)과 DH용 공개키를 포함한 ServerKeyExchange 메시지를 보낸다. (아직까진 이걸로 추천)
- DHE_DSS: DHE_RSA 키 알고리즘과 유사하게 동작하지만 서버가 DSS 키를 가지고 있는 경우 사용된다. DSS는 DSA로도 알려져 있으며 서명에만 쓰인다.
다음은 보안상이나 기타 다른 이유로 잘 쓰이지 않는 키 알고리즘들이다.
- DH: 서버의 키 유형이 Diffie-Hellman 유형인 경우 사용된다.
- DH_anon: DHE와 비슷하지만 서버의 서명이 없다. 서버의 인증서 없이 동작되기 때문에 MITM 공격에 취약하다.
- PSK: 키를 이미 기타 다른 방법으로 공유한 경우 사용되는 키 알고리즘이다.
- SRP: application of the SRP protocol which is a Password Authenticated Key Exchange protocol. Client and certificate authenticate each other with regards to a shared secret, which can be a low-entropy password (whereas PSK requires a high-entropy shared secret). Very nifty. Not widely supported yet.
- An ephermeral RSA key: DHE와 비슷하지만 RSA 키쌍을 생성한다. 그렇기 때문에 성능 부분에 대해 꽤 비싼 비용을 지불하며, 또 그렇게 매력적인 옵션은 아니다.
- ECDH_ECDSA, ECDH_RSA, ECDHE_ECDSA, ECDHE_RSA, ECDH_anon: elliptic curves를 이용한 다양한 DH* 알고리즘들로서 가장 강력하며 추후 표준이 될 수도 있다. (ECDH_anon은 제외)
현 시점에서는 elliptic curves를 이용한 DH* 알고리즘을 지원하는 클라이언트가 그리 많지 않다. 그리고 대부분의 클라이언트가 RSA 또는 DHE_RSA를 지원하기 때문에 이 2개를 먼저 고려하게 될 것이다. 이 2개중 어느것이 더 괜찮냐고 물어본다면 DHE_RSA가 더 괜찮다. 이유인즉 RSA경우 만약에 공격자가 서버의 비밀키를 획득하게 된다면 이전의 모든 SSL/TLS 전문들을 다시 복호화 할 수 있기 때문이다. DHE_RSA인 경우에는 별도로 교환된 키로 암복호화 하기 때문에 이전의 SSL/TLS 전문들을 다시 복호화 하기는 힘들다.
인증서와 인증
디지털 인증서는 비 대칭키이므로 해당 키를 어떻게 분배할 것인가에 대하여 꽤 깊은 고민을 해봐야 한다. 이유인즉, 클라이언트는 서버의 공개키를 사용하여 비밀 통신을 하고 싶지만 공격자는 중간에 자신의 공개키를 서버의 공개키라고 속이고 비밀 통신을 유도할 수 있기 때문이다. 이를 방지하기 위해 표준 인증서인 X.509를 사용한다. X.509는 공인 인증기관(Certification Authority 또는 CA)에서 서명한 인증서로서 클라이언트가 서버의 인증서가 진짜임을 CA가 서명한 것으로 판단할 수 있게 도와준다. CA 서명 기법은 다른 CA들이 추가로 서명을 해줄 수 있는데, 이렇게 함으로써 인증서의 보안성을 높이기도 한다. 이러한 인증서들은 chain of certificates라고도 하는데, 맨 처음은 root CA의 인증서부터 시작해서 중간마다 intermediate CA들 인증서, 그리고 맨 마지막에는 서버의 인증서로 끝난다. 그리고 각각의 인증서들은 이전 인증서로 서명된 값들을 포함하고 있다. 즉, 아래와 같이 클라이언트는 서버 인증서를 검증할 수 있다고 볼 수 있다.
- 서버의 인증서에서 certificate chain을 가져온다.
- 클라이언트는 각 체인의 인증서들의 서명값과 X.590 bit 값들을 검증하고, 인증서 유효기간 또한 검증한다.
- 서버 인증서에 있는 서버 이름을 검증한다. 이유인즉 다른 서버의 인증서를 그대로 가져와 사용할 수도 있기 때문이다.
Handshake Again
이론적으로는 이미 기 구축된 SSL/TLS 연결 내에서 새로운 handshake를 수행할 수 있으며, 실제적으로도 수행되며 일어나고 있다. 이를 수행하기 위해 서버는 SSL/TLS가 구축된 환경 내 아무때나 HelloRequest를 보내고 클라이언트는 ClientHello를 보내면 된다. 전형적은 흐름은 다음과 같다.
- SSL/TLS 연결 가능한 서버로 클라이언트가 handshake를 수행한 후 연결되어 있다.
- 클라이언트는 접근하고자 하는 URL를 접근하려고 하고, 서버는 해당 URL이 특정 인증서로 인증되어야만 접근 가능한 URL이다.
- 서버는 새로운 handshake를 시도한다.
이 과정에서 약간의 취약점이 있는데 이를 간략하게 설명한다면, 일반적으로 SSL/TLS 보안성을 “forward” 측면에서만 활용한다. 새로운 handshake를 수행할 때, 이전 연결에서 유효한 클라이언트에 관한 모든 것들은 새로운 handshake 이후에도 계속 유효해야 하지만 그 반대는 아니다. (이말인즉, 이후 생성된 SSL/TLS 연결에서 교환된 정보는 이전 생성된 SSL/TLS 연결에서는 유효하지 않다는 얘기이다.) 어쨋든 위 상황에서 첫 번째 요청(request)은 두 번째 생성된 SSL/TLS 연결(인증서 기반 연결)에서 보호되지 않는다. 이 점을 공격자가 노린다.
Alerts
Alert 메시지는 경고나 에러 메시지를 보낼 때 사용한다. 이 메시지중 close_notify 메시지가 중요하다. 이 메시지는 서버나 클라이언트가 연결을 끊고자 할 때 보내는 메시지이다. 해당 메시지를 서버나 클라이언트가 받으면 해당 메시지에 대한 응답을 해줘야 하고 해당 연결을 끊도록 해야 한다. 하지만 해당 세션은 abbreviated handshake로 재 연결을 하기 위해 계속 유효하다. 이 메세지는 다른 레코드들 처럼 암호화 기법으로 보호되어 있어 공격자에 의해 접근하기 힘들다. 참고로 오래된 HTTP에서의 SSLv2는 close_notify 없이 SSL/TLS 연결을 끊을 수 있도록 되어 있어서 공격자들이 이를 이용했었다. 현재는 대부분의 HTTP 서버들이 Content-Length나 chunked encoding을 사용하므로 유효하지 않다.