브라우저가 HTTPS 사이트에 처음 접속할 때는 비싼 춤을 전부 춘다 — ClientHello, 인증서, 키 합의, TLS 핸드셰이크 전 과정, 실제 요청의 첫 바이트가 나가기 전에 왕복 한두 번의 지연. 두 번째 접속 때는 그러고 싶지 않다. 10초 전에 방문한 사이트를 위해 그 비대칭 암호 연산을 다시 하는 건 낭비이므로, TLS에는 지름길이 있다. 서버는 첫 핸드셰이크 끝에 작은 토큰을 건네고, 다음 접속 때 당신은 처음부터 다시 하는 대신 그 토큰을 내민다. 서버가 그걸 알아보고, 양쪽은 이미 합의한 키를 복구하고, 춤의 대부분을 건너뛴다. 이것이 세션 재개(session resumption)이고, 속도 면에서는 정말 좋은 아이디어다.
이건 또한 서버가 당신의 다음 방문 때 당신을 알아볼 수 있다는 뜻이다. 설계상 그렇다. 그게 메커니즘의 전부다 — “여기 토큰이 있으니 다음에 내게 보여줘, 그래야 너인 줄 안다.” 프라이버시 모자를 쓰고 그 문장을 다시 읽으면 문제가 스스로 드러난다: 재개 토큰은 서버가 당신에게 주고, 당신이 저장하고, 돌아올 때 다시 내밀어 재식별되게 하는 물건이다. 그게 바로 쿠키의 정의다. 다만 이건 TLS 계층 아래, 애플리케이션 밑, 쿠키 항아리 바깥에 산다 — 그리고 수년간, 거의 아무도 그걸 보고 있지 않았다.
핸드셰이크를 건너뛰는 두 가지 방법
지름길에는 늘 두 가지 맛이 있었다. 오래된 쪽은 세션 ID다: 서버가 과거 세션 표를 자기 메모리에 들고, 당신에겐 그 행을 가리킬 짧은 식별자를 준다. 더 새롭고 확장성 좋은 쪽은 세션 티켓으로, 2008년 RFC 5077에서 표준화되며 저장 방향을 뒤집었다. 서버가 기억하는 대신, 서버는 세션 상태 전체를 자기만 아는 키로 암호화한 덩어리로 만들어 당신에게 들려 보낸다. 돌아올 때 당신이 티켓을 돌려주면 서버가 복호화하고, 아무것도 저장하지 않고 모든 걸 복구한다. 상태 없고, 싸고, 수백만 클라이언트로 확장된다. TLS 1.3(RFC 8446)은 두 아이디어를 단일 사전공유키(PSK) 메커니즘으로 합치고, 그 위에 0-RTT 조기 데이터를 얹었다 — 재개하는 클라이언트가 핸드셰이크가 끝나기도 전, 맨 첫 비행(flight)에서 요청을 보낼 수 있는 기능이다.
이것들 하나하나가 성능 이득이다. 그리고 하나하나가 구조적으로, 서버가 당신의 복귀를 알아보려고 발급하는 토큰이다. 세션 ID는 서버 메모리 속 행의 이름이다. 티켓은 서버가 자신에게 써서 당신에게 들려 보낸 봉인된 봉투다. 어느 쪽이든, 그걸 내미는 건 나 전에 여기 왔었고, 여기 증거가 있다는 말이다.
그걸 소리 내어 말한 논문
2018년 12월, 함부르크 대학의 네 연구자 — Erik Sy, Christian Burkert, Hannes Federrath, Mathias Fischer — 가 ACSAC에서 Tracking Users across the Web via TLS Session Resumption을 발표했다. ‘지나고 보니 뻔한’ 관찰을 가져다 그게 얼마나 멀리 가는지 측정한 첫 연구였다. 그들은 인기 브라우저 48종과 가장 인기 있는 웹사이트 100만 개의 구성을 들여다봤고, 숫자는 “이론적으로 가능하다”보다 나빴다.
그들이 이름 붙인 핵심 수법은 **연장 공격(prolongation attack)**이고, 나쁜 소식이 흔히 그렇듯 우아하다. 재개 토큰에는 수명이 있다 — 서버가 여전히 그 토큰을 받아주는 창(window). 겉보기엔 그 창이 추적 기간의 상한이다: 가령 하루 안에 돌아오지 않으면 티켓이 만료되고 당신은 다시 낯선 사람이 된다. 하지만 서버가 당신이 재개할 때마다 새 토큰을 발급하는 걸 막는 건 없다. 그래서 당신이 창 안에 돌아오면, 서버는 옛 티켓을 알아보고 시계를 리셋한 새 티켓을 건네며, 창은 다시 당신을 덮도록 앞으로 미끄러진다. 당신이 수명보다 자주 방문하는 한 사슬은 끊기지 않는다. 식별자가 당신의 규칙적 방문을 연료 삼아 무한히 스스로 갱신된다.
실제로 얼마나 나쁜가? 당시 많은 브라우저가 기본으로 쓰던 재개 수명에서, 연구진은 평균 사용자가 연속 연장으로 최대 8일간 추적될 수 있음을 발견했다. 그리고 7일 수명 — TLS 1.3 초안이 권고한 상한이고, 최종 RFC 8446이 넘어선 안 되는 최대치로 남긴 값 — 에서는 그들 데이터셋 사용자의 65%가 영구적으로 추적될 수 있었다. 대부분의 사람은 자기가 쓰는 사이트에 일주일짜리 창이 끊길 틈 없이 자주 돌아오기 때문이다. 위험에 상한을 두려던 권고가, 대부분의 사람에게는 그 상한을 지워버릴 만큼 길었던 셈이다.
왜 이게 쿠키보다 더 아픈가
당신은 웹이 당신을 추적한다는 걸 이미 안다. 그리고 방어하는 심상 모델도 있다: 쿠키를 지우고, 시크릿 창에서 돌고, 제3자 저장소를 막는다. 세션 재개가 더 고약한 건 바로 그 모든 것 밑에 앉아 있기 때문이다.
이건 HTTP 메커니즘이 아니라 쿠키 항아리에 없다. 식별자는 TLS 연결의 속성이고, 첫 HTTP 헤더가 존재하기도 전에 협상된다. 쿠키를 지워도 TLS 세션 캐시는 건드려지지 않는다. ‘인터넷 사용 기록 삭제’ 도구도 그걸 건드리도록 만들어졌다고 장담할 수 없다. 애플리케이션 계층 저장소를 중심으로 설계됐는데 이건 전송 계층 상태이기 때문이다. 좋은 추적자가 늘 그렇듯 눈에 안 보인다 — 가리켜 지울 수 있는 아티팩트를 남기지 않는다. 식별하는 주체가 핸드셰이크 자체, 자물쇠를 그리는 바로 그 핸드셰이크이기 때문이다.
그리고 쌓인다. 혼자서도 재개 토큰은 이건 돌아온 클라이언트다라고 말하고, 그것만으로 한 사이트에서의 방문들을 며칠에 걸쳐 묶기에 충분하다. 하지만 혼자인 법이 없다. 당신의 IP 주소, TLS 지문 — ClientHello의 무의식적 억양 — 요청 타이밍에 그걸 합치면, 그 조합은 어느 하나보다 훨씬 더 식별적이다. 이건 인증서 투명성과 패시브 DNS가 다른 각도에서 계속 가르치는 바로 그 패턴이다: 시스템에 관한 질긴 정보는 그것이 무심코 내뿜는 배기가스에서 나온다. 재개 티켓은 배기가스다. 당신은 빠르게 다시 접속하면서 동시에 기억되기를 거부할 수 없다 — 기억되는 것이 곧 빠르게 만드는 메커니즘이기 때문이다.
그게 대체로 고쳐진 과정
여기가 이 글을 종말 서사가 되지 않게 하는 부분이다. 대응은 진짜였고, 좋은 수정이 어떻게 생겼는지 보여주니 이해할 가치가 있다.
첫 번째 지렛대는 뻔하다: 수명을 줄인다. 프로토콜은 티켓 수명을 7일로 묶지만, 브라우저가 자기 재개 상태를 그보다 훨씬 짧게 유지하는 걸 막는 건 없다. 짧을수록 돌아온 방문자가 창 밖으로 떨어지는 일이 잦아지고 사슬이 끊긴다. (논문에 대응해 열린 모질라 버그는 당시 파이어폭스 기본값이 48시간으로 보인다고 적었다.) 세션 수명을 24시간 미만으로 두라던 옛 RFC 5246의 조언이, 보이던 것보다 더 중요했던 셈이다.
두 번째 지렛대가 교차 사이트 구멍을 실제로 막는 쪽이다: 분리(partitioning). 진짜 공포 시나리오는 한 사이트가 당신을 기억하는 게 아니라 — 수천 사이트에 박힌 추적자가 단일 재개 신원으로 당신을 그 전부 사이에서 따라오는 것이다, 옛 제3자 쿠키가 그랬던 식으로. 수정은, 2021년경부터 브라우저가 출시한 더 넓은 ‘상태 분리(state partitioning)‘·네트워크 분리 작업의 일부로 — 2021년 1월 파이어폭스 85는 분리 대상에 TLS 세션 식별자를 명시했다 — TLS 재개 캐시를 당신이 방문 중인 제1자 사이트로 키를 매기는 것이다. 한 사이트에 있는 동안 얻은 재개 티켓은 다른 사이트에 있는 동안엔 아예 제시될 수 없다. 파이어폭스는 그보다 앞선 2019년에 연결 로직에 검사를 하나 엮어 두었다 — 재개 티켓은 채널이 저장소 검사를 통과할 때만 소비된다. Tor 브라우저는 늘 그렇듯 가장 멀리 갔다. 세션 티켓과 세션 ID를 아예 끈다. 조합 — 짧은 수명 + 제1자 분리 — 은 교차 사이트 슈퍼쿠키를 원래 의도대로 되돌린다: 페이지를 따라올 수 없는, 세션 내 속도 최적화로.
상태를 공정히 말하고 싶다 — 최신으로 업데이트된 주류 브라우저에서 이것의 교차 사이트 버전은 대체로 처리됐다. 지금 당신의 브라우저가 사이트를 넘나드는 TLS 슈퍼쿠키를 흘리고 있을까 걱정하며 이 글을 읽고 있다면, 아마 아니다. 사람들이 일을 했기 때문이다. 하지만 ‘대체로’가 무게를 지고 있다. 분리는 브라우저 안에 살고, 인터넷의 긴 꼬리는 브라우저가 아니다 — 앱 속 HTTP 라이브러리, 임베디드 기기에 박힌 SDK, 런타임 기본값으로 TLS 연결을 여는 스크립트다. 그것들은 아무것도 분리하지 않는다. 서버는, 그리고 경로 위 어떤 미들박스든, 재개를 여전히 그 정체 그대로, 모든 클라이언트에서, 영원히 본다. 벡터가 사라진 게 아니다. 대부분의 사람이 마침 브라우징하는 그 한 계층에서 방어됐을 뿐이다.
그 밑에 있는 교훈
RFC를 벗겨내면 이건 신원이 실제로 어디서 오는가에 관한 이야기이고, 불편한 쪽이다. 우리는 추적을 웹에 덧붙여진 것으로 생각하는 경향이 있다 — 누군가 설정하기로 한 쿠키, 누군가 박기로 한 픽셀, 악당이 딸린 결정. 하지만 세션 재개는 추적 결정이 아니었다. 아무도 앉아서 슈퍼쿠키를 만들지 않았다. 누군가 앉아서 두 번째 핸드셰이크를 빠르게 만들려 했고 — 그건 명백히 좋은 목표다 — 추적이 메커니즘에서 공짜로 떨어져 나왔다. 돌아온 클라이언트를 알아보는 것과 돌아온 클라이언트를 추적하는 것이, 두 사람이 서로 다르게 묘사한 같은 연산이기 때문이다.
그게 경계해야 할 모양이고, TLS 지문과 같은 모양이다: 가장 질긴 식별자는 다른 무언가를 떠받치고 있는 것들이다. 제거하면 누군가 의존하는 기능이 깨지기에, 제거되지 않는다. 수명을 줄일 수 있고, 캐시를 분리할 수 있고, Tor 브라우저처럼 재개를 아예 끌 수 있다 — 전부 좋고, 전부 진짜다. 할 수 없는 건 클라이언트를 다시 접속하기엔 빠르면서 동시에 알아볼 수 없게 만드는 것이다. 그 밑에서, 그 둘은 두 이름을 쓰고 있는 한 가지이기 때문이다.