DKIM 리플레이: 유효한 서명은 동의가 아니다

2025년 4월, no-reply@google.com에서 유효한 Google DKIM 서명과 DMARC 통과를 달고 피싱 메일이 도착했다. 아무것도 위조되지 않았다. 그게 바로 DKIM의 문제다.

2025년 4월, 사람들이 no-reply@google.com으로부터 보안 알림을 받기 시작했다. Google이 실제로 보내는 바로 그 주소다. 메일은 수신자의 Google 계정에 법 집행기관의 소환장이 발부됐다며, 사건을 확인하라고 지원 페이지 링크를 걸었다. From 주소는 진짜였다. DKIM 서명은 Google 키로 검증됐다. SPF 통과, DKIM 통과, DMARC 통과. Gmail은 그 메일을 정상 Google 보안 알림과 같은 대화 스레드에 어떤 경고도 없이 표시했다.

아무것도 위조되지 않았다. 이 대목을 곱씹을 필요가 있다. 공격자는 Google을 스푸핑하지 않았다 — 스푸핑은 무언가를 가짜로 만드는 것인데, 여기서 가짜는 하나도 없었다. 업계가 20년간 쌓아 올린 모든 인증 검사가 피싱 메일 위에서 초록불을 켰다. 그 메일이 진짜, 유효한, Google이 발급한 DKIM 서명을 달고 있었기 때문이다. 서명은 진짜였다. 다만 다들 서명이 한다고 믿는 그 일을 하고 있지 않았을 뿐이다.

서명이 실제로 증명하는 것

DKIM(RFC 6376)은 발신 도메인이 개인 키로 메시지에 서명하고 공개 키를 DNS에 게시하는 방식으로 작동한다. 서명자는 헤더 집합 — 보통 From, Subject, Date, 그 외 몇 개 — 과 본문을 골라 그 위에 서명을 계산하고 DKIM-Signature 헤더에 박아 넣는다. 수신자는 공개 키를 가져와 해시를 다시 계산하고, 일치하면 사실 하나가 성립한다: 이 키를 가진 도메인이 이 정확한 콘텐츠에 서명했다.

이 사실을 조심스럽게 읽어야 한다. 느낌보다 훨씬 좁은 말이기 때문이다. 콘텐츠가 그 도메인에 의해 서명됐다는 말이다. 이 특정 사본을 누가, 누구에게, 몇 번 보냈는지, 그 도메인이 당신이 받기를 의도했는지는 말하지 않는다. DKIM은 메시지를 인증한다. 그 메시지를 당신에게 보내는 행위를 인증하지 않는다. 이 둘은 다른 것이고, 리플레이 공격 전체가 그 사이 틈에 산다.

DKIM 서명에는 수신자 개념이 없다. To 헤더가 서명돼 있어도, 메시지가 다른 곳으로 배달되는 걸 막는 건 없다 — 봉투가 실제로 어디로 라우팅하든, 서명은 자신이 덮은 바이트 위에서 계속 유효하다. 카운터도, nonce도, “이 서명은 1회 배달용” 같은 것도 없다. 유효하게 서명된 메시지는 무기명 토큰이다: 사본을 가진 누구든 그걸 제시할 수 있고, 검증된다. 서명자가 이를 제한할 구체적 조치를 취하지 않는 한 영원히 — 그리고 거의 아무도 그 조치를 취하지 않는다.

서명을 리플레이하는 법

DKIM을 무기명 토큰으로 보는 순간, 공격은 저절로 써진다. 암호를 깰 필요가 없다. 대상 도메인이 당신을 위해 무언가에 서명하게 만들고, 서명된 메시지를 온전히 보존한 채, 피해자에게 다시 보내면 된다. 원래 서명이 함께 이동하고 진짜 도메인의 키로 검증된다. 수신자에게 이건 그 도메인이 보낸 인증된 메일이다 — DKIM 자신의 정의상, 실제로 그렇기 때문이다.

Google 캠페인은 얼마나 적은 게 필요한지 깔끔하게 보여 준다. 공격자는 Google 계정을 만들고 OAuth 애플리케이션을 생성한 뒤, 그 앱 이름을 피싱 메시지 전체 — 소환장 문구까지 전부 — 로 설정했다. OAuth 앱 이름을 지으면 Google이 그에 대한 진짜 보안 알림을 보내고, Google은 그 알림에 성실하게 자기 DKIM 키로 서명했다. 이제 공격자는 눈에 보이는 내용이 자신의 피싱 미끼인, 진짜 Google 서명 메일을 손에 쥐었다. 그는 이 메시지를 서명이 바이트 단위로 온전히 남는 인프라를 통해 표적에게 중계했다. 그것은 no-reply@google.com이 보낸 인증된 메일로 도착했다 — 실제로 정확히 그것이었으니까. 링크는 sites.google.com, 즉 Google 자신의 무료 사이트 빌더에 호스팅된 가짜 지원 포털을 가리켰다. 그래서 목적지조차 google.com 자산 위에 앉아 있었다.

세부는 달라도 형태는 같다. 공격자들은 대형 제공업체의 아웃바운드 서버로 메시지에 서명하게 한 뒤 자기 서버에서 뿌리는 식으로, 또 본문 일부만 서명하는 메시지를 악용해 나중에 새 콘텐츠를 덧붙이는 식으로 같은 수를 뒀다. 모든 버전에서, 신뢰받는 도메인의 평판이 그 도메인이 결코 당신에게 보내려 하지 않은 메일에 박음질된다.

방어책이 전부 땜질인 이유

여기서부터 불편해진다. 방어책은 진짜지만, 하나같이 애초에 이걸 막도록 설계되지 않은 프로토콜에 대한 우회책이기 때문이다. RFC 6376은 보안 고려사항에서 리플레이를 직접 거명하기까지 한다. 나중에 발견된 버그가 아니라, 처음부터 알려진 한계였다.

만료 태그 x=가 있다. 서명 만료를 유닉스 타임스탬프로 지정한다. 창을 좁히면 훔친 서명은 몇 시간만 쓸모 있다. 하지만 명세는 검증자가 x= 이후 서명을 만료로 취급할 수 있다고 말할 뿐 — 요구하지 않는다 — 그리고 RFC 6376은 서명 만료가 독립적 리플레이 방지 수단으로 의도된 게 아니라고 명시한다. 표준이 스스로 제공하지 않는다고 말하는 보장을, 선택적 동작에 기대고 있는 셈이다.

oversigning이 있다. 서명자가 h= 태그에 헤더를 실제 등장 횟수보다 많이 나열하면, 그 헤더의 추가 사본을 나중에 붙일 수 없다고 선언하는 것이다; 하나 슬쩍 넣으면 서명이 깨진다. 서명된 메시지에 헤더를 덧붙이는 공격에 대한 표준 방어이고, 반드시 해야 한다. 하지만 이는 메시지를 수정으로부터 강화할 뿐, 완전히 변경 없이 리플레이된 메시지에는 아무 소용이 없다 — Google 사례의 핵심 수법이 바로 그것이다. 거기선 아무것도 수정되지 않았다.

그리고 평판 계층 자체가 있는데, 이게 잔인한 역설이다. Gmail은 무엇이 받은편지함에 닿을지 정하는 데 도메인 평판에 크게 기댄다. 합리적 설계다 — 20년간 흠 없는 기록을 가진 도메인의 유효한 서명은 무언가 값을 해야 마땅하다. 리플레이는 그 강점을 페이로드로 바꾼다. 도메인의 평판이 좋을수록, 거기서 훔친 서명의 가치가 커진다. 주어진 서명이 인정되는 횟수를 제한하고, oversign하고, 공격적으로 만료시킬 수 있고, 다 해야 한다 — 하지만 그건 모두가 믿는 것보다 덜한 의미를 지닌 서명의 증상을 관리하는 일이다.

진짜 해법은 새 프로토콜

IETF의 답은 또 다른 태그가 아니다. 재설계다. DKIM 워킹그룹은 리플레이를 정면으로 다루도록 재헌장(recharter)됐고, 공식 문제 정의 문서에서 출발했다. 더 긴 궤적은 DKIM2다 — 우리가 20년간 땜질해 온 SPF·DKIM·DMARC·ARC 스택의 결함을 다루며 이메일 인증을 다시 짜려는 시도다. DKIM2가 무엇이 되든, 흥미로운 인정은 구조적이다: DKIM을 유지보수하는 사람들이 리플레이를 들여다보고, 태그로는 빠져나갈 수 없다고 결론 내렸다. 서명은 더 많은 의미를 — 콘텐츠뿐 아니라 발송에 대해 — 담아야 하고, 그건 설정이 아니라 프로토콜을 바꾼다는 뜻이다.

그건 몇 년 뒤 일이고, 이메일이 이메일인 만큼 채택은 그보다 더 느릴 것이다. 그동안의 교훈은 자물쇠 아이콘이 진작 가르쳤어야 할 바로 그것이다: 초록 체크마크는 정확히 하나의 질문에 답하고, 그건 대개 당신이 실제로 던지는 질문보다 좁다. DKIM은 어떤 도메인이 이 바이트에 서명했다고 알려 준다. 그 도메인이 당신이 이걸 갖기를 원했다고는 애초에 말해 줄 수 없었다. 20년간 그 틈은 별로 문제가 되지 않았다. 아무도 굳이 악용하지 않았으니까. 2025년 4월, Google을 대가로, 누군가가 그랬다 — 그리고 체크마크는 내내 초록색이었다.

토론 참여

← 블로그로 돌아가기