TLS 핸드셰이크: 대칭키, 비대칭키, 해시는 어떻게 만날까

대칭키 암호화는 빠르지만 비밀 키를 안전하게 나누기 어렵습니다. 비대칭키 암호화는 처음 만난 상대와 비밀을 합의하고 신원을 증명할 수 있지만, 큰 데이터를 처리하기에는 무겁죠. 암호학적 해시 함수는 메시지가 달라졌는지를 짧은 값으로 드러내고 키를 여러 용도로 나누는 재료가 됩니다.
따로 배운 세 도구가 실제 인터넷에서는 어떻게 한자리에 모일까요? 가장 친숙한 답이 전송 계층 보안(Transport Layer Security, TLS)입니다. HTTPS로 웹사이트에 접속하면 브라우저와 서버는 TLS 핸드셰이크를 거쳐 서로를 확인하고, 매 연결마다 새 키를 만든 다음, 그 키로 HTTP 요청과 응답을 보호합니다.
TLS는 버전마다 핸드셰이크 구조가 다른데요. 이번 글에서는 2018년에 처음 표준화된 TLS 1.3의 첫 연결을 따라가며 대칭키, 비대칭키, 해시가 어느 순간에 어떤 일을 맡는지 살펴보겠습니다. TLS 구현 코드를 파고들기보다는 이미 알고 있는 암호 도구들이 어떻게 하나의 안전한 통신을 만드는지에 초점을 맞추겠습니다.
TLS가 해결해야 하는 세 가지 문제
인터넷에서 처음 만난 브라우저와 서버 사이에는 미리 공유한 비밀이 없습니다. 게다가 공격자가 두 장치 사이의 모든 메시지를 읽고 바꿀 수 있다고 가정해야 하는데요. 이 상황에서도 TLS는 세 가지를 보장하려고 합니다.
첫 번째 약속은 통신 내용을 당사자만 읽을 수 있게 하는 기밀성(confidentiality)입니다. 그렇다고 내용만 가리면 끝은 아닙니다. 중간에서 메시지가 바뀌면 반드시 알아차릴 수 있어야 하는데, 이것이 무결성(integrity)이죠. 브라우저가 대화 중인 서버가 정말 접속하려던 도메인의 서버인지 확인하는 인증(authentication)도 필요합니다.
한 종류의 암호 기술만으로 이 셋을 효율적으로 해결하기는 어렵습니다. 그래서 TLS는 역할을 다음처럼 나눕니다.
| 해결할 일 | TLS에서 주로 쓰는 도구 |
|---|---|
| 처음 만난 양쪽이 같은 비밀 만들기 | X25519 같은 ECDHE 방식의 키 합의 |
| 서버가 진짜인지 확인하기 | 인증서와 RSA-PSS, ECDSA, EdDSA 디지털 서명 |
| 여러 용도의 키 안전하게 만들기 | SHA-256이나 SHA-384를 사용하는 HKDF |
| 실제 요청과 응답 보호하기 | AES-GCM이나 ChaCha20-Poly1305 같은 대칭키 AEAD |
비대칭키 기술이 첫 만남을 주선하고, 해시 기반 함수가 필요한 키들을 뽑아내고, 빠른 대칭키 암호가 본 대화를 맡는 구조입니다.
TLS 1.3 핸드셰이크 한눈에 보기
TLS 1.3의 전체 핸드셰이크를 단순화하면 다음과 같습니다. 실선 화살표는 아직 암호화되지 않은 메시지이고, 점선 화살표는 양쪽이 만든 키로 보호되는 메시지입니다.
sequenceDiagram
accTitle: TLS 1.3 전체 핸드셰이크
accDescr: 클라이언트와 서버가 ClientHello와 ServerHello로 암호 도구와 키 공유 값을 교환한 뒤 같은 핸드셰이크 키를 만들고, 서버 인증과 Finished 검증을 거쳐 애플리케이션 키로 HTTP 요청과 응답을 보호한다.
participant client as 클라이언트
participant server as 서버
client->>server: ClientHello<br/>버전, 암호 스위트, 서명 알고리즘<br/>키 합의 그룹, 클라이언트 key_share
server->>client: ServerHello<br/>선택한 버전과 암호 스위트<br/>서버 key_share
Note over client,server: ECDHE 공유 비밀에서 핸드셰이크 키 계산
server-->>client: EncryptedExtensions
server-->>client: Certificate
server-->>client: CertificateVerify
server-->>client: Finished
client-->>server: Finished
Note over client,server: 애플리케이션 키로 전환
client-->>server: HTTP 요청
server-->>client: HTTP 응답
눈여겨볼 부분은 인증서가 ServerHello와 함께 평문으로 오는 것이 아니라는 점입니다.
TLS 1.3에서는 ServerHello 뒤의 핸드셰이크 메시지가 이미 대칭키로 보호됩니다.
실제 HTTP 데이터가 오가기 전부터 대칭키 암호가 일을 시작하는 셈이죠.
이 흐름은 2026년 7월에 발표된 RFC 9846을 기준으로 했습니다. 이 명세는 2018년의 RFC 8446을 대체하지만, 프로토콜 버전은 TLS 1.3 그대로이고 기존 구현과 호환되는 소규모 개정입니다.
첫 단계: 사용할 암호 도구 고르기
클라이언트가 보내는 ClientHello에는 자신이 지원하는 TLS 버전과 암호 스위트(cipher suite), 서명 알고리즘, 키 합의 그룹이 담깁니다.
서버는 양쪽이 모두 지원하는 조합을 골라 ServerHello로 답합니다.
TLS 1.2까지는 암호 스위트 이름 하나가 키 교환, 인증, 대칭키 암호, 해시를 모두 나타냈습니다.
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 같은 긴 이름을 보면 ECDHE로 키를 합의하고, RSA 인증서를 쓰고, AES-128-GCM으로 데이터를 보호한다는 사실을 한 번에 알 수 있었죠.
TLS 1.3에서는 이 묶음을 풀었습니다. 키 합의 방식과 인증서 서명 알고리즘은 별도로 협상하고, 암호 스위트에는 레코드를 보호할 대칭키 알고리즘과 키 유도에 사용할 해시만 남았습니다.
flowchart TB
accTitle: TLS 1.3 암호 스위트 읽기
accDescr: TLS_AES_128_GCM_SHA256 암호 스위트는 레코드를 보호할 대칭키 인증 암호인 AES-128-GCM과 키 유도 및 핸드셰이크 검증에 사용할 SHA-256 해시를 지정한다.
suite["TLS_AES_128_GCM_SHA256"]
suite --> aead["AES_128_GCM<br/>레코드를 보호할 대칭키 AEAD"]
suite --> hash["SHA256<br/>HKDF와 핸드셰이크 검증에 사용할 해시"]
classDef suite stroke:#7c3aed,stroke-width:3px
classDef algorithm stroke:#0284c7,stroke-width:2px
class suite suite
class aead,hash algorithm
이 이름에 X25519나 ECDHE, RSA가 보이지 않는 이유가 바로 여기에 있습니다.
암호 스위트만 보고 키 합의 방식이나 인증서 종류를 단정할 수는 없습니다.
두 번째 단계: 보내지 않고 같은 비밀 만들기
알고리즘을 골랐다면 양쪽은 이후에 쓸 키의 재료를 만들어야 합니다. 여기서 흔히 “서버의 공개 키로 세션 키를 암호화해 보낸다”고 설명하지만, 현대 TLS 1.3의 일반적인 첫 연결은 그렇게 동작하지 않습니다. 브라우저와 서버가 X25519 같은 ECDHE 방식의 키 합의(key agreement)를 수행해 같은 공유 비밀을 각자 계산합니다.
클라이언트는 매 연결마다 임시 개인 키를 만들고, 거기서 얻은 공개 값인 key_share를 ClientHello에 넣습니다.
서버도 임시 키 쌍을 만든 뒤 자신의 key_share를 돌려주는데요.
두 공개 값은 네트워크에 그대로 노출되지만, 각자가 가진 임시 개인 키가 없으면 공유 비밀을 계산할 수 없습니다.
flowchart TB
accTitle: ECDHE로 같은 공유 비밀을 만드는 흐름
accDescr: 클라이언트는 자신의 임시 개인 키와 서버의 공개 key_share를 계산에 사용하고, 서버는 자신의 임시 개인 키와 클라이언트의 공개 key_share를 사용해 네트워크로 비밀을 보내지 않고 같은 공유 비밀을 각각 얻는다.
clientPrivate(["클라이언트<br/>임시 개인 키"]) --> clientCalc["클라이언트의<br/>ECDHE 계산"]
serverShare["서버 공개<br/>key_share"] --> clientCalc
serverPrivate(["서버<br/>임시 개인 키"]) --> serverCalc["서버의<br/>ECDHE 계산"]
clientShare["클라이언트 공개<br/>key_share"] --> serverCalc
clientCalc --> clientSecret["클라이언트가 얻은<br/>공유 비밀"]
serverCalc --> serverSecret["서버가 얻은<br/>공유 비밀"]
clientSecret -. "두 값이 같음" .- serverSecret
classDef privateKey stroke:#7c3aed,stroke-width:2px
classDef publicValue stroke:#0284c7,stroke-width:2px
classDef operation stroke:#0d9488,stroke-width:2px
classDef secret stroke:#16a34a,stroke-width:3px
class clientPrivate,serverPrivate privateKey
class serverShare,clientShare publicValue
class clientCalc,serverCalc operation
class clientSecret,serverSecret secret
공유 비밀 자체가 네트워크를 지나가지 않는다는 점이 핵심입니다.
공격자는 두 key_share를 모두 보더라도 같은 값을 만들 수 없고, 브라우저와 서버만 그 비밀을 손에 넣습니다.
여기서 임시(ephemeral)라는 말도 중요합니다. 연결마다 새 키 쌍을 만들고 핸드셰이크가 끝나면 버리기 때문에, 서버 인증서의 장기 개인 키가 나중에 유출되어도 과거에 녹화된 통신을 복호화할 수 없습니다. 이 성질을 순방향 비밀성(forward secrecy)이라고 합니다.
세 번째 단계: 그 서버가 진짜인지 증명하기
키 합의만으로는 기밀 통신을 만들 수 있지만 상대의 신원까지 알 수는 없습니다.
공격자가 중간에서 클라이언트와 서버 양쪽에 각각 자기 key_share를 보내면, 두 개의 암호화 연결을 만들고 메시지를 중계하는 중간자 공격이 가능하기 때문이죠.
이 빈틈을 인증서와 디지털 서명이 막습니다. 서버가 보내는 TLS 인증서에는 도메인 이름과 공개 키가 들어 있고, 인증 기관(Certificate Authority, CA)의 서명이 붙어 있습니다. 브라우저는 인증서의 도메인, 유효 기간, 신뢰의 사슬을 확인해 그 공개 키가 접속하려던 서버에 속한다고 판단합니다.
하지만 인증서만 보내서는 서버가 그 공개 키에 대응하는 개인 키를 실제로 가졌는지 증명되지 않습니다.
그래서 서버는 지금까지 주고받은 핸드셰이크 메시지의 해시에 개인 키로 서명한 CertificateVerify를 함께 보냅니다.
브라우저는 인증서의 공개 키로 서명을 검증하면서 두 가지를 한꺼번에 확인합니다.
서버가 인증서의 개인 키를 가지고 있다는 사실, 그리고 ClientHello부터 지금까지의 협상 내용이 중간에서 바뀌지 않았다는 사실입니다.
공격자가 더 약한 알고리즘을 쓰도록 메시지를 고치면 핸드셰이크 해시가 달라져 서명 검증이 실패합니다.
네 번째 단계: 해시로 용도별 키 만들기
키 합의로 얻은 공유 비밀을 그대로 AES나 ChaCha20의 키로 쓰지는 않습니다. TLS는 HMAC 기반 키 유도 함수(HMAC-based Extract-and-Expand Key Derivation Function, HKDF)를 사용해 하나의 비밀에서 여러 키를 뽑아냅니다. 이 과정에는 공유 비밀뿐 아니라 지금까지의 핸드셰이크 기록(transcript)에 대한 해시도 들어갑니다.
flowchart TB
accTitle: 단순화한 TLS 1.3 키 유도
accDescr: ECDHE 공유 비밀과 핸드셰이크 해시를 HKDF에 넣어 클라이언트에서 서버로 보내는 키와 서버에서 클라이언트로 보내는 키를 핸드셰이크 및 애플리케이션 데이터 용도로 각각 만든다.
sharedSecret["ECDHE<br/>공유 비밀"] --> hkdf["HKDF"]
transcriptHash["핸드셰이크<br/>해시"] --> hkdf
hkdf --> handshakeKeys["핸드셰이크 키<br/>클라이언트 → 서버<br/>서버 → 클라이언트"]
hkdf --> applicationKeys["애플리케이션 키<br/>클라이언트 → 서버<br/>서버 → 클라이언트"]
classDef input stroke:#7c3aed,stroke-width:2px
classDef derivation stroke:#0d9488,stroke-width:3px
classDef key stroke:#0284c7,stroke-width:2px
class sharedSecret,transcriptHash input
class hkdf derivation
class handshakeKeys,applicationKeys key
방향과 용도마다 다른 키를 만드는 이유는 피해 범위를 나누기 위해서입니다. 클라이언트가 보내는 데이터와 서버가 보내는 데이터가 같은 키를 공유하지 않고, 핸드셰이크용 키와 HTTP 데이터용 키도 서로 다릅니다. 한 키가 다른 문맥에서 재사용되어 생길 수 있는 공격을 구조적으로 막는 셈이죠.
서버와 클라이언트가 마지막에 보내는 Finished 메시지에서도 해시가 등장합니다.
양쪽은 지금까지의 핸드셰이크 전체를 해시하고, HKDF로 만든 전용 키를 사용해 HMAC을 계산합니다.
상대가 보낸 Finished가 맞아야만 “우리가 같은 대화를 보았고 같은 키를 만들었다”는 사실이 최종 확인됩니다.
다섯 번째 단계: 대칭키로 본 통신 보호하기
핸드셰이크가 끝나면 HTTP 요청과 응답은 TLS 레코드(record)라는 작은 단위로 나뉩니다. 각 레코드는 핸드셰이크에서 만든 트래픽 키(traffic key)와 초기화 벡터, 레코드 순서 번호로부터 만든 고유한 논스를 사용해 보호됩니다.
TLS 1.3이 사용하는 대칭키 알고리즘은 모두 인증 암호화(Authenticated Encryption with Associated Data, AEAD) 방식입니다. 대표적인 조합이 AES-GCM과 ChaCha20-Poly1305인데요. 이들은 평문을 암호문으로 바꾸는 동시에 인증 태그를 만들어, 공격자가 암호문이나 레코드 헤더를 건드리면 복호화가 실패하도록 합니다.
flowchart TB
accTitle: TLS 레코드를 대칭키로 보호하는 흐름
accDescr: HTTP 데이터와 레코드 헤더를 트래픽 키 및 고유 논스와 함께 대칭키 인증 암호에 넣어 암호문과 인증 태그를 만든다.
record["HTTP 데이터<br/>+ 레코드 헤더"] --> aead["대칭키 AEAD"]
keyMaterial["트래픽 키<br/>+ 고유 논스"] --> aead
aead --> protectedRecord["암호문<br/>+ 인증 태그"]
classDef input stroke:#7c3aed,stroke-width:2px
classDef operation stroke:#0d9488,stroke-width:3px
classDef output stroke:#16a34a,stroke-width:2px
class record,keyMaterial input
class aead operation
class protectedRecord output
여기서 한 가지 오해하기 쉬운 부분이 있습니다.
TLS_AES_128_GCM_SHA256의 SHA-256이 HTTP 본문의 무결성을 직접 검사하는 것은 아닙니다.
실제 레코드의 기밀성과 무결성은 AES-GCM의 인증 태그가 함께 지키고, SHA-256은 HKDF와 핸드셰이크 기록, Finished 검증에 쓰입니다.
결국 많은 데이터를 빠르게 처리하는 일은 대칭키 암호화가 맡습니다. 비대칭키 연산은 연결을 시작할 때만 제한적으로 사용되므로, TLS는 신원 확인과 안전한 키 합의라는 장점은 얻으면서 본 통신의 성능도 챙길 수 있습니다.
실제 연결에서 세 도구 찾아보기
openssl s_client를 사용하면 현재 사이트와 합의된 TLS 버전과 키 합의용 임시 키, 암호 스위트를 확인할 수 있습니다.
openssl s_client \
-connect daleseo.com:443 \
-servername daleseo.com \
-tls1_3 </dev/null 2>&1 |
rg "^(Server Temp Key| Protocol| Cipher| Verify return code)"
이 글을 작성할 때 실행한 결과는 다음과 같았습니다.
Server Temp Key: ECDH, X25519, 253 bits
Protocol : TLSv1.3
Cipher : AEAD-CHACHA20-POLY1305-SHA256
Verify return code: 0 (ok)
X25519는 공유 비밀을 만드는 비대칭키 기반 키 합의를 뜻합니다.
CHACHA20-POLY1305는 실제 TLS 레코드를 보호하는 대칭키 AEAD이고, SHA256은 키 유도와 핸드셰이크 검증에 쓰이는 해시입니다.
마지막 줄은 서버가 보낸 인증서의 신뢰 사슬 검증이 성공했다는 뜻이죠.
서버 설정과 클라이언트의 암호 라이브러리에 따라 AES-GCM이나 다른 키 합의 그룹이 선택될 수도 있습니다. 결과가 달라도 키 합의, 키 유도, 인증, 대칭키 레코드 보호라는 역할 분담은 같습니다.
TLS 1.2보다 무엇이 단순해졌을까
TLS 1.2도 안전하게 설정하면 여전히 쓸 수 있지만, 선택지가 많아 잘못 조합하기 쉬웠습니다. RSA로 세션 키를 전달하는 정적 키 교환은 순방향 비밀성을 제공하지 않았고, CBC 모드와 별도 MAC의 조합에는 구현 순서에 따라 공격이 생길 여지가 있었죠.
TLS 1.3은 이런 오래된 선택지를 명세에서 걷어냈습니다. 정적 RSA 키 교환과 정적 Diffie-Hellman을 제거하고, 레코드 보호에는 AEAD만 허용합니다. 인증과 키 합의를 암호 스위트에서 분리하고 키 유도에는 HKDF를 사용해, 각 도구가 맡는 역할도 더 선명해졌습니다.
속도도 달라졌습니다.
처음 연결하는 일반적인 TLS 1.3 핸드셰이크는 클라이언트의 첫 메시지에 key_share를 미리 담아 한 번의 왕복(1-RTT)으로 끝낼 수 있습니다.
서버가 다른 키 합의 그룹을 요구해 HelloRetryRequest를 보내는 경우에는 왕복이 한 번 더 필요할 수 있습니다.
세션 재개와 0-RTT는 별도 문제
한 번 연결했던 서버에 다시 접속할 때는 이전 핸드셰이크에서 받은 세션 티켓을 이용할 수 있습니다.
이 티켓으로 양쪽이 사전 공유 키(Pre-Shared Key, PSK)를 복원하면 인증서 검증과 서명 비용을 줄여 더 빠르게 연결을 재개할 수 있는데요.
새 key_share를 함께 사용하면 재개된 연결에서도 순방향 비밀성을 얻을 수 있습니다.
TLS 1.3에는 첫 메시지와 함께 애플리케이션 데이터를 보내는 0-RTT 모드도 있습니다. 왕복을 기다리지 않아 빠르지만, 이 초기 데이터는 순방향 비밀성이 없고 공격자가 다른 연결에서 재전송할 수 있습니다. 결제나 상태 변경처럼 같은 요청이 두 번 처리되면 곤란한 작업에 함부로 쓰면 안 되는 이유입니다. 애플리케이션이 재전송을 안전하게 처리하도록 명시적으로 설계된 경우에만 제한적으로 사용해야 합니다.
TLS가 지켜주는 경계
TLS가 보호하는 범위는 클라이언트와 서버 사이를 이동하는 데이터입니다. 중간의 와이파이 운영자나 인터넷 서비스 제공자, 공격자는 HTTP 본문을 읽거나 몰래 고칠 수 없습니다. 하지만 데이터가 서버에 도착해 복호화된 뒤 어떻게 처리되고 저장되는지는 TLS의 책임이 아닙니다.
인증서 검증에 성공했다는 사실도 그 사이트의 운영자가 선량하다는 뜻은 아닙니다. 인증서는 공개 키와 도메인의 관계를 증명할 뿐이고, 피싱 사이트도 자기 도메인에 맞는 인증서를 발급받을 수 있습니다. TLS는 “의도한 도메인과 변조 없이 비밀리에 통신하고 있다”는 사실을 보장한다고 이해하는 편이 정확합니다.
또한 TLS는 패킷의 목적지 IP나 통신량과 시점까지 모두 숨기지는 않습니다. 내용은 읽을 수 없어도 관찰자는 어느 서버와 얼마나 많은 데이터를 주고받는지에서 일부 정보를 추측할 수 있습니다. TLS는 안전한 통신 채널이지 익명 통신 도구는 아닙니다.
마치며
TLS 1.3을 한 문장으로 줄이면, “비대칭키 기술로 안전한 첫 만남을 만들고, 해시로 대화 전체와 키를 묶은 뒤, 대칭키로 본 통신을 빠르게 보호하는 프로토콜”입니다. 공개 키로 HTTP 데이터를 직접 암호화하는 것도 아니고, 서버 인증서의 공개 키로 세션 키 하나를 잠가 보내는 구조도 아닙니다.
X25519 같은 ECDHE 방식은 공유 비밀을 합의하고, 인증서와 CertificateVerify는 서버의 신원을 증명합니다.
HKDF와 핸드셰이크 해시는 용도별 키를 만들고 협상 내용의 변조를 잡아내며, AES-GCM이나 ChaCha20-Poly1305는 실제 요청과 응답을 보호하죠.
각 도구를 따로 놓고 보면 보이지 않던 현대 암호 통신의 전체 그림이 TLS 안에서 비로소 맞춰집니다.
현재 등록된 암호 스위트와 키 합의 그룹이 궁금하다면 IANA의 TLS 매개변수 목록을 참고하세요.
This work is licensed under CC BY 4.0