TLS Handshake

Memo

TLS는 HTTP 같은 애플리케이션 데이터를 암호화하기 전에, 클라이언트와 서버가 “이번 연결에서 어떤 키와 알고리즘을 쓸지” 합의하는 프로토콜이다.

요즘 기준으로는 TLS 1.3을 기본으로 보면 된다. 핵심만 잡으면 TLS 1.3 full handshake는 다음 흐름이다.

Client                                               Server

ClientHello
  supported_versions
  cipher_suites
  supported_groups
  key_share = client ephemeral public key
                              -------->

                                      ServerHello
                                        key_share = server ephemeral public key
                                  {EncryptedExtensions}
                                  {Certificate}
                                  {CertificateVerify}
                                  {Finished}
                              <--------

{Finished}                    -------->

[Application Data]            <-------> [Application Data]

key_share는 사실상 ECDHE public key

TLS 1.3에서 key_share라고 부르는 값은 완성된 세션 키가 아니다. 클라이언트와 서버가 ECDHE shared secret을 만들기 위해 교환하는 각자의 ephemeral public key다.

예를 들어 X25519를 쓴다고 하면 다음과 같다.

Client:
  client private key 생성
  client public key 계산
  ClientHello.key_share로 client public key 전송

Server:
  server private key 생성
  server public key 계산
  ServerHello.key_share로 server public key 전송

Both:
  ECDH(private_key, peer_public_key)
  => 같은 shared secret 계산

네트워크에 노출되는 것은 public key뿐이다. 실제 shared secret은 각자의 private key와 상대방의 public key로 계산한다.

중요한 점은 이 shared secret을 그대로 대칭키로 쓰지 않는다는 것이다. TLS 1.3은 HKDF 기반 key schedule을 통해 handshake traffic key, application traffic key 등 여러 키를 분리해서 만든다.

인증서는 키 교환이 아니라 서버 인증

헷갈리기 쉬운 부분이 RSA다.

TLS 1.3에서는 static RSA key exchange가 제거됐다. 즉 RSA로 세션 키를 직접 교환하지 않는다. 세션 키 합의는 보통 X25519나 P-256 같은 ECDHE로 하고, RSA 인증서는 서버 인증에 쓰일 수 있다.

정확히는 이런 조합이 가능하다.

Key exchange:   X25519 ECDHE
Authentication: RSA certificate

서버가 Certificate를 보내면 클라이언트는 다음을 검증한다.

그 다음 CertificateVerify에서 서버는 handshake transcript에 서명한다. 클라이언트는 인증서의 public key로 이 서명을 검증한다.

이 단계가 “나는 인증서만 들고 있는 게 아니라, 그 private key도 실제로 갖고 있다”는 증명이다.

Finished는 handshake 전체에 대한 HMAC

Finished는 단순 ACK가 아니다. 지금까지 오간 핸드셰이크 메시지 전체 transcript에 대한 MAC이다.

개념적으로는 이렇다.

verify_data = HMAC(
  finished_key,
  Hash(ClientHello ... CertificateVerify)
)

서버 Finished는 server handshake traffic secret에서 나온 finished_key로 만들고, 클라이언트 Finished는 client handshake traffic secret에서 나온 finished_key로 만든다.

Finished가 확인하는 것은 세 가지다.

왜 1-RTT인가

TLS 1.3 full handshake가 1-RTT라고 하는 이유는 애플리케이션 데이터를 보내기까지 네트워크 왕복이 한 번이면 되기 때문이다.

1. Client -> Server: ClientHello + client key_share
2. Server -> Client: ServerHello + server key_share + cert + Finished
3. Client -> Server: Finished + Application Data

클라이언트가 첫 메시지에 ECDHE public key를 미리 넣어 보내기 때문에, 서버 응답을 받는 순간 양쪽은 shared secret과 traffic key를 만들 수 있다. 그래서 클라이언트는 자기 Finished 뒤에 바로 application data를 붙여 보낼 수 있다.

비교하면 다음과 같다.

방식Application Data까지
TLS 1.2 full handshake보통 2-RTT
TLS 1.3 full handshake보통 1-RTT
TLS 1.3 resumption + 0-RTT0-RTT 가능

0-RTT는 빠르지만 replay risk가 있다. 결제, 주문, signing, nonce 소비 같은 상태 변경 요청에는 기본적으로 쓰면 안 된다.

현업에서는 어떻게 쓰이나

요즘 TLS 1.3에서는 X25519 기반 ECDHE가 흔한 기본 키 교환이다. 서버 인증서는 RSA도 아직 흔하고, ECDSA도 많이 쓴다.

정리하면 다음과 같다.

세션 키 합의: X25519 ECDHE
서버 인증:   RSA 또는 ECDSA certificate
무결성 확인: Finished = transcript HMAC
데이터 암호화: application traffic key로 AEAD

따라서 “RSA 기반 TLS”라고 말할 때는 조심해야 한다. TLS 1.3에서 RSA가 보이면 보통 key exchange가 아니라 certificate authentication 쪽이다.

AWS ACM for Nitro Enclaves처럼 특정 서비스가 “RSA 인증서만 지원”한다고 할 때도 마찬가지다. 그 말은 인증서 키 알고리즘이 RSA라는 뜻이지, TLS 1.3 세션 키를 RSA로 교환한다는 뜻은 아니다.