다음에 올 것을 만들어가는 기술에 대한 심층 기사.

eMudhra의 취약한 CA 키: PKI에 관한 5가지 오해

신뢰받는 CA가 밀리초 만에 깨지는 개인 키를 배포했습니다. eMudhra 사건으로 PKI 관련 다섯 가지 통념을 검증합니다.

거대한 열린 금고 문이 작은 장난감 자물쇠 하나로만 잠겨 있는 모습
17개월 동안 인증기관의 보안은 고작 8비트짜리 엔트로피에 기대고 있었습니다.

eMudhra의 인증기관(CA) 개인 키 중 하나는 숫자 4입니다. 384비트짜리 수가 4로 시작하는 게 아니라, 그냥 4입니다. 나머지는 47개의 0 바이트로 채워서 그럴듯하게 보이도록 했죠. 제가 불가능하다고 말했을 법한 종류의 CA 키 생성 실패인데, 이 실수는 감사, Sectigo의 교차 서명, 그리고 프로덕션에 가까운 인프라에서 17개월을 버텼습니다. 누군가 가장 뻔한 방법, 즉 단 1바이트를 무차별 대입해 보기 전까지는요. 이 사건이 완전히 무너뜨리는 PKI에 관한 다섯 가지 믿음을 소개합니다.

오해 1: "구조적으로 유효한 인증서는 안전한 인증서다"

파이프라인의 모든 자동화 검사는 이 인증서들이 괜찮다고 했습니다. 좁고 쓸모없는 의미에서는 맞는 말이었죠. d = 4에 대응하는 공개 키는 P-384 곡선 위의 완벽하게 유효한 점입니다. ASN.1 인코딩도 파싱되고, 서명도 검증되고, 체인도 만들어집니다. 인증서를 훑어보며 형식이 올바른지 확인하는 모든 린터, 구조 검증기, 컴플라이언스 체크리스트는 이것들을 그냥 통과시킬 겁니다. 형식이 올바르니까요.

그게 함정입니다. 구조 검증은 "이 인증서의 형식이 올바른가?"에는 답하지만, "안에 들어 있는 키가 제대로 생성되었는가?"에는 답하지 못합니다. 타원 곡선 키의 경우 이 두 속성은 거의 완전히 독립적입니다. 개인 키는 그냥 스칼라 d이고, 공개 키는 d × G이며, 여기서 G는 곡선의 생성점입니다. 보안 논리 전체는 공개 점으로부터 d를 복원하려면 이산 로그 문제를 풀어야 한다는 데 있습니다. 하지만 그건 d가 실제로 전체 384비트 공간에서 선택되었을 때만 성립합니다. 대신 d를 8비트에서 고르면, "불가능한" 문제는 최대 255번만 도는 반복문이 됩니다.

인증서 자체에서는 이런 낌새가 전혀 보이지 않습니다. d = 4에서 유도된 공개 점은 제대로 생성된 키에서 유도된 것과 똑같이 무작위처럼 보입니다. 눈으로 알아볼 수 없고, 린트로도 잡히지 않습니다. 알아내는 유일한 방법은 작은 값과 알려진 취약 패턴을 시험해 보는 것인데, 실제로 그렇게 됐습니다. 보고자는 Google의 paranoid_crypto 테스트 스위트를 Common CA Database 코퍼스에 기본 설정 그대로 돌렸고, 바로 걸렸습니다. 노트북에서 밀리초면 끝나는 검사인데, 그 대상은 실제 브라우저의 신뢰를 떠받치는 키였습니다.

그래서 첫 번째 교훈입니다. 유효성은 필요조건이지만 충분조건과는 거리가 멉니다. PKI 테스트가 "인증서가 파싱되고 체인이 연결된다"에서 멈춘다면, 여러분은 봉투만 테스트하고 편지 내용은 무시하고 있는 겁니다.

오해 2: "기준 요구사항과 감사가 키 생성 버그를 잡아낸다"

CA는 CA/Browser Forum 기준 요구사항(Baseline Requirements) 아래에서 운영되고, WebTrust나 ETSI 기준으로 감사를 받습니다. 의식(ceremony) 문서, HSM 요구사항, 키 길이 요구사항이 다 있습니다. eMudhra의 키는 종이 위에서는 맞는 길이였습니다. 384비트였죠. 감사의 맹점은 컴플라이언스가 키의 표현 형식은 검증하지만, 그 키를 만들어 낸 과정의 엔트로피는 검증하지 않는다는 점입니다. 47바이트의 0으로 채워진 키도 디스크에서는 여전히 48바이트를 차지합니다.

eMudhra가 Mozilla 인시던트 티켓에 올린 자체 설명에 따르면, 근본 원인은 블랙 코미디 같습니다. 의식 스크립트의 버그 때문에 HSM에 약한 키 쌍이 하나 더 생성되었고, 두 키 쌍의 이름이 비슷했으며, 사람이 CSR에 쓸 잘못된 쪽을 골랐습니다. 스크립트 버그, 이름 충돌, 선택 실수 — 세 가지 독립적인 실패였고, 그 어느 단계에서도 어떤 통제도 "이 키가 우리가 HSM 비용을 치르고 얻은 엔트로피를 실제로 갖고 있는가?"라고 묻지 않았습니다.

이 부분은 자체 내부 PKI를 운영하는 모든 엔지니어를 걱정시켜야 합니다. 공개적으로 신뢰받는 CA도, 루트 프로그램의 압박과 감사 일정에 쫓기는 곳인데도 4와 같은 개인 키를 배포할 수 있었다면, 여러분의 Vault PKI 백엔드나 Active Directory Certificate Services 템플릿 안에는 무엇이 들어 있을까요? 해결책은 비용이 적게 들고, 실행하지 않을 변명이 없습니다. 키를 생성한 뒤, 그것으로 무엇에든 서명하기 전에 paranoid_crypto나 최소한 간단한 건전성 검사를 돌리세요:

from cryptography.hazmat.primitives.asymmetric import ec
CURVE_ORDER_BITS = 384
def sanity_check_private_key(key: ec.EllipticCurvePrivateKey) -> None:
d = key.private_numbers().private_value
# Reject keys whose effective entropy is far below the curve size.
# A healthy P-384 key should have a bit length close to 384.
if d.bit_length() < CURVE_ORDER_BITS - 32:
raise ValueError(
f"Suspiciously small private scalar: {d.bit_length()} bits"
)
# Also reject known bad values outright.
if d < 2**16:
raise ValueError("Private scalar is trivially brute-forceable")

30초면 끝나는 작업입니다. eMudhra의 키 8개를 모두 잡아냈을 겁니다. 감사는 프로세스가 존재하는지를 검증할 뿐, 4월 어느 화요일에 그 프로세스가 실제로 제대로 작동했는지는 검증하지 못합니다. 그건 출력 테스트만이 할 수 있습니다.

오해 3: "손상된 CA 키는 봉쇄할 수 있다"

보통의 의미에서 CA 키가 "유출"되었다면 도둑이 있습니다. 누군가 특정 시점에 키를 빼내 간 것이고, 피해 범위를 추론할 수 있습니다. 도난 이후에 발급된 인증서, 훔친 키를 가진 자가 발급한 인증서. 끔찍하지만 경계는 있습니다. 저희는 전에 중간 CA 침해가 신뢰 체인을 끊어 놓을 때 무슨 일이 일어나는지에 대해 글을 쓴 적이 있는데, 표준 대응 절차인 폐기, 신뢰 해제, 재발급은 전과 후가 있다는 전제에 기대고 있습니다.

이번 사건에는 전과 후가 없습니다. 인증서가 만들어지는 순간부터 키는 사실상 공개되어 있었고, 인증서를 본 누구든 밀리초 만에 키를 복원할 수 있었습니다. 경계를 정할 도난 사건 자체가 없습니다. 영향 기간은 인증서의 전체 수명, 즉 2025년 4월부터 이후 전부이며, 열린 문을 누가 통과했는지 — 누군가 통과했다면 말이죠 — 알 방법이 없습니다. eMudhra가 중간 CA가 말단 엔티티 인증서를 발급하지 않았다고 밝혔고 이는 Certificate Transparency 로그로도 뒷받침된다는 점은, 그 CA들이 한 일에 대해서는 정말 안심이 됩니다. 하지만 d=4를 가진 누군가가 네트워크 경로상에서 기회를 노려 무엇을 할 수 있었는지에 대해서는 아무 말도 해주지 않습니다.

더 나쁜 점은 대응 자체가 완전히 착지하지 못한다는 겁니다. Chrome과 Firefox는 CRLSet과 OneCRL을 통해 몇 시간 안에 중간 CA를 차단했고, 둘 다 CT 로깅을 요구하므로 위조된 TLS 인증서는 어차피 그 브라우저들에서 검증되지 않았을 겁니다. 다행이죠. 하지만 그 보호는 브라우저의 경계에서 끝나고, 이것이 다음 오해로 이어집니다.

오해 4: "브라우저가 안전하면 생태계도 안전하다"

어두운 가운데 수백 대의 노출된 연결 기기로 둘러싸인, 빛나는 장갑 브라우저 창
브라우저는 몇 시간 만에 패치되었지만, TLS 생태계의 나머지는 여전히 노출되어 있습니다.

브라우저는 TLS 생태계에서 최선의 시나리오이며, 운영 측면에서 여러분에게 중요한 TLS 클라이언트 중에서는 소수입니다. 여러분의 인프라에서 실제로 HTTPS 연결을 만드는 것들을 생각해 보세요: curl, 파이썬 requests, Node, Go 바이너리, Java 서비스, git, 패키지 매니저, 클라우드 SDK. 이 클라이언트들은 나쁘게 겹치는 세 가지 특징을 공유합니다. 디스크에 있는 CA 번들을 신뢰하고, 기본적으로 폐기 여부를 확인하지 않으며, Certificate Transparency를 확인하지 않습니다. Chrome 사용자를 구해준 세 가지 보호 장치가 모두 빠져 있는 겁니다.

Scott Helme가 이를 구체적으로 시연했습니다. 약한 eMudhra 중간 CA 중 하나에 체인된 api.stripe.com용 위조 인증서로, 기본 설정의 curl은 SSL certificate verify ok를 보고했고, 파이썬은 핸드셰이크를 완료했으며, Node는 연결이 authorized라고 보고했고, Windows CryptoAPI는 체인을 받아들였습니다. 테스트한 모든 Linux와 macOS 기기에서도 그랬습니다. 어떤 호스트명이든 위조 인증서가, 배포 스크립트와 CI 러너가 기반으로 하는 기본 도구에서 받아들여진다는 뜻입니다. 이건 브라우저 문제가 아니라 여러분의 인프라 문제입니다.

노출 범위는 대부분의 사람이 떠올리는 것보다 더 나쁩니다:

  • curl | sh 설치 스크립트와 자체 업데이터. rustup, nvm, Homebrew 설치 스크립트, 수많은 "install.sh" 페이지. 여기서 위조 인증서는 공격자 코드를 실행하는 것을 뜻하며, 대개 root 권한으로 실행됩니다.
  • CI/CD 파이프라인. 러너는 클라우드 자격 증명과 서명 키를 쥔 채 의존성을 가져오고 아티팩트를 푸시하며, 종종 오래된 기본 이미지를 씁니다.
  • 서버 간 트래픽. 결제 웹훅, OAuth 토큰 교환, 헤더에 비밀 정보를 담은 클라우드 SDK 호출.
  • IoT 및 임베디드 기기. 펌웨어가 고정되어 있고 CA 번들은 인증서가 만료되는 2039년 7월까지 바뀌지 않습니다.
  • Windows 코드 서명. 약한 키 8개 중 2개는 코드 서명 CA의 것이었고, 그 키로 서명된 악성코드는 정당한 배포자의 소프트웨어처럼 보입니다.

실행 가능한 조언은 지루하지만 옳습니다. CA 번들을 포함하는 모든 것을 업데이트하세요. OS의 ca-certificates 패키지, certifi, Node, Homebrew 등. 기본 이미지가 신뢰 해제를 반영하면 컨테이너 이미지도 다시 빌드하세요. 레이어는 기억하니까요. 실행할 수 없는 부분은 업데이트할 수 없는 기기입니다. 그 스마트 TV는 앞으로 십오 년 반 동안 4와 같은 키를 계속 신뢰할 겁니다. IoT 장비를 네트워크 세그먼트로 분리해야 할 이유가 하나 더 필요했다면, 이게 바로 그 이유입니다.

오해 5: "누군가 이미 확인하고 있겠지"

사건에서 가장 조용히 뼈아픈 문장은 보고자의 말에서 나왔습니다. 발견은 구독자 인증서로 넘어가기 전에 CCADB 코퍼스에 paranoid_crypto를 빠른 코퍼스 테스트로 돌린 데서 나왔습니다. 가장 쉬운 열매였죠. 보고자 본인의 말입니다. "제 시간의 대부분은 모두가 다른 누군가가 이미 해놨으리라 가정했던 기본 사항들을 다시 확인하는 데 쓰였습니다. 보안 근처에서는 절대 저지르면 안 되는 치명적인 실수입니다."

다시 한번 읽어 보세요. 공개적으로 신뢰받는 인증기관의 핵심 자산인 CA 개인 키 8개, 즉 Sectigo의 AAA Certificate Services 루트에 교차 서명된 키들이 17개월 동안 무차별 대입이 가능한 상태로 있었습니다. 감사인도, 루트 프로그램도, CA 자체도 아닌, 기본 설정 그대로 오픈소스 도구를 돌린 한 사람이 그것을 찾아냈습니다. 생태계 전체가 기본 사항은 누군가 챙겼으리라 가정했습니다. 아무도 기본 사항을 챙기지 않았습니다.

이는 보안 인프라 전반에 걸친 더 큰 패턴이며, 제가 계속 재난을 견디는 시스템, 즉 올바른 동작을 가정하지 않는 방어적 엔지니어링을 강조하는 이유이기도 합니다. 이것을 잡아낼 수 있었던 검사들은 민망할 만큼 저렴합니다:

  1. 키를 생성할 때와 서명 전 모든 CSR에 대해 paranoid_crypto나 동등한 약한 키 검사를 실행하세요.
  2. 엔트로피를 경험적으로 검증하세요. 개인 스칼라의 비트 길이를 확인하고, 실효 엔트로피가 말이 안 되게 낮은 것은 위 코드 예시처럼 거부하세요.
  3. 설정만이 아니라 의식(ceremony)도 테스트하세요. 키 의식을 드라이런하고, CSR에 어떤 이름의 HSM 객체가 들어가는지를 포함해 실제로 생성된 산출물을 점검하세요.
  4. 템플릿뿐 아니라 발급된 인증서도 지속적으로 스캔하세요. 인증서가 진실의 기준이고, 그 상류의 모든 것은 의도일 뿐입니다.

인증서가 진실의 기준입니다. 스크립트, 의식, 감사 등 그 상류의 모든 것은 그저 의도일 뿐입니다.

8비트면 충분합니다

우스꽝스러운 원인이 빚어낸 참사에는 어딘가 위안이 되는 면이 있습니다. 스크립트 버그, HSM에 있던 이름이 비슷한 키 두 개, 잘못 고른 사람 한 명. 국가급 공격자도, 새로운 암호 분석도 없었습니다. 17개월짜리 도화선이 달린 한 편의 슬랩스틱 코미디일 뿐이죠. 하지만 그 어처구니없음이 곧 경고입니다. 공개적으로 신뢰받는 CA의 가장 약한 고리가 단 1바이트일 수 있다면, 여러분 PKI의 강도는 곡선 선택이나 HSM 예산으로 정해지지 않습니다. 파이프라인 어딘가에서 누군가 실제로 출력을 테스트했는지 여부로 정해집니다.

브라우저 벤더들은 몇 시간 안에 제 몫을 했습니다. 나머지는 우리 몫입니다. 자신의 키 생성을 감사하고, 저렴한 검사를 돌리고, 닿는 모든 CA 번들을 업데이트하고, 손이 닿지 않는 기기는 분리하세요. 그리고 누군가 이미 기본 사항을 확인했을 거라는 가정은 그만두세요. 보안 근처에서는 그게 세상에서 가장 비싼 가정입니다.