TLS 핸드셰이크
TLS 1.2와 1.3이 암호 조합·서버 신원·트래픽 키를 합의하는 흐름과 인증서 체인 검증을 추적합니다.
이 절은 TCP 위에서 TLS를 쓰는 HTTPS 연결을 다룹니다. HTTP/3는 QUIC에 TLS 1.3 핸드셰이크를 결합하므로 아래 TCP 왕복 계산과 구분합니다.
먼저 TLS 핸드셰이크로 “어떤 버전과 암호 조합을 쓸지”, “서버가 그 서비스 이름에 맞는 인증서를 제시했는지”, “이번 연결에만 쓸 트래픽 키를 어떻게 만들지”를 합의합니다.
TLS 핸드셰이크가 정하는 것
인증서로 서버를 인증하는 전체 핸드셰이크의 결과는 크게 세 가지입니다.
첫째, 클라이언트와 서버가 TLS 버전과 암호 조합을 고릅니다.
둘째, 인증서와 서명으로 서버 신원을 확인합니다.
셋째, ECDHE 같은 임시 키 교환으로 공유 비밀을 만들고, 거기서 이번 연결의 여러 트래픽 키를 파생합니다.
전체 핸드셰이크의 왕복
TLS 1.2와 1.3의 전체 핸드셰이크 왕복
두 버전에서 클라이언트의 첫 메시지를 보냅니다. 1.2의 서버 CCS·Finished를 확인합니다. 각 단계의 값과 전환 이유를 비교합니다.
TLS · FULL HANDSHAKE
HTTP 요청까지, 왕복 한 번의 차이
TLS 1.3은 첫 메시지에 키 공유값을 담아 서버의 인증과 완료 응답을 한 번에 받습니다. TCP 연결이 끝난 뒤부터 비교합니다.
HTTP 요청까지 2 RTT
TLS 1.2 · 키 교환 후 완료 확인
서버의 Finished를 받아야 요청을 보냅니다.
2 RTT 뒤 클라이언트 → 서버: 암호화한 HTTP 요청.
HTTP 요청까지 1 RTT
TLS 1.3 · 첫 메시지부터 키 공유
서버 인증 후 Finished와 요청을 이어 보냅니다.
1 RTT 뒤 요청을 시작합니다. ServerHello 뒤 핸드셰이크도 암호화됩니다.
- 세로 점선: 시간의 흐름 ↓
- 가로 화살표: 메시지 묶음
- TLS 1.3의 핵심 흐름 / HTTP 요청
*는 암호화된 메시지입니다. CCS는 ChangeCipherSpec이며 TLS 1.2의 암호 상태를 전환합니다. HTTP 요청은 핸드셰이크 메시지가 아니라 애플리케이션 데이터입니다. 화살표 수는 패킷 개수를 뜻하지 않습니다.
인증서 기반 ECDHE의 정상 경로만 비교합니다. TCP 연결 시간은 별도이며, 재개·0-RTT·HelloRetryRequest·클라이언트 인증·TLS 1.2 False Start는 제외했습니다. TLS 1.3의 호환성용 CCS는 생략했습니다.
이 예의 조건
- 본문과 전체 도표의 예를 따라 ClientHello부터 1.2 서버 완료까지 진행합니다.
관계도의 보충 설명
| 구분 | 전체 도표의 내용 |
|---|---|
| 제목 | TLS 1.2와 1.3의 전체 핸드셰이크 왕복 |
| 관계 설명 | 시간은 위에서 아래로 흐릅니다. ClientHello, 서버의 ServerHello·Certificate·ServerKeyExchange·ServerHelloDone, 클라이언트의 ClientKeyExchange·CCS·Finished, 서버의 CCS·Finished 순서 뒤 클라이언트가 암호화 HTTP 요청을 보냅니다. 정상 인증서 기반 ECDHE 전체 핸드셰이크이며 요청까지 TLS 2 RTT, 별표는 암호화된 메시지입니다. |
| 관계 설명 | 시간은 위에서 아래로 흐릅니다. ClientHello와 key_share를 보낸 뒤 서버는 ServerHello와 key_share, 암호화한 EncryptedExtensions·Certificate·CertificateVerify·Finished를 반환합니다. 클라이언트는 서버 인증과 Finished 검증 뒤 자기 Finished와 암호화 HTTP 요청을 이어 보냅니다. 정상 인증서 기반 ECDHE이며 요청까지 TLS 1 RTT입니다. EncryptedExtensions 이름은 화면에서 두 줄로 배치했습니다. 별표는 암호화된 메시지입니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| ClientHello 두 버전에서 클라이언트의 첫 메시지를 보냅니다. |
|
|
| 서버 응답 서버의 키 교환과 인증 정보를 받습니다. |
|
|
| 클라이언트 완료 클라이언트의 완료 메시지를 보냅니다. |
|
|
| 1.2 서버 완료 1.2의 서버 CCS·Finished를 확인합니다. |
|
|
TLS 1.2와 1.3의 전체 핸드셰이크 왕복
두 버전에서 클라이언트의 첫 메시지를 보냅니다. 1.2의 서버 CCS·Finished를 확인합니다. 각 단계의 값과 전환 이유를 비교합니다.
TLS · FULL HANDSHAKE
HTTP 요청까지, 왕복 한 번의 차이
TLS 1.3은 첫 메시지에 키 공유값을 담아 서버의 인증과 완료 응답을 한 번에 받습니다. TCP 연결이 끝난 뒤부터 비교합니다.
HTTP 요청까지 2 RTT
TLS 1.2 · 키 교환 후 완료 확인
서버의 Finished를 받아야 요청을 보냅니다.
2 RTT 뒤 클라이언트 → 서버: 암호화한 HTTP 요청.
HTTP 요청까지 1 RTT
TLS 1.3 · 첫 메시지부터 키 공유
서버 인증 후 Finished와 요청을 이어 보냅니다.
1 RTT 뒤 요청을 시작합니다. ServerHello 뒤 핸드셰이크도 암호화됩니다.
- 세로 점선: 시간의 흐름 ↓
- 가로 화살표: 메시지 묶음
- TLS 1.3의 핵심 흐름 / HTTP 요청
*는 암호화된 메시지입니다. CCS는 ChangeCipherSpec이며 TLS 1.2의 암호 상태를 전환합니다. HTTP 요청은 핸드셰이크 메시지가 아니라 애플리케이션 데이터입니다. 화살표 수는 패킷 개수를 뜻하지 않습니다.
인증서 기반 ECDHE의 정상 경로만 비교합니다. TCP 연결 시간은 별도이며, 재개·0-RTT·HelloRetryRequest·클라이언트 인증·TLS 1.2 False Start는 제외했습니다. TLS 1.3의 호환성용 CCS는 생략했습니다.
이 예의 조건
- 본문과 전체 도표의 예를 따라 ClientHello부터 1.2 서버 완료까지 진행합니다.
관계도의 보충 설명
| 구분 | 전체 도표의 내용 |
|---|---|
| 제목 | TLS 1.2와 1.3의 전체 핸드셰이크 왕복 |
| 관계 설명 | 시간은 위에서 아래로 흐릅니다. ClientHello, 서버의 ServerHello·Certificate·ServerKeyExchange·ServerHelloDone, 클라이언트의 ClientKeyExchange·CCS·Finished, 서버의 CCS·Finished 순서 뒤 클라이언트가 암호화 HTTP 요청을 보냅니다. 정상 인증서 기반 ECDHE 전체 핸드셰이크이며 요청까지 TLS 2 RTT, 별표는 암호화된 메시지입니다. |
| 관계 설명 | 시간은 위에서 아래로 흐릅니다. ClientHello와 key_share를 보낸 뒤 서버는 ServerHello와 key_share, 암호화한 EncryptedExtensions·Certificate·CertificateVerify·Finished를 반환합니다. 클라이언트는 서버 인증과 Finished 검증 뒤 자기 Finished와 암호화 HTTP 요청을 이어 보냅니다. 정상 인증서 기반 ECDHE이며 요청까지 TLS 1 RTT입니다. EncryptedExtensions 이름은 화면에서 두 줄로 배치했습니다. 별표는 암호화된 메시지입니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| ClientHello 두 버전에서 클라이언트의 첫 메시지를 보냅니다. |
|
|
| 서버 응답 서버의 키 교환과 인증 정보를 받습니다. |
|
|
| 클라이언트 완료 클라이언트의 완료 메시지를 보냅니다. |
|
|
| 1.2 서버 완료 1.2의 서버 CCS·Finished를 확인합니다. |
|
|
일반적인 TLS 1.2 full handshake는 TLS만 보면 2 RTT가 필요합니다.
TCP 3-way handshake까지 합치면, HTTP 요청을 안전하게 보내기까지 보통 3 RTT가 걸립니다.
단, 세션 재개, False Start, TCP Fast Open 같은 최적화가 있으면 실제 지연은 달라질 수 있습니다.
- 버전 협상: TLS 1.2 — ClientHello, ServerHello; TLS 1.3 — ClientHello의 supported_versions 확장으로 협상.
- 키 교환: TLS 1.2 — RSA key transport, DHE, ECDHE 등이 가능; TLS 1.3 — 일반 full handshake는 (EC)DHE key_share, 재개는 PSK 또는 PSK+(EC)DHE.
- 인증: TLS 1.2 — 인증서와 ServerKeyExchange 서명 등; TLS 1.3 — CertificateVerify로 핸드셰이크 transcript에 서명.
- 데이터 보호: TLS 1.2 — CBC, GCM 등 여러 방식; TLS 1.3 — AEAD 기반 cipher suite만 사용.
예시: 요청을 시작하는 시각 계산 새 TCP 연결을 만들고 RTT가 40ms로 일정하다고 합시다. 재개·False Start·HelloRetryRequest는 없고 DNS 조회, 서버 처리, 전송량에 따른 시간도 제외합니다. TCP 연결에 1 RTT, TLS 1.2 전체 핸드셰이크에 2 RTT가 필요하므로 첫 HTTP 요청을 보낼 준비까지 (1+2)×40=120ms입니다. TLS 1.3의 정상 경로는 (1+1)×40=80ms입니다. 서버 응답을 받는 시간을 묻는다면 HTTP 요청·응답의 왕복과 처리 시간을 별도로 더해야 합니다. 80ms를 페이지 전체의 다운로드 시간이라고 읽지 않습니다.
왕복 조건을 바꾸어 계산하기 RTT가 60ms인 새 TCP 연결의 TLS 1.3에서 HelloRetryRequest로 핸드셰이크 왕복이 한 번 더 필요했습니다. 위와 같은 제외 조건에서 첫 HTTP 요청을 보낼 준비까지 얼마일까요?
TCP와 TLS를 분리한 풀이
TCP의 1 RTT에 TLS의 정상 1 RTT와 추가 1 RTT를 합하므로 3×60=180ms입니다. 이 조건에서는 일반 TLS 1.2 전체 핸드셰이크의 요청 준비 시간과 같아집니다. 연결 재사용이나 서버가 응답을 만드는 시간까지 같은 것으로 비교한 것은 아닙니다. TLS 1.3의 전체 흐름과 재시도를 기준으로 계산 조건을 먼저 고릅니다.
Cipher Suite 읽기
TLS 1.2의 cipher suite 이름은 키 교환, 인증, 대칭 암호, 해시가 한 줄에 들어 있습니다.
예를 들어 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256은 ECDHE로 공유 비밀을 만들고, RSA 인증서 키로 서버 서명을 검증하며, AES-128-GCM으로 레코드를 보호하고, SHA-256을 키 유도와 Finished 검증에 사용한다는 뜻입니다.
TLS 1.3의 cipher suite 이름은 더 짧습니다.
TLS_AES_128_GCM_SHA256처럼 데이터 보호 알고리즘과 해시만 담고, 키 교환과 인증 방식은 별도 확장과 인증서 알고리즘으로 협상합니다.
알고리즘 이름을 독립 해석하기 TLS_AES_256_GCM_SHA384를 협상했다면 AES의 키 길이와 해시 출력 길이는 각각 무엇인가요? 서버 인증서가 RSA인지 ECDSA인지도 이 이름만으로 알 수 있을까요?
암호 조합의 서로 다른 숫자
AES 키는 256비트이고 SHA-384의 출력은 384비트입니다. 해시 이름의 숫자를 AES의 키 길이로 바꾸어 읽지 않습니다. TLS 1.3 suite 이름에는 인증서의 서명 알고리즘이 없으므로 RSA나 ECDSA라는 결론은 따로 확인해야 합니다. 협상한 그룹과 서명 알고리즘도 suite 이름과 구분합니다.
디피-헬만 키 교환
ECDHE(Elliptic Curve Diffie-Hellman Ephemeral)는 현재 TLS에서 가장 중요한 키 교환 방식입니다.
양쪽은 사용할 곡선 그룹과 공개 key share를 교환하며, 각자의 임시 비밀값은 보내지 않습니다. 기준점은 선택한 그룹의 정의에 포함됩니다.
양쪽은 상대의 공개값과 자신의 비밀값을 조합해 같은 공유 비밀에 도달합니다.
다만 순수한 디피-헬만만으로는 상대가 누구인지 인증하지 못하므로, TLS는 인증서와 서명으로 공개값이 진짜 서버 흐름에 속했는지 확인합니다.
여기서 E(Ephemeral)는 장기 인증 키와 별개의 임시 키를 쓴다는 뜻입니다. 연결마다 새 임시 키를 만들고 폐기하는 것이 전방 비밀성을 유지하는 데 중요합니다.
서버의 장기 개인키가 나중에 유출되어도, 과거 연결의 임시 비밀값이 안전하게 폐기되었고 세션 키 로그나 티켓 키 유출 같은 별도 사고가 없다면 녹화된 과거 트래픽을 다시 풀기 어렵습니다.
이 성질을 전방 비밀성(Forward Secrecy)이라고 합니다.
예시: 상대 공개값으로 같은 비밀에 도달하기 키 교환의 원리만 손으로 보기 위해 작은 정수 DH 모델을 사용합니다. 공개된 조건은 p=23, g=5이고, A의 비밀값은 a=6, B의 비밀값은 b=15입니다. A는 5⁶을 23으로 나눈 나머지 8을, B는 5¹⁵의 나머지 19를 공개합니다. A가 받은 19를 자기 비밀값 6으로 거듭제곱하면 19⁶ mod 23=2이고, B가 받은 8에 자기 비밀값 15를 적용해도 8¹⁵ mod 23=2입니다. 서로에게 6이나 15를 보내지 않고 같은 결과를 만든 것입니다.
이 숫자는 계산 원리를 위한 모델이며 안전한 TLS 파라미터가 아닙니다. 실제 ECDHE는 타원곡선 그룹의 연산을 사용하고, 공유 결과도 그대로 HTTP 본문의 암호 키로 쓰는 것이 아니라 키 유도에 넣습니다. 순수 DH 계산만으로 교환한 공개값이 원하는 서버의 것이라고 인증하지도 않습니다.
독립 계산하기 같은 정수 모델에서 B의 비밀값만 5로 바꾸면 B의 공개값은 20입니다. A의 결과 20⁶ mod 23과 B의 결과 8⁵ mod 23이 같은지 확인해 보세요.
공유 결과와 인증의 풀이
두 계산은 모두 16입니다. 나머지를 반복해서 구하거나 작은 계산 프로그램으로 확인할 수 있습니다. 같은 결과에 도달했다는 산술과 상대 신원을 확인했다는 판단은 다릅니다. TLS에서는 인증서·서명·Finished 검증을 통해 공개값과 핸드셰이크가 인증한 상대의 흐름에 속하는지도 확인합니다.
TLS 1.3의 개선점
TLS 1.3은 2018년에 표준화되었고, TLS 1.2보다 핸드셰이크를 짧고 단단하게 만들었습니다.
클라이언트가 ClientHello에 (EC)DHE key_share를 미리 싣기 때문에, 일반적인 full handshake는 1 RTT로 진행됩니다. 서버는 ServerHello 뒤부터 핸드셰이크를 암호화하며, 클라이언트는 서버 인증과 Finished 검증 뒤 요청을 보냅니다. key_share가 맞지 않아 HelloRetryRequest가 발생하면 왕복이 추가됩니다.
정적 RSA/DH 키 교환과 TLS 재협상은 TLS 1.3에서 제거되었습니다. 재개 연결에서는 일부 데이터를 0-RTT로 보낼 수 있지만 다음의 보안 제약이 따릅니다.
0-RTT는 “재접속을 더 빨리 시작할 수 있는 기능”이지, 기본으로 켜 둘 만능 최적화가 아닙니다.
0-RTT early data는 연결 간 재생 공격 방지를 보장하지 않고 전방 비밀성도 없습니다. HTTP 메서드 이름만 보고 안전하다고 판단하지 말고, 애플리케이션이 중복 실행을 감당할 수 있는 작업에만 제한해야 합니다.
아래 명령은 출력 항목을 살펴보는 진단 예시이며 이 교재에서 실행한 관측 결과가 아닙니다. 출력 형식에 따라 grep 결과가 비어도 연결 실패를 뜻하지는 않습니다. s_client는 기본적으로 인증서 오류 뒤에도 진행할 수 있으므로, 서비스 신원 검증을 실패 조건으로 검사할 때는 적절한 신뢰 저장소와 -verify_hostname 및 -verify_return_error가 필요합니다.
# SNI를 명시해 TLS 1.3 연결이 되는지 확인
openssl s_client -connect example.com:443 -servername example.com -tls1_3 </dev/null 2>/dev/null | grep "Protocol"
# 협상된 cipher suite 확인
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null | grep "Cipher"
# leaf 인증서의 주체와 유효기간 확인
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates
# 서버가 보내는 인증서 체인 확인
openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null 2>/dev/null \
| grep -E "s:|i:"인증서와 서버 신원
TLS 핸드셰이크에서 서버가 보내는 X.509 인증서는 “이 공개키는 이 서비스 이름에 묶여 있고, 신뢰할 수 있는 CA가 그 사실을 서명했다”는 주장입니다.
브라우저는 단순히 “인증서가 있다”만 보지 않습니다.
접속한 DNS 이름이 인증서의 subjectAltName(dNSName)에 있는지, 유효기간 안인지, 용도와 확장이 맞는지, 그리고 인증서 체인이 신뢰 저장소의 trust anchor까지 이어지는지를 함께 확인합니다.
현대 서비스 이름 검증은 Common Name(CN) 대신 SAN을 사용합니다. IP 주소로 접속할 때는 SAN의 iPAddress와 맞는지도 확인합니다.
인증서 체인 검증
인증서와 신뢰
CA가 서명해도, 클라이언트의 신뢰 앵커가 필요합니다
화살표는 CA의 서명 방향입니다. 클라이언트는 서버 인증서에서 로컬 신뢰 앵커까지 검증 경로를 구성하며, 서버가 보낸 루트 인증서만으로 신뢰하지 않습니다.
서버가 보낸 루트 인증서라는 이유만으로 신뢰하지 않습니다. 이 그림은 CA 서명 관계이며, 접속 이름·유효기간·용도 검증과 서버의 개인키 소유 증명은 별도로 필요합니다.
서버는 보통 leaf 인증서와 intermediate CA 인증서를 보냅니다.
루트 CA 인증서는 대개 서버가 보내는 대상이 아니라, 운영체제나 브라우저의 신뢰 저장소에 들어 있는 trust anchor입니다.
루트 CA가 모든 서버 인증서를 직접 서명하지 않는 이유는 운영과 사고 격리 때문입니다.
루트 개인키는 오프라인에 가깝게 보호하고, 실제 발급 업무는 중간 CA가 맡습니다.
중간 CA에 문제가 생기면 해당 중간 CA를 폐기하거나 교체할 수 있지만, 루트 CA의 개인키가 유출되면 그 루트에 기대는 신뢰 체계 전체가 흔들립니다.
다음 절에서는 실무에서 인증서를 발급하고 갱신하는 방법을 살펴보겠습니다.