본문 바로가기

암호화의 기초

HTTPS를 이루는 암호 기술을 하는 일에 따라 나누어 봅니다. 데이터를 숨기는 대칭 키 암호와 AEAD, 처음 만난 두 사람이 비밀을 만드는 키 교환, 위조를 검출하는 해시와 HMAC, 출처를 확인하는 전자 서명을 작은 수의 예와 표준 시험값으로 확인하고, TLS가 이들을 어떻게 묶는지 정리합니다.

HTTP는 메시지를 평문으로 보냅니다. 경로 위의 누구든 요청의 경로와 쿠키, 본문의 비밀번호를 읽을 수 있고, 중간에서 응답을 바꿔치기할 수도 있습니다. HTTPS는 HTTP를 TLS(transport layer security) 위에 올려 이 문제를 막습니다. 하지만 "TLS가 암호화한다"는 한마디로는 부족합니다. TLS는 내용을 숨기는 일, 바뀌지 않았음을 확인하는 일, 상대가 진짜인지 확인하는 일을 서로 다른 암호 기술로 나누어 맡깁니다. 이 글에서는 그 기술들이 각각 무엇을 지켜 주고 무엇은 지켜 주지 못하는지 살펴봅니다.

대칭 키 암호는 같은 키로 잠그고 연다

대칭 키 암호(symmetric encryption)는 암호화와 복호화에 같은 비밀 키를 씁니다. 계산이 빨라서, TLS에서 실제로 오가는 HTTP 데이터는 모두 대칭 키 암호로 보호합니다. 지금 쓰는 방식은 내용을 숨기는 동시에 바뀌지 않았는지도 검사하는 AEAD(authenticated encryption with associated data, RFC 5116)입니다.

암호화:  평문 + 비밀 키 + nonce  → 암호문 + 인증 태그
복호화:  암호문 + 인증 태그 + 같은 키 + 같은 nonce → 평문, 또는 "바뀌었음" 오류
방식키 길이지금의 평가
AES-GCM128, 256비트TLS에서 가장 널리 쓰는 AEAD. AES 명령을 갖춘 CPU에서 빠르다
ChaCha20-Poly1305256비트AES 명령이 없는 기기에서도 빠른 AEAD(RFC 8439)
AES-CBC와 HMAC의 조합128, 256비트TLS 1.2까지 쓰였고, 조합 방식의 실수로 여러 공격을 받았다
3DES, RC4, DES112비트 수준 이하안전하지 않아 쓰지 않는다

nonce는 메시지마다 한 번만 쓰는 값입니다. AES-GCM에서 같은 키로 같은 nonce를 두 번 쓰면 두 평문의 관계가 드러나고 인증 태그까지 위조될 수 있으므로, TLS는 레코드마다 순서 번호로 nonce를 만들어 겹치지 않게 합니다.

대칭 키 암호의 근본 문제는 키를 어떻게 나누어 가지느냐입니다. 처음 접속한 웹 서버와 미리 키를 정해 두었을 리 없고, 키를 네트워크로 그냥 보내면 엿듣는 사람도 그 키를 얻습니다.

키 교환은 엿듣는 사람 앞에서 비밀을 만든다

디피-헬만(Diffie-Hellman) 키 교환은 엿듣는 사람이 모든 메시지를 보아도 알 수 없는 공유 비밀을 두 사람이 함께 만드는 방법입니다. 모두에게 공개된 수 p와 g를 정해 두고, 각자 자기만 아는 비밀 a와 b를 고른 뒤, g^a mod p와 g^b mod p만 주고받습니다. 다음 코드는 아주 작은 수로 이를 따라갑니다.

p, g = 23, 5                            # 모두에게 공개된 값(실제로는 아주 큰 수를 쓴다)
a, b = 4, 3                             # 각자만 아는 임시 비밀
A, B = pow(g, a, p), pow(g, b, p)       # 서로에게 보내는 공개 값
print("주고받는 값:", A, B)
print("각자 계산한 공유 비밀:", pow(B, a, p), pow(A, b, p))
주고받는 값: 4 10
각자 계산한 공유 비밀: 18 18

5⁴ mod 23 = 4와 5³ mod 23 = 10을 주고받은 뒤, 한쪽은 10⁴ mod 23을, 다른 쪽은 4³ mod 23을 계산해 둘 다 18을 얻었습니다. 엿듣는 사람은 23, 5, 4, 10을 모두 보지만, 18을 얻으려면 4와 10에서 비밀 a나 b를 거꾸로 찾아야 합니다. 이 예처럼 수가 작으면 하나씩 대입해 금방 찾지만, 실제로는 수백 자리의 수나 타원 곡선(X25519, RFC 7748)을 써서 사실상 찾을 수 없게 합니다. TLS가 쓰는 ECDHE는 이 타원 곡선 방식의 키 교환을 연결마다 새 비밀로 하는 것입니다.

연결마다 비밀을 새로 만들고 다 쓰면 버리기 때문에, 나중에 서버의 장기 키가 유출되더라도 예전에 녹화해 둔 대화를 풀 수 없습니다. 이 성질을 전방 비밀성(forward secrecy)이라고 합니다. 예전의 TLS는 클라이언트가 만든 키를 서버의 RSA 공개 키로 암호화해 보내는 방식도 썼는데, 이 방식은 서버의 개인 키가 유출되면 녹화된 과거의 모든 대화가 풀리므로 TLS 1.3에서 없어졌습니다.

키 교환에도 약점이 있습니다. 중간에 끼어든 공격자가 양쪽에 자기의 공개 값을 대신 보내면, 클라이언트는 공격자와, 공격자는 서버와 각각 비밀을 만들고 가운데서 모든 내용을 풀어 보게 됩니다. 키 교환은 "누구와" 비밀을 만들었는지 알려 주지 않으므로, 상대가 진짜 서버인지 확인하는 수단이 따로 있어야 합니다. 그것이 아래의 전자 서명과 인증서입니다.

더 깊이: 양자 컴퓨터에 대비한 하이브리드 키 교환

X25519 같은 타원 곡선 키 교환은 큰 양자 컴퓨터 앞에서는 안전하지 않습니다. 2026년에 나온 RFC 10024는 암호학적으로 의미 있는 양자 컴퓨터(cryptographically relevant quantum computer, CRQC)를 가진 사람이라면 키 교환을 엿보기만 해도 비밀 값을 되찾을 수 있다고 적습니다. 전방 비밀성은 장기 키가 새는 경우를 막을 뿐이어서, 지금 녹화해 둔 대화를 그런 컴퓨터가 생긴 뒤에 푸는 공격은 막지 못합니다.

그래서 TLS 1.3에는 양자 컴퓨터의 공격도 견디도록 설계된 키 캡슐화 방식 ML-KEM을 기존의 키 교환과 함께 쓰는 하이브리드 방식이 들어왔습니다. ML-KEM은 미국 NIST가 2024년 8월 FIPS 203으로 표준화한 방식이고, RFC 10024는 이를 X25519와 묶은 X25519MLKEM768을 비롯한 세 가지 조합을 정의합니다. 두 방식에서 얻은 비밀을 이어 붙여 쓰므로, 둘 가운데 하나만 안전하게 남아 있어도 전체가 안전합니다. 대신 클라이언트가 보내는 키 공유 값이 X25519만 쓸 때의 32바이트에서 1216바이트로 커집니다.

브라우저는 RFC가 나오기 전부터 이 방식을 써 왔습니다. Google 보안 블로그의 2024년 9월 글에 따르면, Chrome은 표준화 전의 Kyber를 X25519와 묶어 쓰다가, Kyber가 표준화 과정에서 조금 바뀌어 ML-KEM이 되자 Chrome 131부터 코드 포인트 0x11EC의 ML-KEM 조합으로 옮겼습니다.

해시는 지문을 만들고, HMAC은 열쇠로 도장을 찍는다

해시 함수(hash function)는 길이에 상관없이 데이터를 정해진 길이의 값으로 요약합니다. 암호에 쓰는 해시는 출력에서 입력을 거꾸로 찾기 어렵고, 같은 출력을 내는 두 입력을 찾기 어려우며, 입력이 한 비트만 바뀌어도 출력이 완전히 달라지도록 만듭니다. 출력의 길이가 정해져 있으니 같은 출력을 내는 입력은 이론적으로 존재하지만, 그것을 찾는 데 현실적으로 불가능한 계산이 필요하다는 뜻입니다.

해시출력지금의 평가
MD5128비트충돌을 쉽게 만들 수 있어 보안 용도로 쓰지 않는다
SHA-1160비트실제 충돌이 만들어져 인증서 서명 등에서 퇴출되었다
SHA-256, SHA-384256, 384비트지금 가장 널리 쓰는 SHA-2 계열
SHA-3여러 길이SHA-2와 구조가 다른 표준

해시만으로는 누가 만든 값인지 알 수 없습니다. 키가 없으므로 누구나 같은 값을 계산할 수 있어서, 파일을 바꾼 공격자는 바뀐 파일의 해시도 새로 만들어 함께 바꿔 놓을 수 있습니다. 그래서 위조를 막으려면 비밀 키를 함께 넣어 계산하는 메시지 인증 코드(message authentication code, MAC)를 쓰며, 해시로 만드는 표준 방식이 HMAC(RFC 2104)입니다. 다음 코드는 표준 문서에 실린 시험값으로 두 가지를 확인합니다.

import hashlib
import hmac

digest = hashlib.sha256(b"abc").hexdigest()                     # FIPS 180의 SHA-256 시험값
print(digest)
print(f"{len(digest) * 4}비트")                                 # 16진수 한 글자는 4비트

key, msg = b"Jefe", b"what do ya want for nothing?"
tag = hmac.new(key, msg, hashlib.sha256).hexdigest()            # RFC 4231의 시험 사례 2
print(tag)
print(hmac.compare_digest(tag, hmac.new(key, msg, hashlib.sha256).hexdigest()))
print(hmac.compare_digest(tag, hmac.new(key, msg + b"!", hashlib.sha256).hexdigest()))
ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
256비트
5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843
True
False

abc의 SHA-256은 미국 표준 FIPS 180의 예제 값이고, HMAC 값은 RFC 4231의 두 번째 시험 사례와 같습니다. 키와 메시지가 같으면 언제나 같은 태그가 나오고, 메시지에 한 글자만 더해도 태그가 맞지 않습니다. 키를 모르는 공격자는 바꾼 메시지에 맞는 태그를 만들 수 없으므로, 태그가 맞으면 키를 가진 쪽이 만든 메시지가 바뀌지 않고 도착했다고 볼 수 있습니다. 태그를 비교할 때 == 대신 hmac.compare_digest를 쓰는 것은, 앞에서부터 몇 글자가 맞았는지에 따라 비교 시간이 달라져 정답을 한 글자씩 알아내는 공격을 막기 위해서입니다.

HMAC은 키를 나누어 가진 모두가 같은 태그를 만들 수 있으므로, 키를 가진 두 사람 가운데 누가 만들었는지는 구별하지 못합니다. 또 태그는 검사 값일 뿐이어서 메시지의 내용을 숨기지 않습니다.

전자 서명은 개인 키로 서명하고 공개 키로 확인한다

공개 키 암호(public key cryptography)는 짝을 이루는 두 키를 씁니다. 개인 키(private key)는 주인만 갖고, 공개 키(public key)는 누구에게나 나누어 줍니다. 전자 서명(digital signature)은 개인 키로 메시지에 대한 서명 값을 만들고, 누구든 공개 키로 그 서명이 그 메시지에 맞는지 확인하는 방식입니다. HMAC과 달리 서명은 개인 키를 가진 한 사람만 만들 수 있으므로, 공개 키의 주인이 서명했다는 것을 누구나 확인할 수 있습니다.

다음 코드는 교과서에 자주 나오는 작은 RSA 키로 서명과 확인을 따라갑니다. 공개 키는 n = 61 × 53 = 3233과 e = 17이고, 개인 키는 d = 2753입니다.

n, e, d = 3233, 17, 2753                # 교과서 RSA의 작은 예: 공개 키 (n, e), 개인 키 d
message = 2790                          # 서명할 값(실제로는 메시지의 해시를 패딩해 쓴다)
signature = pow(message, d, n)          # 개인 키로 서명한다
print("서명:", signature)
print("공개 키로 확인:", pow(signature, e, n) == message)
print("값을 바꾸면:", pow(signature, e, n) == message + 1)
서명: 65
공개 키로 확인: True
값을 바꾸면: False

65¹⁷ mod 3233을 계산하면 2790이 나오므로, 공개 키로 확인한 값이 원래의 메시지와 같습니다. 이 서명을 만들 수 있는 것은 d를 아는 사람뿐이고, 메시지가 바뀌면 확인에 실패합니다. 실제 서명은 이렇게 수를 그대로 거듭제곱하지 않고, 메시지의 해시에 무작위 패딩을 더한 RSA-PSS나 타원 곡선을 쓰는 ECDSA, Ed25519 같은 방식을 씁니다. 패딩 없는 교과서 RSA는 여러 공격에 약하기 때문입니다.

서명이 확인해 주는 것은 "이 공개 키에 짝이 되는 개인 키로 서명했다"는 사실까지입니다. 그 공개 키가 정말 example.com의 것인지는 서명만으로 알 수 없습니다. 이 공개 키가 누구의 것인지를 믿을 수 있는 제3자가 서명해 보증한 문서가 인증서(certificate)입니다. 인증 기관(certificate authority, CA)이 서버의 공개 키와 도메인 이름을 묶어 서명하고, 브라우저는 미리 믿기로 정해 둔 CA의 공개 키로 그 서명을 확인합니다.

암호 기능 비교

해시·인증·서명·암호화는 보장하는 범위가 다릅니다

해시는 발신자를 인증하지 않습니다. HMAC은 공유 키, 서명은 신뢰할 공개키, AEAD는 대칭키와 nonce 규칙을 전제로 검증합니다.

키와 결과로 구분하는 암호 기능
기능필요한 키와 결과검증으로 알 수 있는 범위
해시키 없이 데이터를 요약한 해시값같은 입력의 요약을 비교합니다. 공격자도 다시 계산할 수 있어 발신자를 인증하지 못합니다.
HMAC공유 비밀 키로 만든 인증 태그공유 키를 가진 측이 만든 메시지인지 검증합니다. 같은 키 소유자끼리 작성자를 구별하지는 못합니다.
디지털 서명개인키로 서명, 공개키로 검증해당 키의 서명과 메시지의 일치를 검증합니다. 신원 해석은 공개키를 누구와 연결해 신뢰하는지에 달렸습니다.
AEAD대칭키·nonce로 만든 암호문과 태그내용을 숨기고 암호문과 부가 데이터의 무결성을 검사합니다. nonce 사용 규칙은 선택한 알고리즘을 따라야 합니다.
기능: 해시
필요한 키와 결과: 키 없이 데이터를 요약한 해시값
검증으로 알 수 있는 범위: 같은 입력의 요약을 비교합니다. 공격자도 다시 계산할 수 있어 발신자를 인증하지 못합니다.
기능: HMAC
필요한 키와 결과: 공유 비밀 키로 만든 인증 태그
검증으로 알 수 있는 범위: 공유 키를 가진 측이 만든 메시지인지 검증합니다. 같은 키 소유자끼리 작성자를 구별하지는 못합니다.
기능: 디지털 서명
필요한 키와 결과: 개인키로 서명, 공개키로 검증
검증으로 알 수 있는 범위: 해당 키의 서명과 메시지의 일치를 검증합니다. 신원 해석은 공개키를 누구와 연결해 신뢰하는지에 달렸습니다.
기능: AEAD
필요한 키와 결과: 대칭키·nonce로 만든 암호문과 태그
검증으로 알 수 있는 범위: 내용을 숨기고 암호문과 부가 데이터의 무결성을 검사합니다. nonce 사용 규칙은 선택한 알고리즘을 따라야 합니다.

TLS는 이 기술들을 어떻게 묶나

TLS는 지금까지 본 기술을 각자 잘하는 일에 씁니다.

지켜야 할 것TLS 1.3에서 쓰는 기술
처음 만난 서버와 비밀을 만든다ECDHE 키 교환
그 서버가 진짜인지 확인한다CA가 서명한 인증서와, 서버가 개인 키로 하는 핸드셰이크 서명
공유 비밀에서 여러 키를 만든다HMAC으로 만드는 키 유도 함수 HKDF
데이터를 숨기고 바뀌지 않았음을 확인한다AES-GCM이나 ChaCha20-Poly1305 같은 AEAD

공개 키 기술은 계산이 느리지만 처음 만난 상대와 비밀을 만들고 상대를 확인하는 데 쓰고, 그렇게 얻은 키로 빠른 대칭 키 암호가 실제 데이터를 맡는 구조입니다. TLS 1.3은 키 교환이 끝난 직후부터 나머지 핸드셰이크 메시지도 암호화합니다. 그래도 TLS가 모든 것을 숨기지는 않습니다. IP 주소와 포트, 통신한 시각과 데이터의 양은 여전히 드러나고, 접속하려는 도메인 이름도 TLS의 SNI로 보이는 경우가 많습니다. 각 단계의 실제 메시지는 다음 글에서 봅니다.

정리

  • TLS는 숨기는 일, 바뀌지 않았음을 확인하는 일, 상대를 확인하는 일을 서로 다른 암호 기술로 나누어 맡깁니다.
  • 대칭 키 암호는 빠르지만 키를 나누어 갖는 문제가 있고, 지금은 내용을 숨기면서 위조도 검출하는 AEAD를 nonce를 겹치지 않게 써서 씁니다.
  • 디피-헬만 키 교환은 공개 값만 주고받아 공유 비밀을 만들며, 연결마다 새 비밀을 쓰는 ECDHE는 전방 비밀성을 주지만 상대가 누구인지는 따로 확인해야 합니다.
  • 해시는 누구나 계산할 수 있어 출처를 증명하지 못하고, HMAC은 키를 가진 쪽만 태그를 만들 수 있지만 키를 가진 사람끼리는 구별하지 못합니다.
  • 전자 서명은 개인 키를 가진 한 사람만 만들고 누구나 공개 키로 확인하며, 그 공개 키가 누구의 것인지는 CA가 서명한 인증서가 보증합니다.
  • TLS 1.3은 ECDHE, 인증서와 서명, HKDF, AEAD를 조합하며, IP 주소나 데이터의 양 같은 정보는 숨기지 못합니다.

연습 문제

1. 같은 키를 가진 두 사람

A와 B가 같은 HMAC 키를 알고 있고, 받은 메시지의 태그가 맞았습니다. 이것만으로 A가 보냈고 B는 보내지 않았다고 말할 수 있나요? 메시지에 계좌 번호가 평문으로 들어 있다면, 태그가 맞는다는 사실이 그 번호를 숨겨 주나요?

해설 보기

말할 수 없습니다. A와 B 모두 같은 키로 같은 태그를 만들 수 있으므로, 키를 가진 둘 가운데 누가 만들었는지는 HMAC으로 구별되지 않습니다. 보낸 사람을 구별해야 한다면 각자 자기 개인 키로 서명하게 해야 합니다. 태그는 메시지가 바뀌지 않았는지 검사하는 값일 뿐이므로 평문의 계좌 번호는 그대로 보입니다. 내용을 숨기려면 AEAD 같은 암호화를 따로 써야 합니다.

2. 다른 비밀로 다시 해 보기

본문의 디피-헬만 예에서 p = 23, g = 5는 그대로 두고 비밀만 a = 6, b = 15로 바꾸면, 주고받는 값과 공유 비밀은 각각 얼마인가요? 중간의 공격자가 양쪽에 자기의 공개 값을 대신 보낸다면 무엇이 잘못되나요?

해설 보기

5⁶ mod 23 = 8, 5¹⁵ mod 23 = 19이므로 주고받는 값은 8과 19입니다. 한쪽은 19⁶ mod 23을, 다른 쪽은 8¹⁵ mod 23을 계산하며 둘 다 2가 됩니다. 공격자가 끼어들면 클라이언트는 공격자와, 서버는 공격자와 각각 다른 공유 비밀을 만들고, 공격자는 양쪽의 메시지를 풀었다가 다시 암호화해 넘기면서 모든 내용을 봅니다. 키 교환 자체는 상대가 누구인지 확인하지 않으므로, TLS는 서버가 자기 개인 키로 키 교환의 내용에 서명하게 하고 클라이언트가 인증서의 공개 키로 그 서명을 확인해 이 공격을 막습니다.

3. 같은 서버에 올린 해시

어느 사이트가 설치 파일과 그 파일의 SHA-256 해시를 같은 웹 서버에 올려 두고, 내려받은 뒤 해시를 비교하라고 안내합니다. 공격자가 이 웹 서버를 장악했다면 해시 비교가 위조를 잡아낼 수 있나요? 어떻게 바꾸면 좋을까요?

해설 보기

잡아내지 못합니다. 해시는 키 없이 누구나 계산할 수 있으므로, 서버를 장악한 공격자는 설치 파일을 바꾼 뒤 바뀐 파일의 해시를 계산해 안내 페이지의 값도 함께 바꿔 둘 수 있습니다. 해시 비교는 내려받는 도중의 손상을 잡는 데는 쓸모가 있지만 위조를 막지는 못합니다. 위조를 막으려면 개발자가 개인 키로 파일에 서명하고 사용자가 미리 받아 둔 공개 키로 확인하게 하거나, 적어도 해시를 공격자가 함께 바꿀 수 없는 다른 경로로 알려야 합니다.

다음 글 TLS 핸드셰이크에서는 TLS 1.3에서 클라이언트와 서버가 어떤 메시지를 주고받으며 키를 만들고 서버를 확인하는지, 그리고 브라우저가 인증서의 사슬을 어떻게 검증하는지 따라갑니다.