TCP는 신뢰성 있는 바이트 스트림을 보장한다. 하지만 그 스트림은 평문이다. 패킷이 지나는 경로 위의 라우터, 공유기, ISP 어디서든 내용을 들여다보거나 바꿀 수 있다. HTTPS의 S, 즉 TLS(Transport Layer Security)는 이 신뢰할 수 없는 네트워크 위에서 안전한 채널을 만든다.
안전한 채널은 세 가지를 보장한다. 기밀성(엿볼 수 없음), 무결성(중간에 바뀌면 감지됨), 인증(상대가 사칭이 아님)이다. TLS는 이 셋을 암호화, 핸드셰이크, 인증서 순으로 하나씩 확보한다. TLS는 SSL(Secure Sockets Layer)의 후신이다. SSL 3.0 이후 표준화되며 이름이 바뀌었지만, 관행적으로 SSL이라 부르는 경우가 여전히 많다.
대칭 암호화와 비대칭 암호화
기밀성부터 본다. 데이터를 암호화하려면 키가 필요하다. 암호화 방식은 크게 대칭과 비대칭으로 나뉜다.
대칭 암호화는 암호화와 복호화에 같은 키를 쓴다. AES가 대표적이다. 연산이 빠르고 대용량 데이터에 적합하다. 문제는 키 공유다. 송신자와 수신자가 같은 키를 가져야 하는데, 이 키를 네트워크로 전달하는 순간 도청될 수 있다. 안전한 채널을 만들기 위한 키를 아직 안전하지 않은 채널로 보내야 하는 모순이다.
비대칭 암호화는 공개키(Public Key)와 개인키(Private Key) 한 쌍을 쓴다. 공개키로 암호화한 데이터는 짝이 되는 개인키로만 복호화된다. RSA와 ECC가 여기 속한다. 공개키는 이름 그대로 공개해도 된다. 누구나 공개키로 암호화해 보내면 개인키를 가진 쪽만 읽을 수 있으므로 키 공유 문제가 풀린다. 대신 연산이 대칭 방식보다 훨씬 느리다.
block-beta
columns 2
block:sym["대칭 암호화"]:1
columns 1
s1["같은 키로 암·복호화"]
s2["빠름"]
s3["키 공유 문제"]
end
block:asym["비대칭 암호화"]:1
columns 1
a1["공개키 / 개인키 쌍"]
a2["키 공유 해결"]
a3["느림"]
end
style sym fill:#E3F2FD
style asym fill:#E8F5E9
TLS는 둘을 결합한다. 비대칭 암호화로 대칭키를 안전하게 합의하고, 실제 데이터는 빠른 대칭 암호화로 주고받는다. 비대칭의 키 공유 해결과 대칭의 속도를 모두 취하는 하이브리드 방식이다. 이 대칭키를 합의하는 과정이 핸드셰이크다.
TLS 1.2 핸드셰이크
핸드셰이크는 대칭키를 합의하고 상대를 인증하는 과정이다. TCP 3-way handshake가 끝난 뒤 그 위에서 진행된다.
sequenceDiagram
participant C as 클라이언트
participant S as 서버
C->>S: ClientHello (버전, 암호 스위트 목록, 클라이언트 랜덤)
S->>C: ServerHello (선택된 암호 스위트, 서버 랜덤)
S->>C: Certificate (서버 공개키)
S->>C: ServerKeyExchange (ECDHE 공개값 + 서명)
S->>C: ServerHelloDone
C->>S: ClientKeyExchange (ECDHE 공개값)
Note over C,S: 양쪽이 같은 대칭키 유도
C->>S: ChangeCipherSpec + Finished
S->>C: ChangeCipherSpec + Finished
Note over C,S: 암호화된 애플리케이션 데이터
클라이언트가 ClientHello로 지원하는 TLS 버전과 암호 스위트(Cipher Suite) 목록, 그리고 클라이언트 랜덤 값을 보낸다. 서버는 ServerHello로 그중 하나의 암호 스위트를 선택해 응답하고, 서버 랜덤 값과 인증서를 전달한다. 인증서에는 서버의 공개키가 담겨 있다.
여기서 대칭키를 합의하는 방식이 두 갈래로 나뉜다. 위 다이어그램은 이 중 ECDHE 방식을 기준으로 그렸다.
RSA 키 교환은 클라이언트가 pre-master secret이라는 임의 값을 생성하고, 서버의 공개키로 암호화해 보낸다. 서버만 개인키로 이를 복호화할 수 있다. 이제 양쪽이 같은 pre-master secret을 가지므로, 여기에 클라이언트 랜덤과 서버 랜덤을 섞어 최종 대칭키를 유도한다. 단점이 있다. 서버의 개인키가 훗날 유출되면, 공격자가 미리 기록해 둔 과거 트래픽을 모두 복호화할 수 있다. 개인키 하나가 과거 전체를 여는 셈이다.
ECDHE 키 교환은 이 약점을 없앤다. ECDHE(Elliptic Curve Diffie-Hellman Ephemeral)는 양쪽이 매 세션마다 임시 키 쌍을 만들어 공개값만 교환하고, 각자 상대의 공개값과 자신의 개인값으로 같은 비밀을 계산한다. 개인값은 네트워크에 나가지 않고 세션이 끝나면 폐기된다. 서버의 장기 개인키가 나중에 유출돼도 과거 세션의 키는 복원되지 않는다. 이 성질을 전방 안전성(Forward Secrecy)이라 한다.
핸드셰이크 마지막에 양쪽은 Finished 메시지를 보낸다. 지금까지 주고받은 핸드셰이크 메시지 전체의 해시를 방금 유도한 키로 암호화한 값이다. 중간에서 누군가 메시지를 조작했다면 이 해시가 어긋나 핸드셰이크가 실패한다. 키 합의 과정 자체의 무결성을 확인하는 단계다.
인증서와 PKI
여기까지 기밀성은 확보했다. 하지만 허점이 남아 있다. 클라이언트가 받은 공개키가 정말 접속하려는 서버의 것인지 어떻게 아는가. 중간자(Man-in-the-Middle)가 자기 공개키를 서버의 것인 양 내밀면, 클라이언트는 공격자와 암호화 채널을 맺고도 안전하다고 믿는다. 인증 문제다.
인증서(Certificate)가 이를 푼다. 인증서는 서버의 공개키와 신원(도메인 등)을 묶고, 신뢰할 수 있는 제3자인 CA(Certificate Authority, 인증 기관)가 서명한 문서다. CA는 자신의 개인키로 인증서에 서명한다. 클라이언트는 CA의 공개키로 이 서명을 검증해 “이 공개키는 이 도메인의 것"임을 확인한다.
그런데 CA의 공개키는 또 어떻게 믿는가. 여기서 신뢰 체인이 등장한다.
flowchart TD
R["루트 CA 인증서
OS · 브라우저 내장"] -->|서명| I["중간 CA 인증서"]
I -->|서명| S["서버 인증서
도메인 공개키"]
C["클라이언트"] -.검증 방향.-> S
S -.-> I
I -.-> R
서버 인증서는 중간 CA가 서명하고, 중간 CA 인증서는 루트 CA가 서명한다. 루트 CA 인증서는 운영체제와 브라우저에 미리 내장되어 있다. 이 내장된 루트가 신뢰 앵커(Trust Anchor)다. 검증은 서버 인증서에서 시작해 루트까지 서명을 따라 올라가며 각 단계를 확인한다. 루트에 도달하면 신뢰가 성립한다.
이 전체 구조가 PKI(Public Key Infrastructure, 공개키 기반 구조)다. 공개키를 신원과 묶고, 그 묶음을 계층적 서명으로 보증하는 체계다. 인증까지 확보되면서 안전한 채널의 세 조건이 채워진다.
레코드 보호
핸드셰이크가 끝나면 양쪽은 같은 대칭키를 갖는다. 이후 애플리케이션 데이터는 레코드(Record) 단위로 나뉘어 이 키로 암호화된다. 기밀성과 무결성이 여기서 함께 처리된다.
현대 TLS는 AEAD(Authenticated Encryption with Associated Data, 인증된 암호화)를 쓴다. AES-GCM과 ChaCha20-Poly1305가 대표적이다. AEAD는 암호화와 동시에 인증 태그를 생성한다. 수신 측은 복호화하면서 이 태그를 검증하고, 데이터가 한 비트라도 변조되면 태그가 어긋나 레코드를 거부한다. 과거에 암호화와 별도로 MAC(Message Authentication Code, 메시지 인증 코드)을 따로 계산해 붙이던 방식을 하나로 합친 것이다.
TLS 1.3
TLS 1.2는 오래 쓰였지만 두 가지 부담이 있었다. 핸드셰이크에 왕복이 많고, 안전하지 않은 옵션이 협상 대상으로 남아 있었다. TLS 1.3은 2018년 표준화되며 이를 정리했다.
sequenceDiagram
participant C as 클라이언트
participant S as 서버
C->>S: ClientHello (+ 키 공유)
Note over S: 키 합의 완료
S->>C: ServerHello (+ 키 공유)
S->>C: 암호화된 {Certificate, Finished}
C->>S: 암호화된 {Finished}
Note over C,S: 1-RTT 만에 암호화 데이터
1-RTT 핸드셰이크로 왕복(RTT, Round-Trip Time)을 줄였다. 1.2는 키 교환을 위해 왕복이 두 번 필요했다. 1.3은 ClientHello에 키 교환 정보를 미리 실어 보낸다. 서버는 첫 응답에서 바로 키 합의를 끝내고 암호화를 시작한다. 왕복이 한 번으로 줄어 접속 지연이 감소한다.
위험한 옵션도 제거했다. 1.3은 RSA 키 교환을 아예 없앴다. 키 교환은 항상 ECDHE 계열로만 이뤄지므로, 모든 연결이 전방 안전성을 기본으로 갖는다. 취약한 것으로 알려진 암호 스위트와 옵션도 협상 목록에서 뺐다. 선택지를 줄여 잘못 설정할 여지를 없앤 것이다.
0-RTT도 추가됐다. 이전에 접속한 적 있는 서버라면, 1.3은 핸드셰이크 왕복을 기다리지 않고 첫 패킷에 데이터를 실어 보낼 수 있다. 재접속 지연이 사실상 사라진다. 다만 이 데이터는 재전송 공격(Replay)에 노출될 수 있어 취급에 주의가 필요하다. 세션 재개와 0-RTT의 동작, 그리고 그 위험은 그 자체로 다룰 내용이 많다.
마무리
TCP가 주는 바이트 스트림은 평문이다. TLS는 그 위에서 대칭·비대칭 하이브리드 암호화로 기밀성을, 핸드셰이크와 레코드 보호로 무결성을, 인증서와 PKI로 인증을 확보한다. 세 가지가 합쳐질 때 신뢰할 수 없는 네트워크 위에 신뢰할 수 있는 채널이 선다. 그 위에서 세션 재개, 상호 인증(mTLS), 인증서 자동 발급 같은 실무 주제가 이어지지만, 출발점은 언제나 이 핸드셰이크다.
참고
- TCP와 UDP — TLS가 그 위에서 동작하는 전송 계층
- HTTP/1.1과 HTTP/2 — HTTPS의 맥락과 QUIC/HTTP3가 TLS 1.3을 전송에 통합하는 방식