비밀키를 보여주지 않고, 이 코인이 네 것이라는 걸 어떻게 증명할까?
열쇠는 곡선 위에 있다. 연산은 둘이다. 하나는 한쪽 방향으로는 쉽고 반대로는 불가능한 함수, 다른 하나는 비밀을 쥔 자만 만들 수 있지만 누구나 확인할 수 있는 대수 항등식. 아래 위젯은 이 춤을 손으로 검산할 만큼 작은 수로 처음부터 끝까지 돌려본다.
무엇이 참이어야 하는가
세 가지가 동시에 참이어야 한다. (a) 주어진 메시지에 대한 유효 서명은 오직 너만 만들 수 있다, (b) 누구나 그 서명을 확인할 수 있다, (c) 확인하는 과정에서 그 서명을 만들어낸 비밀이 드러나지 않는다. 어떤 비밀번호 방식도 이 셋을 다 만족시키지 못한다 — 비밀번호는 보여줘야만 확인되니까. 필요한 건 일방통행 함수다. 만드는 쪽으로는 쉽고, 거꾸로 되돌리는 쪽으로는 불가능하며, 그 둘을 깔끔하게 묶어주는 대수 항등식이 따라붙는.
일방통행 함수 — 곡선 위의 스칼라 곱셈
유한체 위의
에서 를 구하는 데는 약 번의
덧셈이면 충분하다 (배수-덧셈). 반대로 에서 를 되찾는 건
# Toy curve E: y² = x³ + 7 over F_17.
# Same shape (a=0, b=7) as secp256k1, microscopic prime.
P, A, B = 17, 0, 7
INF = None # the point at infinity is the group identity
def add(P_, Q_):
if P_ is INF: return Q_
if Q_ is INF: return P_
x1, y1 = P_; x2, y2 = Q_
if x1 == x2 and (y1 + y2) % P == 0:
return INF
if P_ == Q_:
lam = (3 * x1 * x1 + A) * pow(2 * y1, -1, P) % P
else:
lam = (y2 - y1) * pow(x2 - x1, -1, P) % P
x3 = (lam * lam - x1 - x2) % P
y3 = (lam * (x1 - x3) - y1) % P
return (x3, y3)
def scalar_mul(k, P_):
R = INF
while k > 0:
if k & 1: R = add(R, P_)
P_ = add(P_, P_)
k >>= 1
return R
G = (1, 5) # generator
N = 9 # order of G — 9G = INF
scalar_mul(2, G) # → (2, 10)
scalar_mul(3, G) # → (5, 9)
scalar_mul(9, G) # → None (= O)키 — 누구나 만들 수 있지만 아무도 되돌릴 수 없는 한 걸음
임의 정수 을 하나 골라 비밀로 간직한다 — 이것이
위젯의 개인키 d 슬라이더를 움직이면 G는 그대로 있는데 Q가 곡선 위를 뛰어다닌다. 의 는 저마다 부분군의 다른 점에 가서 박힌다. 퍼즐 하나: 그래프 위 Q의 위치만 보고, 여덟 개를 다 대입하지 않은 채 를 맞힐 수 있을까? 장난감 곡선에서는 가능하지만, secp256k1에서는 불가능하다.
서명 — 메시지에 묶고, 키는 숨긴다
메시지 해시 에 대한
위젯의 서명자 패널은 식을 실시간으로 보여준다. d, h, k를 움직이면 서명이 그 자리에서 다시 계산된다. 검증자 · h, (r, s), Q 만 본다 라고 표시된 줄이 곧 경계다 — 서명 장치 밖으로 나가는 것과 안에 남는 것을 가르는 선.
# Sign a message hash with private key d, nonce k.
def sign(d, h, k):
R = scalar_mul(k, G)
r = R[0] % N
s = (h + r * d) * pow(k, -1, N) % N
if r == 0 or s == 0:
raise ValueError("retry with another k")
return r, s
# Toy: d = 5 (private), h = 3 (message hash), k = 7 (nonce)
sign(5, 3, 7) # → (2, 7)검증 — 키를 알았을 때만 성립하는 항등식
검증자는 , , 만 건네받는다. 다른 건 없다. , , 를 차례로 구하고, 이면 서명을 받아들인다. 대수를 한 줄씩 따라가 보자:
V = u₁·G + u₂·Q = (h/s)·G + (r/s)·dG = ((h + r·d)/s)·G ← group up by G = ((h + r·d) · k / (h + r·d))·G ← s = (h + r·d)/k = k·G = R ← so V.x = R.x = r ✓
이라는 한 줄이 만 보는 사람으로부터 를 가린다. 그러면서 같은 줄을 에 대입하면 가 정확히 로 떨어진다. 검증자는 를 되찾지 않고도 서명자가 닿았던 바로 그 점에 닿는다. 를 알아야만 만들 수 있고, 를 모르고도 확인할 수 있는 이 일치가 — 이 체계의 핵심이다.
# Verify uses only public information: h, (r, s), Q.
def verify(h, r, s, Q):
s_inv = pow(s, -1, N)
u1 = h * s_inv % N
u2 = r * s_inv % N
V = add(scalar_mul(u1, G), scalar_mul(u2, Q))
return V is not INF and V[0] % N == r
Q = scalar_mul(5, G) # public key matching d = 5
verify(3, 2, 7, Q) # → True
# The algebra: V = u1·G + u2·Q
# = (h/s)G + (r/s)·dG
# = ((h + r·d)/s)·G
# = ((h + r·d) · k / (h + r·d))·G # because s = (h+r·d)/k
# = k·G = R. So V.x mod N = r.
# Verifier never sees d. The identity holds iff signer knew d.secp256k1 — 같은 알고리즘, 더 큰 수
비트코인은
수학은 단단하다. 무너지는 건 늘 구현이다. ECDSA는 서명마다 매번 새롭고 고른 무작위 nonce 를 요구한다. 서로 다른 두 메시지에 같은 를 쓰면, 공격자는 중학교 수준의 대수만으로 를 뽑아낸다: — 한 줄짜리 지갑 털이다. 소니 플레이스테이션 3 펌웨어(2010)가 를 재사용한 탓에 마스터 서명 키가 며칠 만에 뜯겨 나갔다. 비트코인 지갑도 RNG가 살짝 어긋난 똑같은 방식으로 코인을 잃었다. 이산로그 격차는 수학으로는 깰 수 없지만, nonce는 코드 한 줄로 깰 수 있다.
d로 서명하고, d 없이 검증한다. 서명이란, 키를 알아야만 만들 수 있지만 누구든 곁에서 들여다보며 맞는지 확인할 수 있는 수다. 같은 키가 결제를 승인하면서, 자신은 끝내 모습을 드러내지 않는다.
, 에서 현·접선 군 법칙으로 3G를 계산하라. 먼저 2배로 2G, 그 다음 G를 더한다. 답을 위젯과 맞춰보자.
d = 3, h = 4, k = 2일 때 ECDSA 서명 를 만들어라. 문제 1의 을 써도 된다. 위젯의 d, h, k를 이 값으로 두고 확인.
h = 4, 서명 (r, s) = (2, 5), 공개키 를 받았다. 검증자 식을 돌려라: u₁, u₂를 구하고, V = u₁G + u₂Q를 계산해 V.x mod 9 = r 인지 확인.
위조자는 d를 모른 채로, 어떤 메시지 해시 와 표적 공개키 Q에 대해 서명 을 지어내려 한다. 증명 말고 구조 차원에서 — 거의 모든 시도가 왜 검증을 통과하지 못하는지 설명하고, 위조에 성공하려면 무엇을 알아야 하는지 답하라.
암호 교과서는 대개 ECDSA를 검증 식 — — 부터 펼쳐 놓고 받아들이라 말한다. Lemma는 거꾸로 들어간다. 검증자는 를 알지도 못하면서 왜 하필 에 닿는가? 이 물음을 따라가면 일방통행이라는 사실(곱셈은 쉽고 나눗셈은 어렵다) 에서 출발해 대수가 처음부터 다시 쌓인다. 교과서는 이 비대칭을 감추지만, 정작 핵심은 그 비대칭이다.