Lemma
수학, 거꾸로
도입 · 누설 없는 증명

비밀키를 보여주지 않고, 이 코인이 네 것이라는 걸 어떻게 증명할까?

주소는 공개돼 있다 — 누구나 볼 수 있다. 그 코인을 쓰게 해주는 는 비밀이어야 한다. 한 번이라도 새어 나가면 가진 코인 전부를 영영 잃는다. 그런데도 모든 거래는 생면부지의 수천 명이 검증한다. 모든 결제를 승인하는 그 비밀을 알아채지 않으면서, 이번 결제를 정말 네가 서명했다는 걸 네트워크는 어떻게 확인할까?

열쇠는 곡선 위에 있다. 연산은 둘이다. 하나는 한쪽 방향으로는 쉽고 반대로는 불가능한 함수, 다른 하나는 비밀을 쥔 자만 만들 수 있지만 누구나 확인할 수 있는 대수 항등식. 아래 위젯은 이 춤을 손으로 검산할 만큼 작은 수로 처음부터 끝까지 돌려본다.

위젯 — 서명 & 검증
곡선 · y² = x³ + 7 (mod 17)생성점 · G = (1, 5)차수 · n = 9
00448812121616GQ=5GR=7GV
서명자 · d를 알고 있음
Q = 5G(12, 16)
R = 7G(2, 7)
r = R.x mod 92
s = (h + r·d)·k⁻¹ mod 97
서명 (r, s) = (2, 7)
검증자 · h, (r, s), Q만 앎
u₁ = h·s⁻¹ mod 93
u₂ = r·s⁻¹ mod 98
u₁G + u₂Q = V(2, 7)
V.x mod 9 = 2·r = 2✓ valid
d를 끌면 Q만 움직이고 G는 그대로다. 누구나 d 5G를 계산할 수 있지만, 모든 배수를 다 시도하지 않고는 Q = (12, 16)에서 d를 복구할 수 없다. 서명이 서 있는 그 일방통행. hk를 끌어보면 양쪽이 맞아 떨어진다: 검증자는 d를 묻지 않고도 V = R에 도달하고, 따라서 V.x ≡ r이 된다.
흐름
1

무엇이 참이어야 하는가

세 가지가 동시에 참이어야 한다. (a) 주어진 메시지에 대한 유효 서명은 오직 너만 만들 수 있다, (b) 누구나 그 서명을 확인할 수 있다, (c) 확인하는 과정에서 그 서명을 만들어낸 비밀이 드러나지 않는다. 어떤 비밀번호 방식도 이 셋을 다 만족시키지 못한다 — 비밀번호는 보여줘야만 확인되니까. 필요한 건 일방통행 함수다. 만드는 쪽으로는 쉽고, 거꾸로 되돌리는 쪽으로는 불가능하며, 그 둘을 깔끔하게 묶어주는 대수 항등식이 따라붙는.

2

일방통행 함수 — 곡선 위의 스칼라 곱셈

유한체 위의 y2=x3+7y² = x³ + 7을 잡자. 이 곡선의 점들은 하나의 군을 이룬다. 임의의 두 점에 합이 정의되고, 그 합은 베주 정리를 떠받치던 바로 그 현·접선 작도로 구한다. 생성점 GG를 하나 잡고 덧셈을 반복하면 이 된다: kG=G+G++GkG = G + G + ⋯ + G (k번).

(k,G)(k, G)에서 kGkG를 구하는 데는 약 log2k\log₂ k번의 덧셈이면 충분하다 (배수-덧셈). 반대로 (G,kG)(G, kG)에서 kk를 되찾는 건 문제다 — 같은 암호용 곡선에서는 n\sqrt{n} (n2256n ≈ 2²⁵⁶) 정도보다 빠른 알고리즘이 아직 없다. 모든 체계가 이 비대칭 위에 서 있다.

# 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)
3

키 — 누구나 만들 수 있지만 아무도 되돌릴 수 없는 한 걸음

임의 정수 d1,,n1d ∈ {1, …, n−1}을 하나 골라 비밀로 간직한다 — 이것이 다. Q=dGQ = dG를 구해 공개한다 — 이것이 다. dd에서 QQ를 구하는 건 누구든 밀리초 안에 끝내지만, QQ에서 dd를 되찾는 건 이 우주의 누구도 해낼 수 없다.

위젯의 개인키 d 슬라이더를 움직이면 G는 그대로 있는데 Q가 곡선 위를 뛰어다닌다. 1,,8{1, …, 8}dd는 저마다 부분군의 다른 점에 가서 박힌다. 퍼즐 하나: 그래프 위 Q의 위치만 보고, 여덟 개를 다 대입하지 않은 채 dd를 맞힐 수 있을까? 장난감 곡선에서는 가능하지만, secp256k1에서는 불가능하다.

4

서명 — 메시지에 묶고, 키는 숨긴다

메시지 해시 hh에 대한 은 두 수 (r,s)(r, s)다. 매번 새로운 임의 kk를 뽑아 R=kGR = kG를 구하고, r=R.xmodnr = R.x \mod n, s=(h+rd)k1modns = (h + r·d) · k⁻¹ \mod n으로 둔다. k1k⁻¹dd를 가려준다 — kk가 임의이고 비밀이면 ss도 임의처럼 보인다. nonce가 없다면 같은 키로 만든 두 서명은 dd에 대한 일차 연립방정식이 되어 누구나 풀어버린다.

위젯의 서명자 패널은 식을 실시간으로 보여준다. 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)
5

검증 — 키를 알았을 때만 성립하는 항등식

검증자는 hh, (r,s)(r, s), QQ만 건네받는다. 다른 건 없다. u1=hs1modnu₁ = h · s⁻¹ \mod n, u2=rs1modnu₂ = r · s⁻¹ \mod n, V=u1G+u2QV = u₁G + u₂Q를 차례로 구하고, V.xr(modn)V.x ≡ r (\mod n)이면 서명을 받아들인다. 대수를 한 줄씩 따라가 보자:

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 ✓

s=(h+rd)/ks = (h + r·d)/k이라는 한 줄이 (r,s,h,Q)(r, s, h, Q)만 보는 사람으로부터 dd를 가린다. 그러면서 같은 줄을 V=u1G+u2QV = u₁G + u₂Q에 대입하면 VV정확히 RR로 떨어진다. 검증자는 dd를 되찾지 않고도 서명자가 닿았던 바로 그 점에 닿는다. dd를 알아야만 만들 수 있고, dd를 모르고도 확인할 수 있는 이 일치가 — 이 체계의 핵심이다.

# 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.
6

secp256k1 — 같은 알고리즘, 더 큰 수

비트코인은 — 소수 p=2256232977p = 2²⁵⁶ − 2³² − 977 위의 y2=x3+7y² = x³ + 7 — 위에서 똑같은 군 법칙과 똑같은 ECDSA 식을 돌린다. 위의 위젯은 그 축소판이다. 22562²⁵⁶ 대신 17,  2256~2²⁵⁶ 대신 부분군 차수 9. 알고리즘은 그대로다. 규모만 달라지는 게 하나 있으니, 곱셈 (여전히 O(logk)O(\log k)) 과 되돌리기 (이제  2128~2¹²⁸) 사이의 격차다. 지갑이 안전한 유일한 이유가 바로 이 격차다.

이제 깨봐

수학은 단단하다. 무너지는 건 늘 구현이다. ECDSA는 서명마다 매번 새롭고 고른 무작위 nonce kk를 요구한다. 서로 다른 두 메시지에 같은 kk를 쓰면, 공격자는 중학교 수준의 대수만으로 dd를 뽑아낸다: d=(s1h2s2h1)/(r(s2s1))d = (s₁·h₂ − s₂·h₁) / (r·(s₂ − s₁)) — 한 줄짜리 지갑 털이다. 소니 플레이스테이션 3 펌웨어(2010)가 kk를 재사용한 탓에 마스터 서명 키가 며칠 만에 뜯겨 나갔다. 비트코인 지갑도 RNG가 살짝 어긋난 똑같은 방식으로 코인을 잃었다. 이산로그 격차는 수학으로는 깰 수 없지만, nonce는 코드 한 줄로 깰 수 있다.

d로 서명하고, d 없이 검증한다. 서명이란, 키를 알아야만 만들 수 있지만 누구든 곁에서 들여다보며 맞는지 확인할 수 있는 수다. 같은 키가 결제를 승인하면서, 자신은 끝내 모습을 드러내지 않는다.

exercises · 손으로 풀기
1손으로 스칼라 곱셈계산기 없이

y2=x3+7(mod17)y² = x³ + 7 (\mod 17), G=(1,5)G = (1, 5)에서 현·접선 군 법칙으로 3G를 계산하라. 먼저 2배로 2G, 그 다음 G를 더한다. 답을 위젯과 맞춰보자.

2손으로 서명

d = 3, h = 4, k = 2일 때 ECDSA 서명 (r,s)(r, s)를 만들어라. 문제 1의 2G=(2,10)2G = (2, 10)을 써도 된다. 위젯의 d, h, k를 이 값으로 두고 확인.

3손으로 검증

h = 4, 서명 (r, s) = (2, 5), 공개키 Q=3G=(5,9)Q = 3G = (5, 9)를 받았다. 검증자 식을 돌려라: u₁, u₂를 구하고, V = u₁G + u₂Q를 계산해 V.x mod 9 = r 인지 확인.

4위조자의 좌절

위조자는 d를 모른 채로, 어떤 메시지 해시 hh와 표적 공개키 Q에 대해 서명 (r,s)(r', s')을 지어내려 한다. 증명 말고 구조 차원에서 — 거의 모든 시도가 왜 검증을 통과하지 못하는지 설명하고, 위조에 성공하려면 무엇을 알아야 하는지 답하라.

왜 교과서는 이렇게 안 가르치나

암호 교과서는 대개 ECDSA를 검증 식 — u1G+u2Q=Ru₁G + u₂Q = R — 부터 펼쳐 놓고 받아들이라 말한다. Lemma는 거꾸로 들어간다. 검증자는 dd를 알지도 못하면서 하필 RR에 닿는가? 이 물음을 따라가면 일방통행이라는 사실(곱셈은 쉽고 나눗셈은 어렵다) 에서 출발해 대수가 처음부터 다시 쌓인다. 교과서는 이 비대칭을 감추지만, 정작 핵심은 그 비대칭이다.

용어집 · 이 페이지에서 쓰임 · 9
Bitcoin (BTC)·비트코인
2009년 익명의 '사토시 나카모토'가 만든 디지털 화폐. 신규 발행량은 약 4년마다 절반으로 줄어드는 고정 스케줄을 따른다.
private key·개인키
소유자만 알고 있어야 하는 비밀 정수. 생성점 `G`와 부분군 차수 `n`을 가진 곡선의 ECDSA에서, 개인키 `d`는 `{1, …, n-1}`의 임의 원소. 소유자는 `d`로 서명을 만들고, 다른 모두는 그 대응 공개키 `Q = dG`로 서명을 검증한다. `d`를 알게 된 자는 소유자 행세로 영원히 서명할 수 있다 — 그게 `d`가 디바이스 밖으로 나가면 안 되는 _유일한_ 이유.
elliptic curve·타원곡선
`y² = x³ + ax + b` (단, `4a³ + 27b² ≠ 0`이라 첨점이나 자기교차가 없는) 형태의 곡선. 유한체 `F_p` 위에서 보면, 아핀 해 모음에 단 하나의 *무한원점*을 더한 집합이 베주 정리에서 물려받은 현·접선 작도로 유한 아벨 군을 이룬다. ECDSA, EdDSA를 비롯한 현대 공개키 암호 대부분이 이 군 위에 서 있다. 비트코인은 소수 `p = 2²⁵⁶ − 2³² − 977` 위의 특수 곡선 `y² = x³ + 7`을 쓰며, 이 곡선의 이름이 secp256k1.
scalar multiplication·스칼라 곱셈
타원곡선 위의 점 덧셈 반복: `kP = P + P + ⋯ + P` (k번). `(k, P)`에서 `kP`를 계산하는 건 빠르다 — 배수-덧셈(double-and-add)으로 `O(log k)` 군 연산이면 끝. `(P, kP)`에서 `k`를 복구하는 건 이산 로그 문제로, 군 연산이 지수적으로 많이 필요할 거라 믿어진다. 이 비대칭성 — 곱셈은 쉽고 역이 어렵다 — 이 모든 타원곡선 암호 (키, 서명, 키 교환)의 *전부*다.
discrete logarithm·이산 로그
문제: 차수가 알려진 군 원소 `g`와 그 순환부분군 `⟨g⟩` 안의 원소 `h`가 주어졌을 때, `g^x = h`를 만족하는 정수 `x`를 찾아라 (곡선의 덧셈 표기로는 `xG = Q`). secp256k1처럼 잘 고른 타원곡선에서 알려진 가장 빠른 알고리즘조차 군 연산을 대략 `√n`번 필요로 한다 (`n`은 부분군 차수). `n ≈ 2²⁵⁶`이면 약 `2¹²⁸` 연산 — 현재나 가까운 미래의 어떤 컴퓨터로도 닿을 수 없는 규모.
secp256k1·secp256k1
비트코인이 키와 서명에 쓰는 특수 타원곡선: `p = 2²⁵⁶ − 2³² − 977` 위의 `y² = x³ + 7`. 생성점 `G`와 부분군 차수 `n`은 표준에 고정된 상수. `secg.org` (Standards for Efficient Cryptography Group)이 정의한 규격 이름이 `secp256k1`. 토이 타원곡선과 _완전히 같은_ 군 법칙, _같은_ ECDSA 식 — 숫자만 더 클 뿐. 256-bit 소수 덕에 `√n ≈ 2¹²⁸` 만큼의 이산 로그 작업이 불가능에 가깝다.
public key·공개키
곡선 위의 점 `Q = dG`. `d`는 개인키, `G`는 생성점. `d`에서 `Q`를 계산하는 건 점 덧셈 반복으로 빠르지만, `Q`에서 `d`를 복구하는 건 이산 로그 문제로 큰 규모에선 불가능하다고 믿어진다. 소유자는 `Q`를 자유롭게 공개한다 — `d`를 노출하지 않고 네트워크 상의 신원이 된다. 비트코인 주소는 공개키를 해시해서 만든다.
digital signature·디지털 서명
개인키 `d`의 소유자가 메시지 해시 `h`에 대해 만들어내는 짧은 값 `(r, s)`. 대응 공개키 `Q = dG`를 가진 누구나 `d`를 모르고도 서명을 검증할 수 있다. ECDSA에서는 서명자가 임의의 nonce `k`를 골라 `R = kG`를 계산하고, `r = R.x mod n`, `s = (h + r·d) · k⁻¹ mod n`. 검증자는 `u₁ = h/s`, `u₂ = r/s`를 계산해 `(u₁G + u₂Q).x ≡ r (mod n)`이 맞는지 확인 — 이 등식은 서명자가 `d`를 알고 있었을 때만 성립한다.
nonce·nonce
"한 번만 쓰는 수" (number used once). ECDSA에서는 매 서명마다 `{1, …, n-1}`에서 새로 뽑은 임의 정수 `k`가 필요하다. `R = kG`를 계산한 뒤 버려진다. nonce는 `s = (h + r·d)·k⁻¹`에서 개인키를 가리는 역할 — `k`가 임의이면 `s`도 임의처럼 보인다. 두 가지 실패가 치명적. (1) 같은 키로 서명한 두 서명이 같은 `k`를 쓰면, 누구나 작은 일차연립을 풀어 `d`를 복구할 수 있다. (2) `k`가 예측 가능하면 (약한 RNG 등) 같은 복구가 통한다. 현실 지갑은 이제 `k`를 메시지와 `d`에서 *결정론적*으로 유도해 사고로 새지 않게 만든다.