키 교환 프로토콜: Diffie-Hellman과 ECDH의 동작 원리
Diffie-Hellman 키 교환의 수학적 원리, ECDH로의 발전, TLS 1.3 핸드셰이크에서의 ECDHE 활용, 완전 순방향 비밀성(PFS) 개념을 다룹니다.
지난 글에서 RSA와 ECC의 공개키 암호화 원리를 살펴봤다. 두 당사자가 처음 만날 때 어떻게 안전하게 공유 비밀을 만들 수 있을까? 이번 글에서는 키 교환(Key Exchange) 프로토콜의 핵심인 Diffie-Hellman과 ECDH를 다룬다.
키 교환 문제
안전한 통신을 하려면 대칭 암호화(AES)의 키를 공유해야 한다. 하지만 처음 만나는 두 당사자가 안전한 채널 없이 키를 교환할 수 있을까? 1976년 Diffie와 Hellman이 이 역설적 문제를 해결하는 프로토콜을 발표했다. 비밀 정보를 공개 채널에서 교환하지 않고도 양측이 같은 비밀을 공유할 수 있다.
Diffie-Hellman (DH): 수학적 원리
DH의 핵심은 이산 로그 문제(Discrete Logarithm Problem)다. g^a mod p를 계산하기는 쉽지만, 결과값에서 a를 역산하기는 매우 어렵다.
# DH 키 교환 Python 구현 (개념 이해용)
# 공개 파라미터 (미리 합의, 누구나 알아도 됨)
p = 23 # 실제로는 2048 bit 이상 소수
g = 5 # 생성원
# Alice의 비밀값
a = 6 # 절대 공개 안 함
A = pow(g, a, p) # A = g^a mod p = 5^6 mod 23 = 8 (공개)
# Bob의 비밀값
b = 15 # 절대 공개 안 함
B = pow(g, b, p) # B = g^b mod p = 5^15 mod 23 = 19 (공개)
# 공개 채널에서 A와 B 교환 (도청자가 봐도 무방)
# Alice가 계산하는 공유 비밀
S_alice = pow(B, a, p) # B^a mod p = 19^6 mod 23 = 2
# Bob이 계산하는 공유 비밀
S_bob = pow(A, b, p) # A^b mod p = 8^15 mod 23 = 2
# S_alice == S_bob == 2
# 수학적 원리: B^a = (g^b)^a = g^(ab) = (g^a)^b = A^b (mod p)
print(S_alice == S_bob) # True
도청자 Eve는 g, p, A, B를 알더라도 이산 로그 문제를 풀어야 a나 b를 구할 수 있다. 2048 bit 소수를 사용하면 현대 컴퓨터로는 불가능한 계산량이다.
DH 취약점: MITM 공격
DH는 인증(Authentication)을 제공하지 않는다. 중간자(Mallory)가 Alice와 Bob 사이에서 각각 별도의 DH 교환을 수행하면 양쪽을 속일 수 있다.
Alice ←→ Mallory ←→ Bob
(DH1) (DH2)
이 문제를 해결하려면 인증이 필요하다 — 서버 인증서(TLS) 또는 사전 공유 키(PSK)가 그 역할을 한다.
ECDH: 타원곡선 적용
ECDH(Elliptic Curve Diffie-Hellman)는 DH의 이산 로그 문제를 타원곡선 위의 점 연산으로 대체한다. 훨씬 짧은 키로 동등한 보안 강도를 제공한다.
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey
# Alice ECDH 키 쌍 생성
alice_private = X25519PrivateKey.generate()
alice_public = alice_private.public_key()
# Bob ECDH 키 쌍 생성
bob_private = X25519PrivateKey.generate()
bob_public = bob_private.public_key()
# 공개키 교환 (네트워크 전송)
alice_public_bytes = alice_public.public_bytes_raw()
bob_public_bytes = bob_public.public_bytes_raw()
# 각자 공유 비밀 계산
shared_secret_alice = alice_private.exchange(
X25519PrivateKey.from_private_bytes(b'').public_key().__class__.from_public_bytes(bob_public_bytes)
)
# 실제 코드:
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PublicKey
shared_alice = alice_private.exchange(X25519PublicKey.from_public_bytes(bob_public_bytes))
shared_bob = bob_private.exchange(X25519PublicKey.from_public_bytes(alice_public_bytes))
assert shared_alice == shared_bob # 동일한 공유 비밀
공유 비밀을 세션 키로 변환
ECDH로 얻은 공유 비밀을 그대로 암호화 키로 사용하면 안 된다. 균일한 분포가 아닐 수 있고, 키 길이도 맞지 않을 수 있다. 반드시 KDF를 통해 세션 키를 유도해야 한다.
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from cryptography.hazmat.primitives import hashes
import os
# 공유 비밀에서 AES 키 유도
def derive_session_key(shared_secret: bytes, salt: bytes = None) -> bytes:
if salt is None:
salt = os.urandom(32)
hkdf = HKDF(
algorithm=hashes.SHA256(),
length=32, # AES-256 키 길이
salt=salt,
info=b'session-key-v1'
)
return hkdf.derive(shared_secret)
session_key = derive_session_key(shared_alice)
# → 이 32바이트를 AES-GCM 키로 사용
TLS 1.3의 ECDHE 키 교환
TLS 1.3은 ECDHE(Ephemeral ECDH)를 기본 키 교환 방식으로 채택했다. “Ephemeral”이 핵심이다.
# 서버 TLS 설정 확인 (nginx)
openssl s_client -connect example.com:443 -tls1_3 2>&1 | grep -E "Protocol|Cipher|Server Temp Key"
# 좋은 출력 예시:
# Protocol : TLSv1.3
# Cipher : TLS_AES_256_GCM_SHA384
# Server Temp Key: X25519, 253 bits ← 임시 EC 키 사용 = PFS 보장
완전 순방향 비밀성 (PFS: Perfect Forward Secrecy)
ECDHE의 핵심 장점이다. 매 연결마다 새로운 임시 키 쌍을 생성하기 때문에, 나중에 서버의 장기 비밀키가 노출되더라도 과거 통신의 암호를 복호화할 수 없다.
# PFS 없는 구 방식 (정적 RSA 키 교환)
Client → [RSA로 암호화된 세션 키] → Server
# 서버 비밀키 노출 시: 저장된 모든 과거 트래픽 복호화 가능
# PFS 있는 ECDHE
Client ← [임시 EC 공개키] → Server (매 연결마다 새로 생성)
# 비밀키 노출돼도 이미 지워진 임시 키는 복원 불가 → 과거 세션 안전
# nginx ECDHE 설정
# /etc/nginx/nginx.conf
"""
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_ecdh_curve X25519:P-256:P-384;
ssl_prefer_server_ciphers off; # TLS 1.3에서는 off 권장
"""
실무 적용 체크리스트
# 1. TLS 설정에서 ECDHE 강제
# ❌ 금지: RSA 키 교환 (PFS 없음)
ssl_ciphers RSA-AES256-GCM-SHA384; # PFS 없음
# ✅ 권장
ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
# 2. 취약한 DH 파라미터 교체
# Logjam 공격 방어: 1024 bit DH 파라미터 사용 금지
openssl dhparam -out /etc/nginx/dhparam.pem 4096
# 3. PFS 상태 확인
sslyze --tlsv1_3 example.com
DH/ECDH 키 교환을 이해하면 TLS가 왜 안전한지, 왜 서버 비밀키가 노출돼도 과거 통신은 안전한지를 명확하게 이해할 수 있다. 다음 글에서는 데이터 무결성을 보장하는 HMAC을 살펴본다.
지난 글: RSA vs ECC: 공개키 암호화 알고리즘 완전 비교
다음 글: HMAC: 해시 기반 메시지 인증 코드
읽어주셔서 감사합니다. 😊