Skip to content
ioob.dev
Go back

AI가 Cloudflare 암호 라이브러리에서 진짜 버그 7개를 찾았다

AI Meets Cryptography 1: What AI Found in Cloudflare's CIRCL

원문 보기 · zkSecurity · Stefanos Chaliasos, Hao Pham (zkSecurity) ·
· 13분 읽기

zkSecurity가 자사 AI 감사 파이프라인을 Cloudflare의 실험적 암호 라이브러리 CIRCL에 겨눴고, 실제 버그 7개를 확정했습니다. threshold RSA의 float64 정밀도 손실부터 속성 기반 암호(ABE)의 접근 제어 완파까지, 전부 이미 upstream에 반영돼 고쳐졌고요. 대부분은 Cloudflare의 HackerOne 바운티로 보상까지 받았습니다.

그런데 zkSecurity가 정말 하고 싶은 이야기는 버그 목록이 아닙니다. 이건 오픈소스 암호 코드에서 AI가 어디서 예리하고 어디서 눈이 머는지를 관찰한 실험 기록에 가까워요. 원문의 표현대로 “버그는 눈에 보이는 산출물일 뿐이고, 우리가 가장 아끼는 건 추론 패턴 쪽”(While the bugs are the visible output, the reasoning patterns are the part we care about most)입니다. 그리고 AI가 뽑은 건 최종 리포트가 아니라 후보였다는 단서가 처음부터 붙습니다. 사람이 각 이슈를 검증하고, 익스플로잇 가능성을 따지고, POC를 최소화하고, 공개 절차를 처리했죠. “AI 후보 findings는 싸지만, 믿을 만한 리포트는 비싸다”(AI candidate findings are cheap while trustworthy reports are not).

float64 정밀도 손실부터 접근 제어 완파까지, 버그 7개

먼저 짚어둘 것 하나. AI가 자기 발견에 매긴 심각도는 노이즈가 심합니다. 아래 표에 AI가 매긴 등급과 Cloudflare가 수정 후 확정한 등급을 나란히 놓았습니다.

#버그AI 심각도CF 심각도발견
1TSS/RSA 다항식 평가의 float64 정밀도 손실CriticalLowOpus 4.6 + skills
2qndleq 증명 위조(prover가 SecParam 제어)HighLowOpus 4.6 + skills
3BLS 집계 검증에 메시지 고유성 부재MediumHighOpus 4.6 + skills
4DLEQ 건전성 파괴(FillBytes 부호 충돌)HighLowOpus 4.6 + skills
5HPKE PSK 검증 우회(비트 OR switch)MediumMedium(중복)GPT-5.3 + skills
6TSS/RSA Lagrange 계수 int64 오버플로우HighMediumOpus 4.6 + skills
7CP-ABE 접근 제어 파괴(AND-share 실수)CriticalCriticalzkao

AI 등급과 확정 등급이 어긋나는 폭 자체가 관전 포인트인데요. 표를 그대로 옮기는 대신 성격이 다른 버그 몇 개만 파고들어 보겠습니다.

부동소수점으로 암호 다항식을 계산하면

1번은 CIRCL의 threshold RSA(tss/rsa)에 있습니다. threshold 서명은 비밀을 n명에게 Shamir 방식으로 쪼개고, Deal()이 각 참가자의 인덱스에서 비밀 다항식을 평가하죠. 계수는 마땅히 big.Int인데, 정작 x^i 항을 이렇게 계산했습니다.

// tss/rsa/rsa_threshold.go
xi := int64(math.Pow(float64(x), float64(i)))

float64의 가수(mantissa)는 53비트입니다. 결과가 대략 2^53을 넘는 순간, 정수로 다시 캐스팅되기도 전에 조용히 반올림돼요. 참가자 100명에 threshold 27이면, 계산해야 할 값이 2^53을 36자릿수쯤 넘어버립니다. 지수가 조금만 커도 이미 깨지는 거죠. 결과적으로 다항식이 틀리게 평가되고, 참가자에게 넘어가는 키 share가 어긋납니다. 서명 조합이 아예 실패하거나, 멀쩡해 보이지만 원래 키로 복원되지 않는 share가 나오고요. AI는 이걸 critical로 봤지만 Cloudflare는 실제 발생 조건이 드물다는 이유로 Low로 확정했습니다. 수정은 코드 안 TODO 주석이 진작 권하던 대로 Horner 방식 평가로 바꿔 전 과정을 big.Int에 묶는 쪽이었습니다.

부호 하나가 무너뜨린 건전성

배치에서 가장 미묘하고, 솔직히 가장 흥미로운 버그는 4번입니다. zk/qndleq의 DLEQ 증명인데, 증명을 손대지도 않고 위조가 됩니다.

정직하고 유효한 증명 pi가 statement (g, gx, h, hx)에 대해 있다고 하죠. x를 모르는 공격자가 바로 그 pi를 그대로 들고 와서, 다른 statement에 붙입니다. gx 대신 -gx(즉 new(big.Int).Neg(gx))를 넣은 statement예요. 이 위조 statement가 challenge c가 짝수일 때마다 통과합니다. 두 가지가 동시에 맞아떨어지기 때문인데요.

하나는 대수적 상쇄입니다. 검증자가 값을 재계산할 때 c가 짝수면 부호 인자가 그대로 소거되어, 정직한 prover가 만든 것과 똑같은 중간값이 복원돼요. 다른 하나는 해시의 부호 충돌이고요. challenge는 statement를 해싱해 뽑는데, 이 해싱이 FillBytes를 씁니다. FillBytesbig.Int의 절댓값만 쓰고 부호를 버리죠. 그래서 doChallenge(..., -gx, ...)doChallenge(..., gx, ...)가 같은 값으로 해싱됩니다.

// even challenge c를 갖도록 고른, (g, gx, h, hx)에 대한 정직한 증명
gxNeg := new(big.Int).Neg(gx) // -gx, 공격자는 x를 몰라도 됨
forgedAccepted := proof.Verify(g, gxNeg, h, hx, N) // accepted!

c가 짝수일 확률은 최소 1/2(해시 출력의 최하위 비트일 뿐입니다)이므로, 정직하게 생성된 증명의 대략 절반에 공격이 먹힙니다. 이게 눈에 띄는 이유는 대충 짠 한 줄이 아니라서예요. 대수적 항등식과, 아무 죄 없어 보이는 직렬화 선택(FillBytes가 부호를 떨구는 것)의 상호작용입니다. “둘 중 어느 것도 그 자체로는 틀리지 않았다. 합쳐지면 건전성이 무너진다”(Neither is wrong on its own. Together they break soundness). zkSecurity가 모델의 발견에서 가장 놀란 지점이 정확히 이 경계를 가로지르는 추론이었습니다. 심각도는 AI가 건전성 파괴라 High로 봤지만, Cloudflare는 공격 난도가 높다는 이유로 Low로 확정했고요. 수정은 challenge 계산에 0 < x < N을 요구하는 checkBounds를 넣어 음수 -gx를 초입에서 거부하는 방식입니다.

와일드카드 잎 하나가 정책 전부를 열어젖힌 CP-ABE 완파

7번은 zkao가 혼자 찾은 버그입니다. 위 여섯 개를 확정한 뒤 같은 라이브러리를 zkao에 물렸더니 이걸 물어왔습니다. 속성 기반 암호(abe/cpabe/tkn20)의 접근 제어 보장을 통째로 깨는 버그예요.

CP-ABE는 (location: usa AND department: finance) OR (role: admin) 같은 정책으로 메시지를 암호화합니다. 사용자는 자기 속성에 묶인 키를 쥐고, 그 속성이 정책을 만족할 때만 복호화되죠. 내부적으로 tkn20은 정책을 AND/OR 게이트 트리로 바꾸고, 하나의 비밀(메시지를 지키는 키)을 그 트리로 내려보내며 나눕니다. OR 게이트는 두 자식에게 비밀 전부를 주고(어느 쪽이든 만족하면 되니까), AND 게이트는 비밀을 쪼갭니다. 한 자식은 랜덤 r, 다른 자식은 parent - r, 둘을 더해야만 부모가 복원되고요. 이 버그를 이해하는 데 필요한 건 AND 게이트 하나뿐입니다. 두 자식이 각자 부분 share만 받아야 하고, 어느 한쪽도 혼자서 부모를 복원해선 안 되죠.

share는 AND를 이렇게 처리했습니다.

// abe/cpabe/tkn20/internal/tkn/formula.go
case Andgate:
    shares[gate.In0], err = randomMatrixZp(rand, k.rows, k.cols) // In0 = 랜덤 r
    ...
    shares[gate.In1] = newMatrixZp(k.rows, k.cols)               // In1 = 0
    shares[gate.In0].sub(shares[gate.Out], shares[gate.In1])     // In0 = parent - 0

랜덤 share를 만들어놓고 곧바로 버립니다. In1을 0으로 두고, 마지막 줄이 In0parent - In1로 덮어쓰는데 이게 그냥 parent죠. 한 자식이 비밀 전부를 갖고 다른 자식은 아무것도 못 갖습니다. AND가 더 이상 AND가 아니에요. 여기서 짚어둘 점 하나. 이게 정확성을 깨지는 않습니다. 두 share를 더하면 여전히 부모가 되니(parent + 0 = parent) 정책을 만족하는 키는 멀쩡히 복호화되고, 옛 코드로 만든 암호문도 호환되고요. 깨지는 건 비밀성.

이걸 완전한 파괴로 만드는 건 이 망가진 AND 게이트가 놓인 위치입니다. CCA 보안을 위해 tkn20은 Boneh-Katz 변환으로 모든 정책을 바깥쪽 AND 게이트로 한 번 더 감싸는데, 그 게이트의 왼쪽 자식이 내부 “와일드카드” 잎이에요. 권한 기관이 발급하는 모든 속성 키가 이 와일드카드를 달고 나옵니다. 이제 두 사실을 겹쳐보죠. 와일드카드 잎은 AND 게이트의 첫 자식(In0)이고, In0은 하필 비밀 전부를 받는 그 자식입니다. 그러니 모든 키가 정책과 무관하게 메시지 키를 혼자 복원하는 잎 하나를 쥐고 있는 꼴이 됩니다. 수정은 한 줄.

shares[gate.In1].sub(shares[gate.Out], shares[gate.In0]) // In1 = parent - random

zkSecurity가 감탄한 건 zkao가 단순 오타로 보이는 이 한 줄을 CP-ABE라는 복잡한 개념 위에서 제대로 추론해 영향을 정확히 평가했다는 점입니다. 많은 LLM은 오타를 짚고도 “심층 방어” 또는 “코드 위생” 이슈로 치부하고 더 파고들지 않죠. 그러면 개발자가 취약점을 과소평가하거나 놓칩니다.

AI가 medium이라 매긴 교과서적 치명 결함

배치에서 AI가 유일하게 과소평가한 건 3번, BLS 집계 검증입니다. AI는 medium이라 붙였습니다. 실은 교과서에 실린 rogue key 공격, 널리 알려진 치명급 결함인데요. zkSecurity는 critical로 신고했고 Cloudflare는 high로 확정했습니다.

sign/blsVerifyAggregate는 BLS BASIC 집계 모드를 구현합니다. 이 모드는 배치의 모든 메시지가 서로 다를 때만 안전하고, 그게 rogue key 공격에 대한 방어예요. 함수는 집계 페어링 등식은 확인하면서 메시지 고유성은 검사하지 않았습니다. 그 중요한 요구사항을 호출자에게 떠넘겼죠. 그러면 표준 rogue key 공격이 성립합니다. 피해자의 공개키와 메시지를 본 공격자가, 피해자의 비밀키를 전혀 모른 채 집계 서명을 위조할 수 있어요. CIRCL엔 기댈 proof-of-possession 인프라도 없어서 빠진 검사가 더 위험하고요.

왜 AI가 이걸 medium이라 불렀을까요. 이유는 zkSecurity도 모릅니다. 추론을 읽어보니 빠진 고유성 검사를 정확히 짚었고 rogue key 공격 이름까지 댔는데, 그다음 “BASIC 모드의 계약이 고유성 요구를 호출자에게 둔다”는 사실에 닻을 내렸습니다. “호출자가 알아서 처리해야 한다”를 완화 요소로 취급하고 등급을 깎아버렸죠.

이 한 건이 zkSecurity가 뽑은 첫째 교훈으로 이어집니다. “AI는 심각도에 약하다. 그것도 비대칭으로 약하다”(The AI is bad at severity, and bad in an asymmetric way). 표 대부분에서 AI는 영향을 과대평가했지만, BLS에선 반대로 널리 알려진 치명 결함을 medium으로 깎았습니다. zkSecurity의 가설은 이렇죠. CIRCL처럼 여러 애플리케이션이 갖다 쓰는 라이브러리는 영향이 모델 눈에 안 보이는 downstream 호출자에 달려 있어서, 판단 자체가 원래 어렵습니다. 여기엔 짚어둘 단서가 하나 더 붙는데요. Cloudflare의 등급은 자사 바운티 프로그램 관점, 즉 그 버그가 Cloudflare의 라이브 서비스에 영향을 주느냐로 매겨졌습니다. 2번은 증명 건전성을 완전히 깼는데도 low인데, 이는 해당 코드가 Cloudflare 서비스에서 안 쓰이거나 영향이 제한적이라는 뜻일 뿐, 다른 배포에서도 영향이 작다는 말은 아닙니다. 지금으로선 심각도 판정은 사람이 쥐는 자리예요.

모델 짝은 대칭이 아니고, 역할은 뒤집힌다

두 번째 교훈은 모델 조합 쪽입니다. 여섯 버그 중 다섯을 Claude Opus 4.6이 skills와 짝을 이뤄 찾았고, 같은 skills와 같은 시스템 프롬프트 아래 GPT-5.3은 발견보다 검증에 머물렀습니다. 표의 HPKE 버그 하나만 스스로 표면화했고요. 그런데 몇 주 뒤 당시 최신 짝인 Opus 4.7과 GPT-5.4로 다시 돌리자 역할이 사실상 뒤집혔습니다. GPT-5.4가 더 많이 찾고, Opus 4.7은 검증만 하는 쪽이 됐죠. 특정 모델 이름에 결론을 끼워 맞추지 말라는 경고입니다. “프런티어는 그새 또 움직였고, 앞으로도 계속 움직일 것”(The frontier has moved on again since, and it will keep moving)입니다.

세 번째는 6번이 드러낸 습관이에요. AI는 오버플로우와 정수 절단이라는 완전히 독립적인 두 버그를 하나의 finding으로 뭉쳐 신고했습니다. 둘 다 진짜예요. 쓸모 있는 작업이긴 하지만, 관계를 추론하기보다 나란히 늘어놓기만 했습니다. 참인 관찰들을 모아두고도 더 큰 익스플로잇으로 엮지 않는 이 패턴을 zkSecurity는 다른 곳에서도 봤습니다.

zkSecurity는 최인기 암호 crate/package 기준으로 200개 넘는 프로젝트를 스캔해 1,000건 넘는 후보를 얻었고, 오늘의 최대 병목은 발견이 아니라 triage라고 못 박습니다. 신고 하나하나를 전문가가 기술적 유효성부터 확인해야 프로젝트에 닿아요. “우리도 AI 스팸을 남들만큼 싫어하기 때문”(we hate AI spam as much as everyone else does)입니다. 이번 CIRCL 사례가 증명한 건 AI가 프로덕션 암호 코드에서 진짜 취약점을 판다는 사실 자체가 아닌데요. 그 발견을 신뢰 가능한 리포트로 바꾸는 값비싼 사람 노동이 여전히 그 사이에 서 있다는 쪽입니다.


Share this post on:

Comments

Loading comments...