브라우저가 HTTP 요청의 첫 바이트를 보내기 전에, 서버와 작은 의식을 치러야 한다.
더 이상 대단한 의식은 아니다. TLS 1.3이 그렇게 만들었다. 하지만 여전히 약속과 증명과 키 재료를 주고받는 실제 교환이고, 50-100밀리초 안에 일어난다 — 대부분의 개발자가 생각조차 안 할 만큼 빠르게. https://를 치고, 페이지가 뜨고, 핸드셰이크는 칠 아래로 사라진다.
사라지면 안 된다. 현대 웹 스택에서 가장 잘 만들어진 엔지니어링 중 하나다.
1단계: ClientHello
클라이언트가 카드를 테이블에 올린다.
ClientHello 메시지에 포함되는 것: 브라우저가 지원하는 TLS 버전, 암호 스위트 목록(키 교환, 암호화, 해시 알고리즘의 조합), 32바이트 랜덤 값, SNI를 통한 서버 이름, 그리고 가장 중요한 key_share.
이전 핸드셰이크와의 큰 속도 차이다. TLS 1.2에서는 브라우저가 서버의 키 교환 알고리즘 선택을 기다려야 키 재료를 생성할 수 있었다. TLS 1.3은 안 기다린다. 클라이언트가 서버가 선호할 알고리즘(보통 X25519)을 추측하고, 임시 키 재료를 생성하고, 미리 포함시킨다.
이 결정 하나가 TLS 1.3이 더 빠르게 느껴지는 이유다. 프로토콜이 기다리는 예의를 버렸다.
2단계: ServerHello
서버가 클라이언트 제안 중에서 고른다. 암호 스위트를 선택하고, 자기 랜덤 값으로 응답하고, 클라이언트의 키 교환 추측에 동의하면 자기 key_share를 포함시킨다.
이 시점에서 양쪽 모두 랜덤 값과 키 공유를 교환했다. 각각 독립적으로 Diffie-Hellman을 써서 같은 공유 비밀을 유도할 수 있다. 핸드셰이크 암호화 키가 계산된다. 여기서부터 모든 것은 암호화된다.
TLS 1.2에서는 왕복 두 번이 걸렸다. TLS 1.3은 한 번으로 줄였다.
3단계: 서버 인증서
서버가 인증서 체인을 보낸다 — 도메인의 리프 인증서, 중간 인증서, 그리고 암묵적으로 서명한 루트 CA. 체인이 서버의 정체를 증명한다.
브라우저가 루트 스토어 — 약 150개의 신뢰하는 인증 기관 목록 — 와 대조해서 체인을 검증한다. 체인의 어떤 링크가 깨지거나, 만료되거나, 폐기되거나, 비신뢰이면 핸드셰이크가 실패한다.
실제로 가장 자주 문제가 생기는 단계다. 만료된 인증서, 빠진 중간 인증서, 호스트명 불일치. 암호학은 괜찮다. PKI 관리가 사람이 실수하는 곳이다.
4단계: CertificateVerify
서버가 인증서의 개인키를 실제로 갖고 있다는 걸 증명한다. 지금까지의 전체 핸드셰이크 트랜스크립트 — 교환된 모든 메시지 — 의 해시에 개인키로 서명한다.
누군가 인증서를 복사해서 서버인 척하는 걸 막는 단계다. 인증서는 공개다. 개인키는 아니다. 진짜 서버만 이 서명을 만들 수 있다.
5단계: Server Finished
서버가 전체 핸드셰이크 트랜스크립트에 대한 MAC(Message Authentication Code)을 담은 Finished 메시지를 보낸다. 서버가 클라이언트와 같은 메시지를 보았고 전송 중 변조가 없었음을 증명한다.
이 시점에서 서버는 핸드셰이크에 대해 할 말이 끝났다. 애플리케이션 데이터 준비 완료.
6단계: 클라이언트 검증
클라이언트 차례. 루트 스토어와 인증서 체인을 검증한다. 호스트명이 인증서의 Subject(또는 SAN)과 일치하는지 확인한다. CertificateVerify 서명을 인증서의 공개키로 검증한다. Finished MAC이 유효한지 확인한다.
뭐든 실패하면 — 호스트명 불일치, 만료된 인증서, 잘못된 서명, 비신뢰 CA — 브라우저가 연결을 끊고 에러 페이지를 보여준다. 부분 신뢰는 없다. 전부 아니면 전무.
7단계: Client Finished
클라이언트가 자기 Finished 메시지를 보낸다. 핸드셰이크 완료. 양쪽 모두 공유 비밀에서 유도한 동일한 대칭 키를 갖게 됐다.
애플리케이션 데이터가 흐른다. HTTP 요청 — GET, POST, 헤더, 쿠키 — 이 암호화 채널을 통해 이동한다. 핸드셰이크는 끝났다. 50밀리초 걸린 보이지 않는 협상이 완료됐다.
왜 TLS 1.3이 더 빠른가
TLS 1.2는 애플리케이션 데이터가 흐르기 전에 왕복 두 번이 필요했다. TLS 1.3은 한 번. ClientHello의 투기적 키 공유가 트릭이다 — 서버의 선호 알고리즘을 추측해서 왕복 한 번을 절약한다.
반복 연결을 위한 0-RTT 재개 모드도 있다. 클라이언트가 이전 핸드셰이크의 세션 티켓을 캐시하고 첫 메시지에 초기 데이터를 포함시킨다. 주의점: 0-RTT 데이터는 재생 가능하다. 공격자가 첫 메시지를 잡아서 재전송할 수 있다. 그래서 0-RTT는 멱등 요청 — GET, POST 아님 — 에만 써야 한다.
뭐가 잘못되나
각 단계에서 깨질 수 있다. 인증서 만료 — 가장 흔한 실패, 갱신을 까먹어서. 호스트명 불일치. 빠진 중간 인증서 — 서버가 리프만 보내서 클라이언트가 체인을 못 만듦. 시계 오차 — 클라이언트 시스템 시계가 틀려서 유효한 인증서가 만료된 것처럼 보임. 지원하지 않는 암호 — 서버와 클라이언트가 공통 암호 스위트가 없음.
대부분의 TLS 실패는 암호학적이지 않다. 운영적이다. 수학은 작동한다. 인증서를 유지보수하는 사람이 취약한 부분이다.