GREASE: 일부러 아무 의미도 없게 만든 TLS 값들

당신 브라우저는 암호가 아닌 암호와 아무 일도 안 하는 확장을 서버에 하루 수십억 번 제안한다. 이름은 GREASE, TLS가 계속 변할 수 있게 지켜주는 유일한 장치다.

브라우저가 HTTPS 연결을 시작하는 순간의 네트워크 트레이스를 열어 제안하는 암호 스위트 목록을 보라. 진짜 암호들보다 앞, 맨 위쯤에 종종 0x0A0A 같은 값이 있다. 이건 암호 스위트가 아니다. 한 번도 암호 스위트였던 적이 없다. RFC가 앞으로도 절대 암호 스위트가 되지 않을 것을 보장한다. 그런데도 브라우저는 그걸 서버에 제안한다 — 매일, 자기가 맺는 TLS 연결의 아주 큰 몫에서, 일부러.

그 값이 GREASE이고, Chrome이 왜 핸드셰이크에 의도적 헛소리를 실어 보내는가의 이야기는, 인터넷이 자기 프로토콜을 바꿀 능력을 하마터면 잃을 뻔한 이야기다.

미래를 위해 예약, 도착과 동시에 사망

확장 가능한 모든 프로토콜은 같은 약속을 한다. 값의 목록이 있고 — 암호 스위트, 확장, 버전 — 명세는 말한다: 모르는 값을 보면 무시하고 계속 진행하라. 그 조항이 프로토콜이 자랄 수 있는 이유 전부다. 내년에 새 암호를 추가해도, 그보다 오래된 소프트웨어는 어깨를 으쓱하고 자기가 아는 걸로 협상해야 한다.

TLS는 정확히 이렇게 말한다. 모르는 확장이나 모르는 암호 스위트를 받은 구현은 그걸로 질식하지 말고 무시해야 한다. 그게 TLS 1.3이 TLS 1.2용으로 만든 서버와 대화하게 하고, 새 키 교환 그룹이 세상을 망가뜨리지 않고 배포되게 하는 계약이다.

아무도 확인하지 않는 계약의 문제는 이거다: 썩는다. 구현자가 TLS 스택을 만들어 오늘 존재하는 값들로 테스트하고, 잘 되는 걸 보고, 출시한다. “모르는 값 무시” 경로는 그의 테스트에서 한 번도 안 돈다 — 그의 테스트엔 모르는 값이 없으니까. 그래서 버그가 그 경로에 몇 년씩 앉아 있을 수 있다 — 모르는 암호 스위트를 보는 순간 핸드셰이크를 거부하는 서버, 미래의 버전 번호를 광고하는 ClientHello를 떨궈버리는 미들박스 — 그런데 아무도 눈치 못 챈다. 그걸 건드릴 무언가를 아무것도 보내지 않으니까. 확장 지점이 “지원”되는 방식은, 10년째 쇠사슬로 잠긴 비상구가 “지원”되는 방식과 같다. 필요해지기 직전까지는 멀쩡해 보인다.

이걸 인터넷의 모든 방화벽, 로드 밸런서, “보안” 장비, 반쯤 방치된 서버로 곱하면, 집단적 결과가 경화(ossification)다: 프로토콜이 너무 많은 박스에 너무 여러 번 파싱되어, 각 박스가 유효한 핸드셰이크의 모양을 저마다 하드코딩한 탓에, 인터넷의 상당한 몫이 나가떨어지지 않고서는 새 값을 도입할 수 없게 된다. 명세는 TLS가 확장 가능하다고 말한다. 배포된 현실은 콘크리트에 굳었다고 말한다.

버전 춤, 그리고 그 대가

TLS는 이 실패를 이론화하지 않았다. 겪었다.

수년간 브라우저는 버전 폴백 춤이라는 초라한 루틴을 돌렸다. 브라우저는 자기가 지원하는 가장 높은 TLS 버전으로 핸드셰이크를 시도한다. 그게 실패하면 — 깔끔한 에러가 아니라, 그냥 떨궈지거나 리셋된 연결이면 — 그 버전을 이해 못 하는 너무 낡은 서버나 미들박스에 부딪혔다고 가정하고 더 낮은 버전으로 재시도한다. 그리고 또 낮춘다. 뭔가 될 때까지 계속 내려간다.

그 춤의 이유는 순수한 경화였다: 너무 많은 서버와 미들박스가, 명세가 시킨 대로 우아하게 협상을 낮추는 대신, 자기가 모르는 버전을 광고하는 ClientHello를 거부했다. 그래서 브라우저는 재시도로 망가진 박스를 덮었다 — 강한 버전의 망가진 연결보다 약한 버전의 되는 연결이 나으니까.

공격자들이 알아챘다. 첫 핸드셰이크를 실패하게 만들 수 있다면 — 능동적 네트워크 공격자는 연결을 툭 쳐서 그렇게 할 수 있다 — 브라우저가 스스로를 다운그레이드하게, 자기가 깰 줄 아는 데까지 쭉 내려가게 강요할 수 있다. 그게 2014년 POODLE이 헤드라인으로 만든 공격 부류다: 브라우저 자신의 재시도 욕심이 전적으로 몰고 간, 낡은 암호로의 다운그레이드. 업계는 패치를 붙였다. TLS_FALLBACK_SCSV라는 신호 값으로, 클라이언트가 서버에 “나는 폴백해서 여기 내려온 것뿐이니, 당신이 실제로 더 나은 걸 지원한다면 거부해 달라”고 말할 수 있게. 통했다. 하지만 그건 증상에 대한 처방이었다. 병은, 망가진 서버가 아무도 그들의 확장성 버그를 드러내지 않는 탓에, 보이지 않게 계속 망가진 채로 있어도 됐다는 것이었다.

그리고 상황이 워낙 나빠져서, TLS 1.3 — 현재 버전 — 은 전선(wire) 위에서 자기 모습으로조차 보이지 않는다. TLS 1.2 핸드셰이크의 모양을 중심으로 경화된 미들박스를 통과하려고, TLS 1.3은 변장을 한다: 레코드는 여전히 버전 1.2라 주장하고, 실제로는 필요도 없는 무의미한 ChangeCipherSpec 메시지를 보내고, 그냥 익숙해 보이려고 레거시 세션 ID 필드를 채운다. 진짜 버전 협상은 확장 안에 숨는다. 현대적 프로토콜이 살아남으려고, 새것을 감당 못 하는 박스들 앞에서 옛것으로 위장해야 했다. 누구도 자랑스러워하지 않은 설계다. 협박 편지에 가깝다.

헛소리를 일부러 보내라

GREASE는 진짜 병을 공격하는 처방이다. 구글 엔지니어 David Benjamin에게서, Chrome 뒤의 TLS 스택에서 나왔고, 2020년 1월 RFC 8701로 정리됐다. 이름은 역두문자어 — Generate Random Extensions And Sustain Extensibility — 이고, 발상은 지저분하면서 동시에 탁월하다.

“모르는 값 무시” 경로가 아무도 그걸 돌리지 않기 때문에만 망가진다면, 돌려라. 끊임없이. 진짜 연결에서, 진짜 서버를 향해, 항상.

그래서 Chrome은 — 이제 대부분의 브라우저가 — 핸드셰이크에 GREASE 값을 뿌린다: 서버가 무시해야 할 가짜 암호 스위트, 가짜 확장, 가짜 지원 그룹, 가짜 버전 번호, 가짜 ALPN 프로토콜. RFC 8701은 두 바이트 필드용으로 열여섯 개를 예약하는데, 전부 오해할 수 없는 패턴을 따른다 — 0x0A0A, 0x1A1A, 0x2A2A, 0xFAFA까지, 하나같이 0x?A?A 형태다. 어떤 구현자도 연속된 블록을 특수 처리해 편법 쓰지 못하게 수 공간에 얇게 흩뿌려져 있고, 영원히 예약돼 있다: 어떤 진짜 TLS 기능도 이 값을 배정받지 않으므로, GREASE 값은 무의미한 채로 남을 것이 보장된다.

제대로 만든 서버는 GREASE 암호 스위트를 보고, 못 알아보고, 명세가 요구하는 대로 정확히 무시하고, 진짜를 협상한다. 아무 일도 안 일어난다. 그게 성공 사례다 — 확장성 경로가 여전히 작동함을 증명하는 지루한 무사건. 하지만 옛 편협 버그를 가진 서버나 미들박스는 모르는 값을 보고 즉시 나가떨어진다. 평범한 연결에서, 훤히 보이는 데서. 이제 무언가가 매 핸드셰이크마다 방아쇠를 보내기 때문에, 버그는 더 숨을 수 없다. 몇 년 뒤 그게 새 프로토콜이 배포 못 되는 하중 지지 이유가 되는 대신, 개발 중에, 혹은 박스가 출고되는 날에 드러난다.

백신이다. 무해한 미지의 용량을 주입해, 미지를 견뎌야 할 면역계 — 그 코드 — 가 계속 작동하고 안 될 때 즉시 잡히게 한다. GREASE는 어떤 개별 연결도 더 안전하게 만들지 않는다. 생태계 전체가 나중에 보안을 추가할 수 있는 상태로 유지되게 한다 — 더 어렵고 더 값진 쪽이다.

패턴은 번진다

한번 이 수법을 보면, 어디서나 보인다. 경화는 TLS 문제가 아니라 “누구든 들여다볼 수 있는 모든 필드” 문제이기 때문이다.

QUIC 헤더엔 명세상 항상 1로 설정되던 고정 비트가 있다. 예상대로, 박스들은 “그 비트는 항상 1”을 QUIC을 탐지하고 훼방 놓는 방법으로 쓰기 시작했다. 그래서 IETF는 RFC 9287 “Greasing the QUIC Bit”을 냈다 — 엔드포인트가 그 비트를 무작위화하기로 합의할 수 있게, 상수여야 했던 그 한 비트를 정확히 같은 이유로 그리싱한 것이다. HTTP/2 구현은 피어가 무시해야 할 모르는 설정과 프레임을 보낸다. 일부러 보내는 예약값은 Chrome의 TLS 코드 속 해킹에서 스택 전체의 표준 방어 수단으로 승격됐다.

GREASE를 보안 도구 각주로 바꾸는 반전도 있다. TLS 지문 — JA3, 그 후계자 JA4 — 은 클라이언트가 제안하는 암호 스위트와 확장의 정확한 목록을 해싱해 클라이언트를 식별한다. GREASE가 그 순진한 버전을 깼다. 값이 연결마다 무작위면 모든 Chrome 핸드셰이크가 다른 값으로 해싱돼 지문이 무용지물이 되니까. 지문 도구들은 해싱 전에 0x?A?A 값을 걸러내는 법을 배워야 했다. 그러자 Chrome은 2023년에 한 발 더 나가 확장의 순서까지 무작위화하기 시작했다 — 서버들이 고정된 순서를 중심으로 경화되어, “정확히 이 순서의 확장들”을 진짜 브라우저의 서명으로 취급하기 시작했기 때문이다. 같은 병, 같은 처방: 어떤 것이 의존될 수 있고 그 의존이 프로토콜을 얼릴 것이라면, 아무도 의존 못 하게 그걸 변하게 만들어라.

그 아래 깔린 조용한 교훈이 이거다. “모르는 값을 반드시 무시해야 한다(MUST)“는 명세는 그저 말일 뿐, 스스로는 아무것도 안 한다 — 너무 늦어 손쓸 수 없기 전까진 아무도 그걸 지키게 만들지 않으니까. 프로토콜을 실제로 확장 가능하게 지키는 건 RFC의 조항이 아니라, 그 조항을 끊임없이 시험대에 올리는 트래픽이다. 인터넷이 진화할 능력은, 브라우저가 언젠가 필요할지 모를 문이 녹슬어 닫히지 않도록 의도적 헛소리를 영원히 보내는 데 달려 있는 것으로 드러났다. 0x0A0A는 아무 의미도 없다. 그게 요점의 전부다.

토론 참여

← 블로그로 돌아가기