본문으로 건너뛰기 TLS 프로토콜 심화 가이드 | 핸드셰이크·암호 스위트·TLS 1.3 키 유도

TLS 프로토콜 심화 가이드 | 핸드셰이크·암호 스위트·TLS 1.3 키 유도

TLS 프로토콜 심화 가이드 | 핸드셰이크·암호 스위트·TLS 1.3 키 유도

이 글의 핵심

TLS 1.2와 1.3의 핸드셰이크 차이, 암호 스위트(키 교환·인증·대칭키·해시) 구조, AEAD·완전 순방향 비밀성·0-RTT의 트레이드오프, QUIC·HTTP/3과의 결합까지 전문가 수준으로 정리합니다.

TLS(Transport Layer Security)는 HTTP든 메일 프로토콜이든, TCP 위에서 동작하는 애플리케이션에 기밀성·무결성·인증을 부여하는 계층입니다. 인증서를 발급받아 배포하는 것으로 끝나는 작업이 아니라, 인증서 체인과 만료 시점, 핸드셰이크 절차, 암호 스위트, 키 유도가 하나로 맞물려 지연 시간과 보안 수준을 함께 결정합니다. 이 구조를 미리 이해해두면 장애가 발생했을 때 훨씬 빠르게 원인을 좁힐 수 있습니다. 이 글은 RFC 8446(TLS 1.3)과 RFC 5246(TLS 1.2)을 기준으로 프로토콜 내부를 실무 관점에서 살펴봅니다.

실무에서는 인증서 만료로 서비스 전체가 중단되는 사고가 드물지 않게 발생합니다. 만료 알람을 걸어두지 않은 상태에서 인증서가 만료되면 브라우저에는 경고가 표시되고, curl 요청은 인증서 오류로 실패하며, 모니터링 시스템은 “응답 코드는 200인데 무언가 이상하다”는 애매한 신호만 내보내는 경우가 많습니다. 이런 상황에서는 openssl s_client -connect host:443 -servername host 명령으로 인증서 체인과 만료일을 직접 확인하고, CA 대시보드에서 갱신한 뒤 웹 서버를 리로드하는 절차만 거치면 대부분 해결됩니다. 문제는 절차 자체가 어려운 것이 아니라, 무엇을 놓쳤는지 모른 채 시간을 허비하는 데 있습니다. 이런 경험 이후로는 만료 30일 전 알림스테이징 환경에서 먼저 갱신 절차를 검증하는 것을 기본 운영 원칙으로 삼는 것이 좋습니다. 사설 CA나 EV 인증서까지 갈 필요 없이, 공개 웹 서비스라면 Let’s Encrypt만으로도 충분한 경우가 대부분입니다. 유료 인증서가 주는 심리적 안정감보다, 자동 갱신과 모니터링이 안정적으로 돌아가는 쪽이 실질적인 이득이 훨씬 큽니다. 다만 내부망에서 mTLS를 운용한다면 이야기가 달라지며, 이는 뒤에서 별도로 다룹니다.

이 글을 읽고 나면 TLS 1.2와 1.3의 핸드셰이크가 왜 다른지, 암호 스위트 문자열이 무엇을 의미하는지, 완전 순방향 비밀성(PFS)·AEAD·0-RTT가 실무에서 어떤 의미를 갖는지 설명할 수 있게 될 것입니다. QUIC·HTTP/3과의 연결 지점도 함께 정리합니다. TCP/UDP 계층에 대한 배경 지식이 필요하다면 TCP 가이드UDP 가이드를 먼저 참고하면 전체 스택을 이해하는 데 도움이 됩니다.

TLS가 제공하는 세 가지 보장

TLS가 책임지는 영역은 크게 세 가지입니다.

  • 기밀성(Confidentiality): 도청자가 평문 데이터를 볼 수 없도록 암호화합니다.
  • 무결성(Integrity): 전송 중 데이터가 변조되었는지를 MAC 또는 AEAD 태그로 검증합니다.
  • 인증(Authentication): 일반적으로 서버가 X.509 인증서를 통해 자신의 신원을 증명합니다. 클라이언트 인증(mTLS)은 선택적으로 추가됩니다.

TLS는 TCP를 대체하는 프로토콜이 아니라, TCP 연결 위에서 신뢰할 수 있는 키 재료를 협상한 뒤, 그 이후의 통신을 대칭키 암호로 보호하는 계층입니다. 비대칭키 암호는 계산 비용이 높기 때문에 핸드셰이크 단계에서만 사용하고, 실제 데이터 전송에는 훨씬 빠른 대칭키 암호를 사용하는 구조입니다.

TLS 1.2 핸드셰이크의 흐름

TLS 1.2의 풀 핸드셰이크는 대략 다음과 같은 순서로 진행됩니다. 클라이언트가 ClientHello에서 지원 가능한 암호 스위트 목록과 키 교환 후보를 전송하면, 서버는 ServerHello로 그중 하나를 선택합니다. 이어서 서버는 Certificate 메시지로 인증서 체인과 공개키를 전달하고, 필요에 따라 ServerKeyExchange로 추가 키 교환 정보를 보냅니다. 클라이언트는 ClientKeyExchange를 통해 프리마스터 시크릿(premaster secret) 관련 정보를 전달하고, 양측은 ChangeCipherSpecFinished 메시지를 교환하며 핸드셰이크를 마무리합니다.

여기서 핵심은 두 가지입니다. 첫째, ClientHello에 담긴 암호 스위트 후보 중에서 서버가 하나를 선택한다는 점입니다. 둘째, Certificate 메시지로 전달된 서버 신원과 체인을 클라이언트가 검증할 때 신뢰 앵커(루트 CA)SNI에 지정한 호스트명이 인증서의 SAN(Subject Alternative Name)과 일치하는지를 함께 확인한다는 점입니다. 이후 프리마스터 시크릿에서 마스터 시크릿으로, 마스터 시크릿에서 PRF(Pseudo-Random Function)를 통해 실제 암호화·MAC·IV에 사용할 키 블록이 파생됩니다. 이 키 유도 과정은 선택된 암호 스위트에 따라 세부 방식이 달라지며, 세션 재개(session resumption) 기능을 사용하면 풀 핸드셰이크 횟수를 줄여 지연 시간을 낮출 수 있지만 서버 측에 세션 상태를 유지해야 하는 부담이 생깁니다.

sequenceDiagram
  participant C as 클라이언트
  participant S as 서버
  C->>S: ClientHello (랜덤, 세션, cipher suites 등)
  S->>C: ServerHello
  S->>C: Certificate
  S->>C: ServerKeyExchange (일부 키 교환)
  S->>C: CertificateRequest (선택, mTLS)
  S->>C: ServerHelloDone
  C->>C: 인증서 검증 (CA·호스트명)
  C->>S: ClientKeyExchange
  C->>S: CertificateVerify (mTLS 시)
  C->>S: ChangeCipherSpec
  C->>S: Finished (협상된 키로 HMAC)
  S->>C: ChangeCipherSpec
  S->>C: Finished
  Note over C,S: 애플리케이션 데이터 (레코드 레이어)

TLS 1.3의 변화: 더 안전하고 더 빠르게

TLS 1.3은 보안성과 단순화라는 두 가지 목표를 동시에 달성하기 위해 여러 요소를 정리했습니다. RSA 키 전송 방식으로 프리마스터 시크릿을 암호화해 전달하던 기존 방식은 완전히 제거되었습니다. 이 방식은 서버의 장기 개인키가 유출되면 과거에 캡처된 트래픽까지 복호화될 수 있어 완전 순방향 비밀성을 약화시키는 문제가 있었기 때문입니다. TLS 1.3에서는 (EC)DHE 계열의 일시 키 교환만을 사용하며, 이렇게 얻은 공유 비밀에서 HKDF(HMAC 기반 키 유도 함수)를 통해 핸드셰이크 트래픽 키와 애플리케이션 트래픽 키를 단계적으로 파생시킵니다.

또한 TLS 1.3은 핸드셰이크 메시지 중 암호화되는 구간을 늘려서, 중간자가 협상 과정에서 얻을 수 있는 정보를 최소화했습니다. 대표적인 새 기능이 0-RTT(early data)입니다. 이전 연결에서 얻은 세션 정보를 재사용해 핸드셰이크가 끝나기 전부터 애플리케이션 데이터를 먼저 보낼 수 있는 기능인데, 그 대가로 리플레이 공격에 노출될 위험이 있습니다. 0-RTT로 전달된 초기 데이터는 공격자가 그대로 캡처해서 재전송할 수 있기 때문에, 결제 요청과 같은 POST 처리에는 절대 사용해서는 안 됩니다. 멱등하고 민감도가 낮은 요청에만 제한적으로 적용해야 하며, RFC 8470에서 정의하는 Early-Data 헤더와 애플리케이션 레벨의 방어 정책이 함께 갖춰져야 합니다. 실무에서는 0-RTT를 기본값으로 켜두지 않고, 이득이 명확한 시나리오에서만 제한적으로 활성화하는 것을 권장합니다.

암호 스위트 해부하기

TLS 1.2에서 사용하는 TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384와 같은 암호 스위트 이름은 네 가지 요소로 구성됩니다.

  • ECDHE: 일시 타원곡선 디피-헬만 키 교환. 연결마다 새로운 키를 생성하므로 완전 순방향 비밀성에 기여합니다.
  • RSA: 인증서 공개키 알고리즘, 즉 서버 인증(서명 검증) 방식입니다.
  • AES_256_GCM: 대칭 암호와 운용 모드(AEAD)입니다.
  • SHA384: PRF 및 핸드셰이크 무결성 검증에 사용되는 해시 함수입니다.

ECDHEDHE가 포함된 스위트를 사용하면 매 연결마다 세션 키가 달라지기 때문에, 서버의 장기 개인키가 나중에 유출되더라도 과거 트래픽을 복호화할 수 없습니다. 이것이 바로 완전 순방향 비밀성(Perfect Forward Secrecy)입니다. TLS 1.3의 암호 스위트 이름은 대칭 암호와 해시 함수 중심으로 훨씬 단순해졌으며, 키 교환 방식은 별도의 KeyShare 확장에서 협상됩니다. 실제 서버 구성에서는 하위 호환을 위해 TLS 1.2용 스위트 문자열과 TLS 1.3용 스위트 이름이 함께 설정되어 있는 경우가 많습니다.

CBC 모드와 별도의 HMAC을 조합하는 예전 방식은 패딩 오라클 공격(padding oracle attack)과 여러 차례 연관되어 지적받았습니다. 이에 비해 AEAD(Authenticated Encryption with Associated Data)는 암호화와 무결성 검증을 하나의 연산으로 처리해 레코드 레이어의 구조를 단순하게 만들고 이런 부류의 취약점을 원천적으로 줄입니다. GCM과 ChaCha20-Poly1305가 대표적인 AEAD 방식입니다.

키 유도와 레코드 레이어

TLS 1.2는 프리마스터 시크릿에서 마스터 시크릿을 만들고, PRF를 통해 쓰기 키·MAC 키·IV로 구성된 키 블록을 잘라서 사용합니다. TLS 1.3은 (EC)DHE로 얻은 공유 비밀과 핸드셰이크 트랜스크립트(지금까지 교환된 메시지의 해시)를 HKDF에 단계적으로 입력해 트래픽 키를 유도합니다. 또한 TLS 1.3에는 Key Update 메커니즘이 있어, 하나의 연결이 오래 유지되더라도 중간에 키를 갱신할 수 있습니다.

레코드 레이어는 상위 계층의 데이터를 일정 단위로 잘라 시퀀스 번호·콘텐츠 타입·프로토콜 버전과 함께 인증·암호화해서 전송합니다. 패킷 캡처로 TLS 트래픽을 분석할 때는 직접 바이트를 해석하기보다 Wireshark의 TLS 디코딩 기능을 활용하는 것이 안전합니다. TLS 1.3에서는 하위 호환을 위해 레코드 헤더에 TLS 1.2처럼 보이는 값을 그대로 두는 등 레거시 호환을 위한 디테일이 몇 가지 남아 있으므로, 캡처 분석 시 이 점을 감안해야 합니다.

QUIC과 TLS 1.3의 결합

QUIC은 UDP 위에서 동작하지만, 암호화와 핸드셰이크 로직을 프로토콜 자체에 통합했다는 점에서 “TCP 위에 TLS를 얹은” 기존 방식과는 결합 구조가 다릅니다. QUIC은 TLS 1.3의 핸드셰이크 메시지를 그대로 사용하지만, 이를 QUIC 고유의 패킷 보호 방식에 매핑해서 처리합니다. 이 매핑이 구체적으로 어떻게 동작하는지는 UDP 가이드의 QUIC 섹션에서 더 자세히 다룹니다.

운영 트러블슈팅: 인증서와 협상 문제 진단하기

실무에서 TLS 관련 장애의 원인은 대부분 다음 세 가지 중 하나입니다.

  1. 인증서 만료: 갱신 알림이 없으면 가장 흔하게 발생하는 사고입니다.
  2. 체인 불완전: 중간 인증서(intermediate certificate)가 누락되어 일부 클라이언트에서만 검증에 실패하는 경우입니다.
  3. 호스트명 불일치: SNI로 전달한 이름과 인증서의 SAN이 일치하지 않는 경우입니다.

이런 문제를 진단할 때는 다음과 같이 openssl s_client로 실제 협상 결과를 직접 확인하는 것이 가장 빠릅니다.

# 인증서 체인, 만료일, 협상된 프로토콜 버전을 확인
openssl s_client -connect example.com:443 -servername example.com

# 인증서 유효기간만 빠르게 확인
openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -dates

# 협상된 ALPN(h2/h3 여부)과 cipher를 확인
openssl s_client -connect example.com:443 -servername example.com -alpn h2 2>/dev/null \
  | grep -E "Cipher|ALPN"

-servername 옵션을 빠뜨리면 SNI 없이 접속하게 되어, 다수의 호스트를 하나의 IP로 서비스하는 환경(가상 호스팅)에서 잘못된 인증서를 받아 원인 파악에 혼선을 줄 수 있습니다. 반드시 실제 접속 시 브라우저가 보내는 것과 동일한 호스트명을 -servername에 지정해야 합니다. 첫 번째 명령의 출력에서 Certificate chain 섹션을 통해 중간 인증서가 제대로 포함되어 있는지, Verify return code0 (ok)인지 확인하는 것이 체인 문제를 가장 빠르게 진단하는 방법입니다.

mTLS와 인증서 회전

내부망에서 서비스 간 통신에 mTLS(mutual TLS)를 적용하는 경우, 클라이언트 인증서까지 관리 대상에 포함되기 때문에 운영 부담이 크게 늘어납니다. 인증서 회전(rotation)이 수동으로 이루어지는 환경에서는 만료 시점에 여러 서비스가 동시에 장애를 일으키는 상황이 발생하기 쉽습니다. 따라서 mTLS를 도입할 때는 발급부터 배포, 회전까지 자동화된 파이프라인을 먼저 구축하는 것이 현실적인 접근입니다. 자동화가 어려운 상황이라면 차라리 mTLS 도입을 미루고 네트워크 수준의 격리(VPC, 서비스 메시의 사이드카 프록시 등)로 대체하는 편이 안전할 수 있습니다. C++ 환경에서 OpenSSL과 Asio를 직접 다루는 방법은 C++ SSL/TLS 가이드에서 이어서 확인할 수 있습니다.

마무리

HTTP 시맨틱 자체에 대한 내용은 HTTP 완전 가이드에서 별도로 다루고 있습니다. 이 글에서 다룬 내용을 한 문장으로 정리하면, 실무에서 TLS 관련 장애는 인증서 문제가 가장 먼저 발생하고, 그 다음이 협상·성능 이슈, 마지막이 0-RTT처럼 편의성과 안전성을 맞바꾼 기능의 오용입니다. 각 단계는 실제로 한 번씩 겪어봐야 체감되는 경우가 많지만, 최소한 인증서 만료 알림만은 지금 바로 설정해 두는 것이 좋습니다.

같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.