웹 인증서와 패스키 서명에 쓰이는 타원곡선 P-256은 1997년 8월 16일 01시 52분 34초(UTC)를 가리키던 컴퓨터 시계에서 나온 숫자로 만들어졌어요. NSA가 출처를 밝히지 않아 오랫동안 백도어를 의심받던 숫자예요. 10월 16일 Anthropic이 연구 글 「Claude reveals ECDSA curve origins」에서 이 답을 밝혔고 저도 그 계산을 직접 다시 돌려 똑같은 숫자를 얻었어요. 같은 주 Hacker News에서는 바로 그 곡선으로 서명하는 패스키가 함정인지를 두고 글이 맞붙었어요. 두 이야기를 이어서 읽었어요.
설명 없이 내려온 숫자
ECDSA는 지금 가장 널리 쓰이는 타원곡선 서명 방식이에요. 미국 NIST는 2000년 FIPS 186-2에 크기가 다른 곡선 다섯 개(P-192 · P-224 · P-256 · P-384 · P-521)를 표준으로 실었어요. 곡선을 정하는 계수 b는 ‘시드’라는 20바이트 숫자를 SHA-1로 해시해서 얻어요. 해시는 거꾸로 돌릴 수 없으니 원하는 b가 나오도록 숫자를 짜 맞추기 어렵다는 논리예요. 문제는 시드 자체였어요. NSA가 어떻게 골랐는지 설명 없이 건넨 값이었거든요. P-256의 시드는 이렇게 생겼어요.
C49D3608 86E70493 6A6678E1 139D26B7 819F7E90의심은 오래 이어졌어요. Neal Koblitz와 Alfred Menezes는 2015년 논문 「A Riddle Wrapped in an Enigma」에서 이 논란을 정리했어요. Daniel Bernstein과 Tanja Lange는 시드를 70비트쯤 마음대로 고를 수 있으면 남들은 모르는 약점이 있는 곡선을 골라낼 수 있다는 것을 보였어요. 시드를 만든 NSA 연구원 Jerry Solinas는 재미 삼아 지은 영어 문장을 해시했다고 기억했지만 그 문장을 끝내 떠올리지 못했고 2023년에 세상을 떠났어요. 그해 암호학자 Filippo Valsorda가 시드의 출처를 밝히는 사람에게 현상금 12,288달러를 걸었어요. 상금을 자선 단체에 기부하면 세 배인 36,864달러로 늘려 주는 조건이었어요.
그렇게 오래된 기록도 찾을 수 없었다. 남아 있다면 틀림없이 성궤 옆 어느 하드디스크에 잠들어 있을 것이다.
Jerry Solinas, 시드를 만든 NSA 연구원(Anthropic 글에서 재인용)
30분 만에 나온 답
2026년 10월, Anthropic의 한 직원이 사내 연구용 모델에게 이 시드가 어떻게 만들어졌는지 물었어요. ‘문장을 해시했다’는 단서는 일부러 주지 않았어요. Claude는 30분도 안 돼 NIST 소수 곡선 다섯 개의 시드 출처를 찾았어요. 문장을 해시한 적은 처음부터 없었어요. 시드는 1990년대 C 라이브러리에 들어 있던 난수 생성기 lrand48을 유닉스 시각으로 초기화한 뒤 뽑은 값이었어요.
찾아낸 과정은 이래요. Claude는 곡선을 만든 프로그램이 당시 흔하던 의사난수 생성기를 썼을 거라고 짐작했어요. 그런 생성기는 약해서 출력만 보고도 알아볼 수 있거든요. 시드를 한 비트씩 밀어 가며 오래된 생성기의 연속 출력인지 검사하다가 이진 곡선 시드 네 개에서 패턴을 찾았어요. 네 시드를 이루는 32비트 워드 20개의 최상위 비트가 전부 0이었어요. 48비트 상태 가운데 상위 31비트만 내놓는 lrand48의 흔적이에요. 숨은 17비트에 들어갈 값 2¹⁷가지를 모두 넣어 보니 다음 워드가 예측대로 이어졌어요.
소수 곡선의 시드에는 출력의 일부(lrand48() >> 23)만 담겨 있어서 격자 기저 축소(LLL)로 생성기의 상태를 되찾았어요. 이 생성기는 거꾸로도 쉽게 돌아가요. 상태를 거슬러 올라가 srand48이 처음 만든 형태(하위 16비트가 0x330E)에 닿자 ANSI X9.62의 예시 곡선까지 합친 소수 곡선 시드 열 개가 모두 1997년 5월에서 1998년 4월 사이의 유닉스 시각을 가리켰어요. 이진 곡선 넷은 1984년 2월을 가리켰는데 두 배로 하면 1998년 4월 17일이 돼요. 프로그램이 시각을 반으로 나눠 쓴 것으로 보여요.

이진 곡선 시드 넷을 32비트 워드 20개로 나눠 비트마다 칠했어요.
최상위 비트가 20개 모두 0이에요. 우연히 이럴 확률은 100만분의 1쯤이에요.

P-256의 시드에서 생성기 상태를 되찾아 거꾸로 돌렸어요.
처음 상태의 아래 16비트는 srand48이 붙이는 0x330E이고 위 32비트는 유닉스 시각 871,696,354, 곧 1997년 8월 16일 01:52:34(UTC)예요.

거슬러 오른 시각을 늘어놓으면 모두 1997년 5월에서 1998년 4월 사이예요.
P-256은 1997년 8월 16일에 만들어졌어요. 이진 곡선 넷은 1998년 4월 17일 몇 분 간격으로 만들어졌어요.
놀랍다. 25년 묵은 수수께끼를 풀었다. 음, 잠깐. 의심해 보자. 혹시 나는 시뮬레이션이나 시험 환경 같은 곳에 있는 건가?
Claude의 사고 과정, Anthropic 글에서
직접 돌려 봤어요
Anthropic이 공개한 코드를 돌려 보고 같은 계산을 짧게 다시 써 봤어요. P-256이 가리키는 시각 1997년 8월 16일 01:52:34(UTC)는 유닉스 시각으로 871,696,354예요. 한국 시간으로는 같은 날 오전 10시 52분 34초예요. 이 값으로 생성기를 초기화하고 20바이트짜리 후보를 차례로 뽑으면 1,842번째 후보가 P-256의 시드와 한 자리도 다르지 않아요.
from datetime import datetime, timezone
import hashlib
t = int(datetime(1997, 8, 16, 1, 52, 34, tzinfo=timezone.utc).timestamp()) # 871696354
x = (t << 16) | 0x330E # srand48(t)가 만드는 상태
seed = b""
for i in range(20 * 1842): # 20바이트 후보 1,842개
x = (0x5DEECE66D * x + 0xB) % 2**48 # lrand48 한 걸음
if i >= 20 * 1841: # 마지막(1,842번째) 후보만 모은다
seed += bytes([x >> 40]) # lrand48() >> 23 = 상태의 윗 8비트
print(seed.hex().upper()) # C49D360886E704936A6678E1139D26B7819F7E90
# ANSI X9.62 / FIPS 186 검증: 시드 → SHA-1 → c, 그리고 b²·c ≡ −27 (mod p)
p = 2**256 - 2**224 + 2**192 + 2**96 - 1
b = 0x5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B
z = int.from_bytes(seed, "big")
w0 = int.from_bytes(hashlib.sha1(seed).digest(), "big") % 2**95
w1 = int.from_bytes(hashlib.sha1(((z + 1) % 2**160).to_bytes(20, "big")).digest(), "big")
c = (w0 << 160) | w1
print((b * b * c + 27) % p == 0) # True시드가 같다는 것만으로는 부족해서 표준이 정한 검증도 해 봤어요. ANSI X9.62와 FIPS 186은 시드를 SHA-1로 해시해 c를 만들고 곡선 계수 b가 아래 식을 만족하는지 보라고 해요. 표준에 실린 P-256의 b를 넣으면 참이 나와요. P-192 · P-224 · P-384 · P-521도 같은 방법으로 모두 맞았어요. Filippo Valsorda가 2023년 현상금 글에 적어 둔 시드 목록과도 한 글자까지 같아요.

풀리지 않은 것도 남았어요. P-256은 1,842번째 후보였는데 같은 실행의 876번째 후보도 조건에 맞는 곡선이었어요. 왜 건너뛰었는지는 아무도 몰라요. Valsorda도 검증해 보니 쓸 수 있는 시드가 실제로 실린 것보다 두 배쯤 많았다고 적었어요. 이진 곡선 B-163의 시드는 여전히 설명되지 않고 P-224 · P-256 · P-384 · P-521의 기저점(base point)이 어디서 왔는지도 아직 몰라요.
그래서 백도어는 있었나
Anthropic은 이번 발견이 암호를 깬 것도 새로운 공격도 아니라고 밝혔어요. 오히려 곡선에 백도어가 없다는 믿음이 더 단단해졌다고 했어요. HN에서는 반론도 나왔어요. 시각이나 후보 번호를 바꿔 가며 약한 곡선이 나올 때까지 돌렸을 수도 있지 않느냐는 거예요. 현상금을 건 Valsorda는 현상금이 주인을 찾았다고 알린 글에서 이렇게 계산했어요. NSA가 1997~98년 중 아무 시각이나 정하고 처음 나온 쓸 만한 후보 16개 가운데 하나를 마음대로 골랐다고 쳐도 선택의 여지는 30비트쯤이에요. Bernstein과 Lange의 방법에 필요했던 70비트에 한참 못 미쳐요.
있을지 모를 약한 곡선 무리를 일부러 노리기에도, 자기들만 열 수 있는 백도어를 심기에도 모자라다.
Filippo Valsorda, 현상금을 건 암호학자
HN의 tptacek은 ‘참담한 소식이다… 한 세대를 이어 온 NIST 암호 음모론에는’이라고 적었어요. Valsorda는 이제 개인적으로 Curve25519 계열보다 NIST 곡선을 권한다고 했어요. 다만 누명을 벗은 NIST 곡선도 곧 양자내성 서명에 자리를 내주고 물러날 참이라고 덧붙였어요. 현상금 36,864달러는 Let’s Encrypt를 운영하는 비영리 단체 ISRG에 기부됐어요.
같은 주, 패스키를 두고 벌어진 싸움
이 곡선은 패스키 아래에도 깔려 있어요. 패스키를 정의하는 W3C WebAuthn 명세를 보면 사이트가 서명 알고리즘을 정하지 않을 때 기본값이 ES256(P-256 위의 ECDSA)과 RS256이에요. 그 패스키를 두고 10월 11일 HN에 「Passkeys are a trap. Do not use them.」이 올라와 1,146점을 받았어요. 글쓴이(The Hack)는 패스키에 ‘숨은 다이얼’이 있다고 했어요. 어떤 인증기로 만든 키인지 사이트에 알려 주는 인증기 증명(attestation)을 사이트들이 조금씩 요구하기 시작하면 승인받은 기기와 운영체제가 아니고서는 로그인할 수 없게 된다는 거예요. 대신 TOTP(인증 앱의 6자리 코드)를 쓰라고 권했어요.

사흘 뒤 보안 연구자 Scott Helme가 「Passkeys are not a trap」으로 반박했어요(HN 672점). 댓글은 814개로 원글(577개)보다 많았어요. Helme의 요지는 이래요. 사람들이 주로 쓰는 iCloud 키체인 · Google 비밀번호 관리자 · 1Password · Bitwarden의 동기화 패스키는 증명을 보내지 않아요. 사이트가 요구해도 돌아오는 값은 none이에요. 증명이 있을 때 알려 주는 것도 인증기의 제조사와 모델뿐이고 운영체제나 브라우저, 루팅 여부는 알려 주지 않아요. 그리고 TOTP 코드는 가짜 사이트에도 그대로 입력할 수 있지만 패스키는 만든 도메인에서만 작동해요.


피싱에 강한 자격 증명을 피싱당하기 쉬운 것과 맞바꾸는 건 정말 나쁜 거래다.
Scott Helme, Passkeys are not a trap
곡선은 누명을 벗었고 남은 건 규칙이에요
제가 보기에 두 이야기는 같은 질문의 앞뒤예요. P-256을 둘러싼 오랜 의심은 누구나 검증할 수 있는 계산으로 끝났어요. 패스키를 두고 벌어진 논쟁은 수학 문제가 아니라 누가 열쇠와 규칙을 쥐느냐의 문제예요. Helme도 인증기 증명이 일반 소비자 로그인까지 번진다면 크게 반대해야 한다고 적었어요. 패스키를 도입하는 개발자라면 이 정도는 챙길 만해요.
알고리즘 목록에 ES256(-7)을 넣어요. WebAuthn 명세는 여러 인증기를 받으려는 사이트라면 EdDSA(-8) · ES256(-7) · RS256(-257)을 넣으라고 권해요.
인증기 증명은 꼭 필요할 때만 요청해요. Helme는 소비자 서비스에는 증명을 요청하지 말라는 권고가 나온 지 이미 몇 년 됐다고 적었어요.
패스키는 믿을 만한 비밀번호 관리자에 저장해요. 여러 기기와 플랫폼을 오가려면 패스키를 옮길 수 있는 곳을 고르라는 것도 Helme의 권고예요.
25년 넘게 아무도 그 숫자가 어디서 왔는지 몰랐고 사람들은 숫자 뒤에 누가 있는지 의심했어요. 거기 있던 건 1997년 8월의 시계였어요. 그리고 의심을 끝낸 건 누구나 다시 돌려 볼 수 있는 계산이었어요.