TECH 으로 돌아가기
TECH HACKER NEWS 오늘 8분 읽기 26 READS

컴파일러가 암호 코드를 망가뜨린다? DJB의 cryptoint로 보는 ‘상수 시간’ 코드 지키기

암호 코드의 적이 컴파일러일 수도 있어요

Daniel J. Bernstein, 줄여서 DJB라고 불리는 암호학자가 있어요. 여러분이 매일 쓰는 SSH와 TLS 안에 들어 있는 Curve25519, ChaCha20, Poly1305를 만든 사람이에요. 그가 2025년에 cryptoint라는 작은 C 라이브러리와 논문을 공개했는데, 주제가 꽤 흥미로워요. 한마디로 ‘암호 코드를 안전하게 짜 놔도 컴파일러가 최적화하면서 망가뜨린다. 그러니 컴파일러로부터 코드를 지키자’는 거거든요.

먼저, 상수 시간(constant-time) 코드가 뭐냐면

비밀번호를 한 글자씩 비교하다가 틀린 글자가 나오면 바로 false를 돌려주는 함수를 떠올려보세요. 첫 글자부터 틀리면 금방 끝나고, 앞의 세 글자가 맞으면 조금 더 오래 걸리겠죠. 공격자는 이 미세한 시간 차이를 수천 번 재서 정답을 한 글자씩 알아낼 수 있어요. 금고 다이얼을 돌리면서 딸깍 소리로 번호를 알아내는 것과 비슷해요. 이런 공격을 타이밍 사이드 채널 공격이라고 해요.

그래서 암호 구현에서는 비밀 값에 따라 if로 분기하거나, 다른 메모리 주소를 읽거나, 나눗셈처럼 입력에 따라 속도가 달라지는 명령을 쓰면 안 돼요. 대신 비트 마스크 트릭을 써요. 예를 들어 조건에 따라 a나 b를 고를 때 if 대신 이렇게 쓰는 거죠.

mask = -(uint32_t)cond; / cond가 1이면 모든 비트가 1, 0이면 0 /
r = (a & mask) | (b & ~mask); / 분기 없이 선택 /

어떤 값이 들어와도 같은 명령이 같은 순서로 실행되니까 걸리는 시간이 일정해요.

그런데 컴파일러가 이걸 다시 if로 바꿔버려요

문제는 요즘 컴파일러가 너무 똑똑하다는 거예요. 위 코드가 결국 ‘조건에 따라 a나 b를 고르는 것’이라는 걸 알아채면, 그게 더 빠르다고 판단할 때 조건 분기로 되돌려버릴 수 있어요. C 표준은 실행 시간에 대해 아무것도 약속하지 않으니까, 컴파일러 입장에서는 정당한 최적화예요.

이론으로만 그치는 얘기가 아니에요. 2024년에는 양자내성암호 표준인 ML-KEM(Kyber)의 레퍼런스 구현을 특정 버전의 Clang으로 컴파일하면, 분기 없이 짠 마스크 코드가 분기로 바뀌어서 비밀 정보가 샐 수 있다는 사실이 밝혀졌어요. 비밀 값을 상수로 나누는 코드가 실행 시간이 들쭉날쭉한 CPU 나눗셈 명령으로 컴파일되는 KyberSlash 취약점도 비슷한 시기에 나왔고요.

cryptoint는 어떻게 막을까

cryptoint는 int8부터 int64까지, 부호 있는 정수와 부호 없는 정수 타입별로 이런 기본 연산을 함수로 제공해요.

핵심은 구현 방식이에요. amd64나 arm64 같은 주요 아키텍처에서는 인라인 어셈블리로 원하는 명령어를 직접 박아 넣어서 컴파일러가 손대지 못하게 해요. 어셈블리를 못 쓰는 환경을 위한 C 버전에서는 값이 항상 0인 volatile 변수를 연산에 섞어 넣는 트릭을 써요. volatile은 컴파일러에게 ‘이 값은 언제든 바뀔 수 있으니 짐작하지 마’라고 알려주는 표시예요. 컴파일러가 값을 미리 알 수 없으니 마스크 연산을 분기로 단순화하지도 못하는 거죠. 실제 값은 0이라 계산 결과는 그대로고요. 이 라이브러리는 libmceliece나 lib25519처럼 DJB가 관여한 프로젝트에서 실제로 쓰이고 있어요.

업계는 어떻게 대응하고 있나

이 문제와 싸우는 건 cryptoint만이 아니에요. BoringSSL과 OpenSSL은 자체 constant_time 헬퍼와, 빈 인라인 어셈블리로 만든 value barrier를 쓰고 있어요. Rust 쪽에는 subtle 크레이트가 있고, 언어 차원에서 해결하려는 Jasmin 같은 암호 전용 언어도 있어요. LLVM에 상수 시간 선택을 보장하는 내장 함수를 넣자는 제안도 나왔고, 하드웨어 쪽에서는 Intel의 DOIT나 Arm의 DIT처럼 명령어 실행 시간이 데이터와 상관없도록 보장하는 모드가 생겼어요. 결국 언어도, 컴파일러도, CPU도 실행 시간을 보장해주지 않던 공백을 여러 층에서 메우는 중이에요. cryptoint는 그중에서 지금 당장 C 코드에 쓸 수 있는 실용적인 해법이고요.

한국 개발자에게 주는 시사점

국내에서도 KpqC 공모전을 통해 HAETAE, AIMer, SMAUG-T, NTRU+ 같은 국산 양자내성암호 알고리즘이 선정됐고, 공공과 금융권에서 PQC 전환 논의가 본격적으로 시작됐어요. 이런 알고리즘을 C로 구현하고 최적화하는 분이라면 cryptoint의 접근법을 꼭 참고해보세요. 컴파일러 버전이나 최적화 옵션만 바꿔도 안전하던 코드가 위험해질 수 있으니, 바이너리 수준까지 확인해야 한다는 점도 기억해두시고요.

암호를 직접 구현하지 않는 대부분의 개발자에게도 교훈이 있어요. API 토큰이나 HMAC 서명을 ==로 비교하지 마세요. Python은 hmac.compare_digest, Node.js는 crypto.timingSafeEqual, Java는 MessageDigest.isEqual, Go는 subtle.ConstantTimeCompare를 쓰면 돼요. 웹훅 서명을 검증하는 코드에서 의외로 자주 보이는 실수거든요.

마무리

한 줄 정리: 안전한 소스 코드가 안전한 바이너리를 보장하지는 않아요. cryptoint는 그 틈을 메우는 도구예요.

여러분 코드베이스에서 토큰이나 서명을 비교하는 부분, 지금 한번 검색해보면 어떨까요? 컴파일러 최적화 때문에 코드가 의도와 다르게 동작했던 경험이 있다면 같이 나눠주세요.


🔗 출처: Hacker News

SOURCE · HACKER NEWS
원문 전체 보기 → https://cr.yp.to/papers/cryptoint-20250424.pdf
SHARE
NEXT · CHOOSE

변화를 읽었다면,
내가 만들 수익 구조를 고릅니다.

정보를 더 모으는 데서 멈추지 않고, 광고·외주·판매·중개·구독 중 내 상황에 맞는 출발점을 정해보세요.

21가지 수익 구조 살펴보기 →
처리 중...