TLS
TLS 는 두 상대 사이에 안전한 통로를 세우는 규칙입니다. 연결을 시작할 때 양쪽이 먼저 인사를 주고받으며 쓸 열쇠를 맞춥니다. 그 다음부터 오가는 데이터는 그 열쇠로 잠긴 채 나갑니다.
쉽고 빠른 이해
TLS 는 두 상대가 통신하기 전에 안전한 통로부터 세우는 프로토콜입니다. 예를 들어 클라이언트가 ClientHello 를 보내고 서버가 ServerHello 로 답하는 것으로 시작합니다.
이게 없으면 통신 내용을 중간에서 누구나 엿보거나 몰래 바꿔치기할 수 있습니다. TLS 는 상대를 확인하고 데이터를 잠가서 이걸 막습니다.
돌아가는 방식은 이렇습니다.
- 양쪽이 먼저 핸드셰이크(인사를 주고받는 절차)로 서로를 확인하고 앞으로 쓸 열쇠를 맞춥니다
- 열쇠가 정해지면 그 뒤로 오가는 데이터를 전부 그 열쇠로 잠가서 보냅니다
- 레코드 계층 하나에 인사 메시지·실제 데이터·오류 알림 같은 여러 종류를 타입으로 구분해서 함께 실어 보냅니다
대가도 있습니다. 처음 제안한 열쇠 후보가 서로 안 맞으면 인사를 한 번 더 주고받아야 합니다. 그리고 데이터 내용은 감춰도 얼마나 오갔는지(길이)는 그대로 드러납니다.
상세
TLS(Transport Layer Security, 전송 계층 보안)의 주된 목표는 통신하는 두 상대 사이에 보안 채널을 제공하는 것입니다. 밑단 전송에 요구하는 것은 하나뿐입니다. 신뢰성 있고 순서가 지켜지는 데이터 흐름입니다. TLS 는 응용 프로토콜에 독립적입니다. 상위 프로토콜이 TLS 위에 투명하게 얹힐 수 있습니다.
TLS 는 두 구성 요소로 이루어집니다. 핸드셰이크 프로토콜과 레코드 프로토콜입니다.
핸드셰이크 프로토콜은 통신 당사자를 인증하고, 암호 모드와 파라미터를 협상하고, 공유 키 재료를 세웁니다. 이 프로토콜은 변조에 저항하도록 설계되어 있습니다. 능동 공격자가 있어도, 연결이 공격 아래 있지 않았다면 협상했을 파라미터와 다른 것을 협상하도록 강제하지 못해야 합니다. 핸드셰이크가 지원하는 키 교환 모드는 셋입니다. (EC)DHE(Diffie-Hellman over either finite fields or elliptic curves, 유한체 위나 타원곡선 위의 디피-헬만을 함께 가리키는 표기), PSK(pre-shared key, 사전 공유 키) 단독, PSK 와 (EC)DHE 를 함께 쓰는 방식입니다. 핸드셰이크가 세우는 입력 비밀들이 핸드셰이크 트랜스크립트(그때까지 오간 핸드셰이크 메시지를 순서대로 이어붙인 기록)와 함께 합쳐져 실제로 암호화에 쓰는 키 재료가 됩니다. 이 키 유도 과정은 HKDF(HMAC-based Extract-and-Expand Key Derivation Function) 의 HKDF-Extract 와 HKDF-Expand 함수를 씁니다.
레코드 프로토콜은 핸드셰이크가 세운 파라미터로 두 상대 사이의 트래픽을 보호합니다. 보낼 메시지를 받아 다룰 만한 크기로 조각내고, 레코드를 보호하고, 그 결과를 전송합니다. 받은 데이터는 검증하고, 복호하고, 다시 이어 붙여 상위 클라이언트에게 전달합니다. 명세는 이 프로토콜을 레코드 계층으로도 부릅니다 — 같은 것을 가리키는 다른 이름입니다. 레코드에는 타입이 붙습니다. 그래서 여러 상위 프로토콜을 같은 레코드 계층 위에 다중화할 수 있습니다. RFC(Request for Comments) 8446 이 정하는 콘텐츠 타입은 handshake, application_data, alert, change_cipher_spec 넷입니다.
TLS 가 응용 프로토콜과 밑단 전송 사이 어디에 앉는지, 그리고 레코드 계층이 콘텐츠 타입별로 어떻게 한 계층에 다중화하는지를 그리면 이렇습니다.
flowchart TD
subgraph 위층["응용 프로토콜"]
AP["TLS 위에 투명하게 얹히는 상위 프로토콜"]
end
subgraph 중간층["TLS"]
HP["핸드셰이크 프로토콜"]
subgraph 레코드층["레코드 계층"]
direction TD
C1["handshake"]
C2["application_data"]
C3["alert"]
end
end
subgraph 아래층["신뢰성 있는 전송"]
TR["순서가 지켜지는 데이터 흐름"]
end
AP --> C2
HP --> C1
중간층 --> 아래층
핸드셰이크 프로토콜과 레코드 계층은 함께 TLS 를 이룹니다. 핸드셰이크 프로토콜이 만든 메시지는
handshake 타입으로 실립니다. 위층 프로토콜이 보낸 데이터는 application_data 타입으로 실립니다.
오류를 발견한 쪽이 보내는 알림 메시지는 alert 타입에 실립니다. 맨 아래에는 밑단 전송이 요구받는
것 하나, 신뢰성 있고 순서가 지켜지는 데이터 흐름만 있습니다.
레코드 보호 함수는 암호화해서 TLSPlaintext 구조체를 TLSCiphertext 구조체로 바꿉니다. 보호를 벗기는 함수는 그 반대로 복호합니다. TLS 1.3 에서는 이전 판과 달리 모든 암호를 AEAD(Authenticated Encryption with Associated Data, 연관 데이터가 있는 인증 암호)로 다룹니다. AEAD 함수는 암호화와 인증을 한 연산으로 묶습니다. 암호화된 레코드 하나는 평문 헤더와 암호화된 본문으로 이루어집니다. 그 본문 안에 타입과 선택적인 패딩이 들어 있습니다. 헤더 뒤에 본문이 오고, 본문 안에서 다시 타입과 패딩이 이 차례로 놓이는 자리를 그리면 이렇습니다.
block-beta
columns 4
H["평문 헤더"]:1
block:본문:3
columns 2
T["타입"]
P["선택적 패딩"]
end
헤더는 평문으로 남고, 본문은 암호화됩니다. 그 암호화된 본문을 복호하면 타입과(있다면) 패딩이 나옵니다.
표준이 정하지 않는 자리가 있습니다. 어떤 프로토콜이 TLS 로 보안을 어떻게 더하는지는 TLS 표준이 정하지 않습니다. 핸드셰이킹을 어떻게 개시할지, 주고받은 인증서를 어떻게 해석할지는 TLS 위에서 도는 프로토콜의 설계자와 구현자 판단에 맡깁니다.
교환 순서
전체 핸드셰이크 한 벌은 클라이언트의 ClientHello 로 시작해 양쪽의 Finished 로 끝납니다. RFC 8448 은 이 한 벌을 「Simple 1-RTT Handshake」로 부릅니다. RTT(Round-Trip Time, 왕복 시간)는 한 번 오갔다 돌아오는 것을 세는 단위입니다. RFC 9001 도 TLS 1.3 에 대해, 패킷 손실이 없다면 대부분의 새 연결이 한 왕복 안에 맺어지고 보호된다고 적습니다.
아래 그림에서 명세가 쓰는 표기가 넷입니다. + 는 바로 앞 메시지에 실려 가는 눈여겨볼 확장을
가리킵니다. * 는 늘 보내지는 것이 아니라 선택적이거나 상황에 따라 달라지는 메시지와
확장입니다. {} 는 핸드셰이크 트래픽 비밀(상세에서 다룬 키 유도 과정이 핸드셰이크 도중에
내놓는 키 재료)에서 유도한 키로 보호되는 메시지입니다. [] 는 애플리케이션 트래픽 비밀(같은
키 유도 과정이 핸드셰이크가 끝난 뒤 단계에서 내놓는 키 재료)에서 유도한 키로 보호되는
메시지입니다.
sequenceDiagram
participant 클라이언트
participant 서버
클라이언트->>서버: ClientHello + key_share*
서버-->>클라이언트: ServerHello + key_share*
서버-->>클라이언트: {EncryptedExtensions} {CertificateRequest*}
서버-->>클라이언트: {Certificate*} {CertificateVerify*} {Finished}
클라이언트->>서버: {Certificate*} {CertificateVerify*} {Finished}
클라이언트->>서버: [Application Data]
핸드셰이크는 세 단계로 나뉩니다.
| 단계 | 하는 일 | 이 단계의 메시지 |
|---|---|---|
| Key Exchange | 공유 키 재료를 세우고 암호 파라미터를 고른다. 이 단계 뒤로는 전부 암호화된다 | ClientHello · ServerHello |
| Server Parameters | 나머지 핸드셰이크 파라미터를 정한다. 클라이언트를 인증할지, 응용 계층 프로토콜을 무엇으로 할지 | EncryptedExtensions · CertificateRequest |
| Authentication | 서버를 인증한다. 선택적으로 클라이언트도. 키 확인과 핸드셰이크 무결성을 준다 | Certificate · CertificateVerify · Finished |
Key Exchange 단계에서 클라이언트가 ClientHello 를 보냅니다. 여기에는 난스(무작위로 고른 값) 하나(ClientHello.random), 제안하는 프로토콜 판 목록, 대칭 암호와 HKDF 해시 쌍의 목록이 들어갑니다. 그리고 key_share(이 쪽이 계산한 디피-헬만 공개 값들) 확장에 담은 디피-헬만 키 셰어 묶음, pre_shared_key 확장에 담은 PSK(사전 공유 키) 레이블 묶음, 또는 그 둘 다가 들어갑니다. 미들박스(중간에서 트래픽을 다루는 장비. 새 프로토콜 값을 받으면 오동작하기도 합니다) 호환을 위한 필드나 메시지가 더 붙기도 합니다.
서버는 ClientHello 를 처리해 이 연결에 맞는 암호 파라미터를 정합니다. 그리고 협상된 연결 파라미터를 담은 자기 ServerHello 로 답합니다. 공유 키는 ClientHello 와 ServerHello 의 조합이 정합니다.
이어서 서버가 Server Parameters 를 세우는 메시지 둘을 보냅니다. EncryptedExtensions 는 암호 파라미터를 정하는 데 필요하지 않은 ClientHello 확장들에 대한 응답입니다. 개별 인증서에 딸린 확장은 여기서 빠집니다. CertificateRequest 는 인증서 기반 클라이언트 인증을 원할 때 보냅니다. 클라이언트 인증을 원하지 않으면 이 메시지는 생략됩니다.
마지막으로 클라이언트와 서버가 Authentication 메시지를 주고받습니다. 인증서 기반 인증이 필요할 때마다 TLS 는 언제나 같은 메시지 묶음(Certificate · CertificateVerify · Finished)을 씁니다. PSK 기반 인증은 이 묶음을 따로 쓰지 않고 키 교환의 부수 효과로 일어납니다 — 아래 Finished 가 PSK 모드에서 핸드셰이크 자체를 인증하는 것이 그 자리입니다. Certificate 는 끝점의 인증서와 인증서별 확장을 담습니다. 서버가 인증서로 인증하지 않으면 서버 쪽 Certificate 가 생략됩니다. 서버가 CertificateRequest 를 보내지 않았으면 클라이언트 쪽 Certificate 도 생략됩니다. CertificateVerify 는 핸드셰이크 전체에 대한 서명입니다. Certificate 메시지의 공개 키에 대응하는 개인 키로 서명합니다. Finished 는 핸드셰이크 전체에 대한 MAC(Message Authentication Code, 메시지 인증 코드)입니다. 이 메시지가 키 확인을 주고, 끝점의 신원을 교환된 키에 묶고, PSK 모드에서는 핸드셰이크 자체도 인증합니다.
서버의 메시지를 받은 클라이언트는 자기 Authentication 메시지로 답합니다. 요청받았다면 Certificate 와 CertificateVerify 를, 그리고 Finished 를 보냅니다. 여기서 핸드셰이크가 끝납니다. 양쪽은 레코드 계층이 애플리케이션 데이터를 인증 암호로 보호해 주고받는 데 필요한 키 재료를 유도합니다. Application Data 는 Finished 를 보내기 전에는 보내면 안 됩니다. 예외는 명세가 따로 정한 자리뿐입니다.
키 셰어가 어긋날 때
클라이언트가 충분한 key_share 확장을 주지 못했을 때가 있습니다. 그 한 예로 서버가 받아들이지 않거나 지원하지 않는 DHE(유한체 위의 디피-헬만)·ECDHE(타원곡선 위의 디피-헬만) 그룹만 넣은 경우를 명세가 듭니다. 그러면 서버가 HelloRetryRequest 로 그 어긋남을 바로잡고, 클라이언트는 알맞은 key_share 확장을 달아 핸드셰이크를 다시 시작해야 합니다. 왕복이 한 번 늘어납니다.
sequenceDiagram
participant 클라이언트
participant 서버
클라이언트->>서버: ClientHello + key_share
서버-->>클라이언트: HelloRetryRequest + key_share
클라이언트->>서버: ClientHello + key_share
서버-->>클라이언트: ServerHello + key_share
Note over 클라이언트,서버: 이 뒤는 전체 핸드셰이크와 같습니다
핸드셰이크 트랜스크립트는 처음의 ClientHello 와 HelloRetryRequest 교환까지 포함합니다. 새 ClientHello 로 초기화되지 않습니다. 공통 암호 파라미터를 하나도 협상하지 못하면 서버는 알맞은 alert 로 핸드셰이크를 중단해야 합니다.
세션 재개와 PSK
기본 핸드셰이크에는 최적화된 변형이 여럿 있습니다. 세션 재개가 그중 하나입니다. 핸드셰이크가 한 번 끝나면 서버는 클라이언트에게 PSK 신원을 보낼 수 있습니다. 최초 핸드셰이크에서 유도한 고유한 키에 대응하는 값입니다. 클라이언트는 이후 핸드셰이크에서 그 PSK 신원으로 해당 PSK 사용을 협상합니다. 서버가 그 PSK 를 받아들이면 새 연결의 보안 맥락이 원래 연결에 암호적으로 묶입니다. 전체 핸드셰이크 대신 최초 핸드셰이크에서 유도한 키가 암호 상태를 부트스트랩합니다. TLS 1.2 와 그 아래에서는 세션 ID 와 세션 티켓이 이 기능을 맡았습니다. 두 메커니즘 모두 TLS 1.3 에서 폐기되었습니다.
sequenceDiagram
participant 클라이언트
participant 서버
Note over 클라이언트,서버: 최초 핸드셰이크
클라이언트->>서버: ClientHello
서버-->>클라이언트: ServerHello ... Finished
Note over 클라이언트,서버: 핸드셰이크 완료 뒤
서버-->>클라이언트: PSK 신원
Note over 클라이언트,서버: 이후 새 핸드셰이크
클라이언트->>서버: ClientHello + pre_shared_key(PSK 신원)
서버-->>클라이언트: ServerHello (PSK 사용 수락)
Note over 클라이언트,서버: 새 연결의 보안 맥락이 최초 연결에 암호적으로 묶입니다
최초 핸드셰이크가 끝나야 PSK 신원이 나갑니다. 그 신원을 클라이언트가 다음 핸드셰이크의 ClientHello 에 실어 보내야 서버가 PSK 사용을 협상합니다. 서버가 받아들이는 차례가 마지막입니다 — 그 전에는 새 연결과 최초 연결이 아직 엮이지 않습니다.
예시
RFC 8448 이 실제 핸드셰이크 트레이스를 싣고 있습니다. 서버는 인증되고 클라이언트는 익명으로 남는 가장 단순한 한 벌입니다.
ClientHello
클라이언트가 먼저 임시 x25519(이 예시가 고른 타원곡선 디피-헬만의 곡선 하나) 키 쌍을 만듭니다. 공개 키는 32 옥텟입니다.
public key (32 octets): 99 38 1d e5 60 e4 bd 43 d2 3d 8e 43 5a 7d
ba fe b3 c0 6e 51 c1 3c ae 4d 54 13 69 1e 52 9a af 2c
그리고 ClientHello 를 만듭니다. 196 옥텟입니다.
ClientHello (196 octets): 01 00 00 c0 03 03 cb 34 ec b1 e7 81 63
ba 1c 38 c6 da cb 19 6a 6d ff a2 1a 8d 99 12 ec 18 a2 ef 62 83
02 4d ec e7 00 00 06 13 01 13 03 13 02 01 00 00 91 00 00 00 0b
00 09 00 00 06 73 65 72 76 65 72 ff 01 00 01 00 00 0a 00 14 00
12 00 1d 00 17 00 18 00 19 01 00 01 01 01 02 01 03 01 04 00 23
00 00 00 33 00 26 00 24 00 1d 00 20 99 38 1d e5 60 e4 bd 43 d2
3d 8e 43 5a 7d ba fe b3 c0 6e 51 c1 3c ae 4d 54 13 69 1e 52 9a
af 2c 00 2b 00 03 02 03 04 00 0d 00 20 00 1e 04 03 05 03 06 03
02 03 08 04 08 05 08 06 04 01 05 01 06 01 02 01 04 02 05 02 06
02 02 02 00 2d 00 02 01 01 00 1c 00 02 40 01
이 덤프가 담고 있는 것이 명세의 ClientHello 구조체입니다. 값이 나오는 차례는 구조체가 필드를 적은 차례와 같습니다.
struct {
ProtocolVersion legacy_version = 0x0303; /* TLS v1.2 */
Random random;
opaque legacy_session_id<0..32>;
CipherSuite cipher_suites<2..2^16-2>;
opaque legacy_compression_methods<1..2^8-1>;
Extension extensions<8..2^16-1>;
} ClientHello;
여섯 필드가 이 차례로 이어 붙어 몸통을 이룹니다. 옥텟과 바이트는 둘 다 8비트를 가리키는 같은 크기입니다 — 여기서부터는 바이트로 부릅니다. 위 덤프에서 각 필드가 차지하는 자리를 바이트 수 그대로(1 칸 = 1 바이트) 그리면 이렇습니다.
block-beta columns 192 A["legacy_version"]:2 B["random"]:32 C["legacy_session_id"]:1 D["cipher_suites"]:8 E["legacy_compression_methods"]:2 F["extensions"]:147
extensions 한 필드가 몸통의 대부분을 차지합니다. legacy_session_id 는 자리만 있고 비어 있어
가장 좁습니다. 여섯 필드의 합은 2+32+1+8+2+147=192 옥텟입니다. 앞서 말한 196 옥텟과 4 옥텟
차이가 나는데, 그 4 옥텟은 이 구조체 밖에 있는 핸드셰이크 메시지 포장입니다 — 덤프 맨 앞
01(메시지 타입)과 00 00 c0(길이, 0xc0=192)입니다.
03 03 이 legacy_version 입니다. 구조체가 이 값을 0x0303 으로 못 박아 두었습니다. 뒤이은
cb 34 ... 4d ec e7 32 옥텟이 random 입니다. 그다음 00 이 legacy_session_id 의 길이이고,
여기서는 비어 있습니다. 00 06 은 cipher_suites 의 길이입니다. 여섯 바이트, 곧 두 바이트짜리
CipherSuite 셋입니다. 13 01, 13 03, 13 02 가 클라이언트가 제안한 세 벌입니다. 이어지는
01 00 이 legacy_compression_methods 입니다. 00 91 부터가 확장 목록의 길이와 내용입니다.
확장 안에서 눈에 띄는 대목이 key_share 입니다. 00 33 은 명세의 확장 번호표에서
key_share(51) 입니다. 그 뒤 ... 00 20 다음에 오는 32 바이트가 위에서 만든 클라이언트의 x25519
공개 키와 정확히 같습니다. 그다음 00 2b 은 supported_versions(43) 이고 값이 03 04 입니다.
ServerHello
서버도 임시 x25519 키 쌍을 만듭니다.
public key (32 octets): c9 82 88 76 11 20 95 fe 66 76 2b db f7 c6
72 e1 56 d6 cc 25 3b 83 3d f1 dd 69 b1 b0 4e 75 1f 0f
그리고 ServerHello 로 답합니다. 90 옥텟입니다.
ServerHello (90 octets): 02 00 00 56 03 03 a6 af 06 a4 12 18 60
dc 5e 6e 60 24 9c d3 4c 95 93 0c 8a c5 cb 14 34 da c1 55 77 2e
d3 e2 69 28 00 13 01 00 00 2e 00 33 00 24 00 1d 00 20 c9 82 88
76 11 20 95 fe 66 76 2b db f7 c6 72 e1 56 d6 cc 25 3b 83 3d f1
dd 69 b1 b0 4e 75 1f 0f 00 2b 00 02 03 04
여기도 같은 방식으로 명세의 ServerHello 구조체와 견줘 봅니다.
struct {
ProtocolVersion legacy_version = 0x0303; /* TLS v1.2 */
Random random;
opaque legacy_session_id_echo<0..32>;
CipherSuite cipher_suite;
uint8 legacy_compression_method = 0;
Extension extensions<6..2^16-1>;
} ServerHello;
같은 축척(1 칸 = 1 바이트)으로 자리를 그리면 이렇습니다.
block-beta columns 86 A["legacy_version"]:2 B["random"]:32 C["legacy_session_id_echo"]:1 D["cipher_suite"]:2 E["legacy_compression_method"]:1 F["extensions"]:48
ClientHello 보다 몸통이 훨씬 작습니다. 여섯 필드의 합이 86 옥텟으로, ClientHello 몸통 192
옥텟의 절반에도 못 미칩니다. 여기서도 4 옥텟 차이(90 - 86)는 핸드셰이크 메시지 포장입니다 —
덤프 맨 앞 02(메시지 타입)와 00 00 56(길이, 0x56=86)입니다. cipher_suite 와
legacy_compression_method 는 길이 접두사 없이 값 하나만 담아 ClientHello 쪽
cipher_suites·legacy_compression_methods 보다 좁고, random 과 extensions 둘이서 나머지
대부분을 차지합니다.
03 03 이 legacy_version 입니다. 이 자리가 0x0303 으로 고정된 데에는 사연이 있습니다. 이전
판에서는 이 필드가 판 협상에 쓰였고 이 연결에 고른 판 번호를 담았습니다. 그런데 새 값을 받으면
실패하는 미들박스가 있었습니다. 그래서 TLS 1.3 서버는 supported_versions 확장으로 자기 판을
알리고, legacy_version 필드는 TLS 1.2 의 판 번호인 0x0303 으로 두어야 합니다.
a6 af ... d3 e2 69 28 32 옥텟이 random 입니다. 00 이 legacy_session_id_echo 의 길이입니다.
13 01 이 cipher_suite 입니다. 서버가 ClientHello.cipher_suites 목록에서 고른 한 벌입니다.
앞서 클라이언트가 제안한 셋 가운데 첫째와 같습니다. 제안하지 않은 cipher suite 를 받은
클라이언트는 illegal_parameter alert 로 핸드셰이크를 중단해야 합니다. 이어지는 00 이
legacy_compression_method 이고 00 2e 부터가 확장입니다.
ServerHello 의 확장은 둘뿐입니다. 00 33 으로 시작하는 key_share 와 00 2b 으로 시작하는
supported_versions 입니다. key_share 안의 32 바이트는 서버의 x25519 공개 키와 같습니다.
ServerHello 는 암호 맥락을 세우고 프로토콜 판을 협상하는 데 필요한 확장만 담아야 합니다.
그리고 모든 TLS 1.3 ServerHello 는 supported_versions 확장을 담아야 합니다.
실패
TLS 의 오류 처리는 단순합니다. 오류를 발견한 쪽이 상대에게 메시지를 보냅니다. 치명적 alert 메시지를 보내거나 받으면 양쪽 모두 즉시 연결을 닫아야 합니다. 구현이 치명적 오류 상황을 만나면 알맞은 치명적 alert 를 보내는 편이 좋고, 데이터를 더 보내거나 받지 않고 연결을 닫아야 합니다.
| alert | 언제 오나 |
|---|---|
unexpected_message |
부적절한 메시지를 받았다. 잘못된 핸드셰이크 메시지, 이른 Application Data 등 |
bad_record_mac |
보호를 벗길 수 없는 레코드를 받았다. AEAD 는 복호와 검증을 한 연산으로 묶는다. 그래서 복호 실패와 검증 실패를 가리지 않고, 부채널 공격(처리 시간이나 소비 전력 같은 곁가지 정보로 비밀을 추측하는 공격)을 피하기 위해서도 모든 보호 해제 실패를 이 값 하나로 알린다 |
handshake_failure |
주어진 선택지로는 받아들일 만한 보안 파라미터 묶음을 협상하지 못했다 |
bad_certificate |
인증서가 손상되었거나 검증되지 않는 서명을 담고 있다 |
unsupported_certificate |
지원하지 않는 종류의 인증서였다 |
certificate_revoked |
인증서가 서명자에게 폐기당했다 |
certificate_expired |
인증서가 만료되었거나 현재 유효하지 않다 |
certificate_unknown |
인증서 처리에서 그 밖의 문제가 생겨 받아들일 수 없었다 |
illegal_parameter |
핸드셰이크의 한 필드가 부정확하거나 다른 필드와 어긋난다. 형식 문법은 맞는데 내용이 틀린 오류에 쓴다 |
unknown_ca |
유효한 인증서 체인을 받았지만 CA(Certification Authority, 인증기관) 인증서를 찾지 못했거나 알려진 신뢰 앵커와 맞추지 못했다 |
access_denied |
유효한 인증서나 PSK 를 받았지만 접근 제어를 적용한 뒤 협상을 진행하지 않기로 했다 |
protocol_version |
상대가 협상하려 한 프로토콜 판을 알아보기는 하지만 지원하지 않는다 |
unexpected_message 는 제대로 된 구현끼리의 통신에서는 관측될 일이 없어야 합니다.
보장과 가정
명세가 파는 것은 셋입니다. 각각이 무엇 위에 서 있는지가 함께 적혀 있습니다.
인증
채널의 서버 쪽은 언제나 인증됩니다. 클라이언트 쪽은 선택적으로 인증됩니다. 인증은 비대칭 암호로 일어날 수도 있고 대칭 사전 공유 키로 일어날 수도 있습니다. 비대칭 쪽 예로 명세는 RSA, ECDSA(Elliptic Curve Digital Signature Algorithm, 타원곡선 전자서명 알고리즘), EdDSA(Edwards-Curve Digital Signature Algorithm, 에드워즈 곡선 전자서명 알고리즘)를 듭니다.
이 보장은 인증서 검증 경로 위에 서 있습니다. 서버가 보내는 인증서는 명시적으로 달리 협상하지
않는 한 X.509v3 이어야 합니다. 서버의 종단 개체 인증서 공개 키는 클라이언트의
signature_algorithms 확장에서 고른 인증 알고리즘과 호환되어야 합니다. 이 가정이 깨지는 자리는
alert 로 드러납니다. 유효한 체인을 받았는데 CA 인증서를 찾지 못했거나 알려진 신뢰 앵커와 맞추지
못하면 unknown_ca 가 오고 연결이 닫힙니다. 인증서가 만료되었으면 certificate_expired,
폐기되었으면 certificate_revoked 입니다.
기밀성
채널이 세워진 뒤에 채널로 보낸 데이터는 양 끝점에만 보입니다.
감춰지지 않는 것이 있습니다. TLS 는 전송하는 데이터의 길이를 감추지 않습니다. 다만 끝점이 TLS 레코드에 패딩을 넣어 길이를 흐리고 트래픽 분석 기법에 대한 보호를 개선할 수는 있습니다.
무결성
채널이 세워진 뒤에 채널로 보낸 데이터는 공격자가 탐지되지 않은 채로 고칠 수 없습니다.
핸드셰이크 자체에도 같은 성질이 걸려 있습니다. 핸드셰이크 프로토콜은 변조에 저항하도록 설계되어 있습니다. 능동 공격자가 있어도, 연결이 공격 아래 있지 않았다면 협상했을 파라미터와 다른 것을 협상하도록 강제하지 못해야 합니다.
이 셋이 서 있는 자리
세 성질은 네트워크를 완전히 장악한 공격자 앞에서도 참이어야 한다고 적혀 있습니다.
밑단 전송에 대한 요구는 하나뿐입니다. 신뢰성 있고 순서가 지켜지는 데이터 흐름입니다. 그 아래가 어떻게 생겼는지는 TLS 가 묻지 않습니다.
암호 파라미터는 핸드셰이크가 협상해서 정합니다. 공통 파라미터를 하나도 협상하지 못하면 서버는 알맞은 alert 로 핸드셰이크를 중단해야 합니다. 곧 보장이 무너지는 것이 아니라 연결이 서지 않습니다.
응용 계층과의 경계에도 가정이 하나 있습니다. TLS 표준은 프로토콜이 TLS 로 보안을 어떻게 더하는지를 정하지 않습니다. 핸드셰이킹 개시와 인증서 해석은 TLS 위에서 도는 프로토콜의 설계자와 구현자 판단에 맡겨져 있습니다. 그 판단이 틀리면 TLS 자신은 그대로여도 그 위의 보안이 서지 않습니다.
0-RTT 의 재전송 노출
0-RTT 는 왕복을 하나도 기다리지 않고 먼저 보내는 데이터입니다. 명세는 이 데이터를 early data 라고도 부릅니다. TLS 는 이 데이터에 대해 내재적인 재전송 보호를 주지 않습니다. 명세가 경계하는 위협은 둘입니다. 0-RTT 데이터 한 뭉치를 그대로 복제해 재전송 공격을 거는 네트워크 공격자가 첫째입니다. 클라이언트의 재시도 동작을 이용해 서버가 같은 애플리케이션 메시지를 여러 벌 받게 만드는 공격자가 둘째입니다.
첫째 부류는 0-RTT 데이터가 많아야 한 번만 받아들여지도록 상태를 공유해 막을 수 있습니다. 서버는 명세가 기술한 방법 가운데 하나를 구현하거나 동등한 수단으로 그 수준의 재전송 안전성을 제공하는 편이 좋습니다. 다만 운영상의 이유로 모든 배포가 그 수준의 상태를 유지하지는 않을 것이라는 점이 명세 자신에 적혀 있습니다. 그래서 정상 동작에서 클라이언트는 서버가 실제로 어느 메커니즘을 구현하는지 알 수 없습니다. 클라이언트는 재전송되어도 안전하다고 판단하는 early data 만 보내야 합니다.
둘째 부류는 TLS 계층에서 막을 수 없습니다. 애플리케이션이 다뤄야 합니다.
관련 항목
핸드셰이크를 이루는 구성 요소
인증서 · X.509 · 신뢰 앵커 · 인증기관 · 인증서 체인 · 인증서 폐기 · 공개 키 기반구조 · 키 교환 · 디피-헬만 · 타원곡선 암호 · x25519 · ECDSA · EdDSA · RSA · 사전 공유 키 · 키 유도 · HKDF · 핸드셰이크 트랜스크립트 · 핸드셰이크 트래픽 비밀 · 애플리케이션 트래픽 비밀 · 난스 · 세션 재개 · 세션 티켓 · 0-RTT · early data · 재전송 공격 · 클라이언트 인증서 인증
레코드를 이루는 구성 요소
AEAD · 인증 암호 · 레코드 계층 · 콘텐츠 타입 · 패딩 · 트래픽 분석 · 다중화 · MAC · 프래그멘테이션 · 부채널 공격
핸드셰이크에 실리는 확장
server_name · SNI(Server Name Indication, 서버 이름 표시) · ALPN(Application-Layer Protocol Negotiation, 응용 계층 프로토콜 협상) · supported_versions · key_share · signature_algorithms · supported_groups · pre_shared_key · psk_key_exchange_modes · early_data · cookie · certificate_authorities · post_handshake_auth · status_request · max_fragment_length · padding · use_srtp · heartbeat
실패로 만나는 값
handshake_failure · bad_record_mac · bad_certificate · unknown_ca · certificate_expired · certificate_revoked · illegal_parameter · protocol_version · unexpected_message · access_denied · HelloRetryRequest
위아래로 이웃하는 계층
TCP(Transmission Control Protocol, 전송 제어 프로토콜) · QUIC · 신뢰성 있는 전송 · 응용 계층 프로토콜
TLS 가 맞서는 방해자와 공격자
미들박스 · 능동 공격자 · 중간자 공격
다른 이름: Transport Layer Security · 전송 계층 보안