웹사이트에 접속하다 보면 주소 앞에 https://가 붙어 있는 것을 볼 수 있다.
HTTPS를 사용한다는 것은 브라우저와 서버 사이에서 주고받는 데이터를 TLS를 이용해 보호한다는 의미다.
예를 들어 내가 서버에 다음과 같은 데이터를 보낸다고 해보자.
password=hello1234
이 데이터가 평문 그대로 네트워크를 지나간다면 중간에서 패킷을 가로챈 사람이 내용을 그대로 확인할 수 있다.
HTTPS에서는 이런 데이터를 암호화해서 전송한다.
password=hello1234
↓ 암호화
8a71c09f3e4d....
공격자가 패킷을 가로채더라도 암호문만 보이기 때문에 원래 내용을 쉽게 알아낼 수 없다.
그런데 여기서 한 가지 궁금한 점이 생긴다.
브라우저와 서버는 어떤 키로 데이터를 암호화하는 것일까?
이걸 이해하려면 먼저 대칭 암호화와 비대칭 암호화의 차이를 알아야 한다.
대칭 암호화는 암호화와 복호화에 같은 키를 사용하는 방식이다.
예를 들어 orange라는 비밀키가 있다고 생각해보자.
Hello
↓ orange라는 키로 암호화
암호문
↓ orange라는 키로 복호화
Hello
암호화할 때 사용한 키와 복호화할 때 사용하는 키가 동일하기 때문에 대칭 암호화라고 부른다.
대표적인 알고리즘으로 AES가 있다.
과거에는 DES도 사용되었지만 현재는 보안상의 이유로 거의 사용하지 않는다.
대칭 암호화의 가장 큰 장점은 빠르다는 것이다.
HTTPS처럼 많은 데이터를 계속 주고받아야 하는 환경에서는 실제 데이터 암호화에 대칭 암호화를 사용한다.
AES는 현재 가장 널리 사용되는 대칭키 암호화 알고리즘 중 하나다.
예를 들어 AES-128은 128비트 키를 사용하며 총 10개의 라운드를 거쳐 데이터를 변환한다.
AES를 단순히 "문자를 뒤섞는 알고리즘"이라고 생각하기 쉽지만 실제로는 조금 더 체계적이다.
각 라운드에서는 대략 다음과 같은 연산이 수행된다.
SubBytes
ShiftRows
MixColumns
AddRoundKey
각 바이트 값을 다른 값으로 치환하고, 위치를 이동시키고, 서로 섞은 뒤 마지막으로 암호화 키에서 만들어진 라운드 키를 XOR 연산으로 적용한다.
이런 연산을 여러 번 반복하면서 원래 데이터와 전혀 다른 형태의 암호문을 만든다.
AES-128은 10라운드, AES-192는 12라운드, AES-256은 14라운드를 사용한다.
HTTPS에서도 AES-128-GCM 같은 형태가 많이 사용된다.
중요한 것은 AES가 매우 빠르다는 점이다.
그래서 실제 HTTP 요청이나 응답 데이터를 암호화할 때는 이런 대칭 암호화 알고리즘을 사용한다.
그런데 문제가 하나 있다.
브라우저와 서버가 AES를 사용하려면 둘 다 같은 비밀키를 가지고 있어야 한다.
그렇다면 처음 만난 브라우저와 서버는 이 비밀키를 어떻게 안전하게 만들어낼까?
여기서 비대칭 암호 방식과 키 교환 알고리즘이 등장한다.
비대칭 암호 방식에서는 서로 다른 두 개의 키를 사용한다.
공개키
개인키
이름 그대로 공개키는 다른 사람에게 공개해도 되지만 개인키는 소유자만 가지고 있어야 한다.
대표적인 알고리즘으로 RSA가 있다.
RSA에서는 공개키로 암호화한 데이터를 대응되는 개인키로 복호화할 수 있다.
데이터
↓ 서버 공개키
암호문
↓ 서버 개인키
원래 데이터
과거 TLS에서는 이 성질을 이용해 세션키를 전달하기도 했다.
하지만 현재 TLS에서는 RSA를 이런 식의 키 교환 용도로 사용하지 않는다.
그 대신 Diffie-Hellman 계열의 키 교환 방식이 사용된다.
Diffie-Hellman의 재미있는 점은 서로 비밀키를 직접 전달하지 않고도 같은 비밀값을 만들 수 있다는 것이다.
두 사람이 있다고 해보자.
Client
Server
둘은 먼저 공개된 값 g와 p를 사용한다.
g = 5
p = 23
이 값들은 공격자가 알아도 상관없다.
Client는 자신만 알고 있는 비밀값 a를 하나 선택한다.
a = 6
그리고 다음 값을 계산한다.
A = g^a mod p
A = 5^6 mod 23
A = 8
Server 역시 자신만의 비밀값 b를 선택한다.
b = 15
그리고 같은 방식으로 계산한다.
B = g^b mod p
B = 5^15 mod 23
B = 19
이제 Client와 Server는 A와 B를 서로 교환한다.
Client → 8 → Server
Client ← 19 ← Server
중간에서 누군가 패킷을 보고 있다고 해도 g, p, A, B는 모두 볼 수 있다.
하지만 각자가 가지고 있는 a와 b는 공개되지 않는다.
Client는 Server가 보내준 B를 이용해 다음 값을 계산한다.
B^a mod p
19^6 mod 23
= 2
Server도 Client가 보내준 A를 이용한다.
A^b mod p
8^15 mod 23
= 2
둘 다 결과가 같다.
그 이유는 결국 두 계산 모두 다음 값이 되기 때문이다.
g^(ab) mod p
결과적으로 Client와 Server는 비밀값을 네트워크로 직접 전송하지 않고도 같은 공유 비밀을 만들어낼 수 있다.
실제 TLS에서는 이 값을 그대로 AES 키로 사용하는 것이 아니라, 이 공유 비밀과 핸드셰이크 과정의 여러 값을 KDF에 넣어 여러 개의 암호화 키를 만들어낸다.
흔히 이 키들을 세션키라고 부른다.
공격자가 다음 값들을 알고 있다고 생각해보자.
g = 5
p = 23
A = 8
그러면 결국 다음 식에서 a를 알아내면 된다.
5^a mod 23 = 8
숫자가 이렇게 작다면 1부터 하나씩 넣어보는 브루트포스로 금방 알아낼 수 있다.
하지만 실제 암호화에서는 비교할 수 없을 정도로 큰 숫자를 사용한다.
이때 g^a mod p를 계산하는 것은 쉽지만 결과만 가지고 다시 a를 구하는 것은 매우 어렵다.
이를 이산 로그 문제라고 한다.
Diffie-Hellman은 이 문제의 계산 난이도를 이용한다.
현대 TLS에서는 일반적인 Diffie-Hellman보다 ECDHE를 많이 사용한다.
ECDHE는 다음의 약자다.
Elliptic Curve Diffie-Hellman Ephemeral
타원곡선 위에서 Diffie-Hellman과 비슷한 방식으로 공유 비밀을 만든다.
기본적인 아이디어는 동일하다.
Client 개인값
↓
Client 공개값 ──────────────┐
│
↓
공유 비밀
↑
│
Server 공개값 ──────────────┘
↑
Server 개인값
다만 일반적인 정수 모듈러 연산 대신 타원곡선상의 수학 문제를 이용한다.
이를 타원곡선 이산 로그 문제라고 한다.
같은 수준의 보안을 제공하면서 상대적으로 작은 키를 사용할 수 있기 때문에 현대 암호 시스템에서 널리 사용된다.
마지막에 붙어 있는 E, 즉 Ephemeral도 중요하다.
연결할 때마다 임시 키를 새로 만들어 사용한다는 의미다.
덕분에 나중에 서버의 장기 개인키가 유출되더라도 과거에 캡처해둔 TLS 통신까지 한꺼번에 복호화되는 것을 막을 수 있다.
이 특성을 Forward Secrecy라고 한다.
과거에는 RSA를 이용한 TLS 키 교환도 사용되었다.
구조를 단순하게 표현하면 다음과 같다.
Server
↓
RSA 공개키 전달
↓
Client
Client
↓
세션키 생성
↓
서버 RSA 공개키로 암호화
↓
Server
Server
↓
RSA 개인키로 복호화
↓
세션키 획득
여기서는 Client가 만든 비밀값 자체가 네트워크를 통해 전달된다.
물론 RSA 공개키로 암호화되어 있기 때문에 바로 읽을 수 있는 것은 아니다.
하지만 공격자가 과거 TLS 패킷을 모두 저장해두었다가 나중에 서버 RSA 개인키를 탈취한다면 과거에 전송된 세션키를 복호화할 가능성이 생긴다.
ECDHE는 구조가 다르다.
Client Server
개인값 a 개인값 b
│ │
↓ ↓
공개값 A 공개값 B
A ────────────────────→
←──────────────────── B
Client에서 공유 비밀 계산 Server에서 공유 비밀 계산
실제 공유 비밀 자체가 네트워크를 통해 이동하지 않는다.
Client와 Server가 각자 계산해서 같은 값을 만들어낸다.
이 때문에 현대 TLS에서 ECDHE가 중요하다.
TLS 1.3에서는 과거의 RSA key exchange 방식이 제거되었으며, 일반적인 HTTPS 연결에서는 ECDHE를 이용한 키 교환을 흔히 볼 수 있다.
이제 이 내용을 실제 HTTPS 연결에 대입해볼 수 있다.
브라우저에서 HTTPS 서버에 접속하면 HTTP 데이터를 보내기 전에 먼저 TLS 핸드셰이크가 진행된다.
TLS 1.3을 단순화하면 다음과 같은 흐름이다.
Client Server
ClientHello
─────────────────────────────────→
ServerHello
EncryptedExtensions
Certificate
CertificateVerify
Finished
←─────────────────────────────────
Finished
─────────────────────────────────→
========== 암호화된 HTTP 통신 ==========
먼저 Client가 ClientHello를 전송한다.
여기에는 TLS 협상에 필요한 여러 정보가 들어간다.
예를 들면 다음과 같다.
지원하는 TLS 버전
지원하는 Cipher Suite
Client Random
지원하는 암호 그룹
ECDHE Key Share
Server는 ClientHello를 확인하고 자신이 사용할 TLS 설정을 결정한다.
그리고 ServerHello를 전송한다.
선택된 TLS 버전
선택된 Cipher Suite
Server Random
Server ECDHE Key Share
Client와 Server는 서로 전달한 ECDHE 공개값과 자신이 가지고 있는 개인값을 이용해 같은 공유 비밀을 계산한다.
그 공유 비밀을 기반으로 TLS의 키 파생 함수를 사용해 실제 통신에 필요한 여러 키를 만든다.
그 이후부터는 핸드셰이크 메시지의 상당 부분도 암호화된다.
여기까지 보면 한 가지 문제가 남는다.
ECDHE를 사용하면 Client와 Server가 공유 비밀을 안전하게 만들 수 있다.
하지만 Client 입장에서 지금 통신하고 있는 서버가 정말 내가 접속하려던 서버인지 어떻게 확인할까?
공격자가 중간에서 자신을 서버라고 속일 수도 있다.
그래서 TLS에서는 인증서를 사용한다.
Server는 핸드셰이크 과정에서 자신의 인증서를 Client에게 전달한다.
Certificate
브라우저는 인증서를 확인한다.
대표적으로 다음과 같은 내용을 검증한다.
신뢰할 수 있는 인증기관에서 발급했는가
현재 접속한 도메인과 인증서의 도메인이 일치하는가
인증서가 유효기간 안에 있는가
인증서 체인이 정상적인가
그리고 서버는 CertificateVerify 메시지를 통해 인증서에 대응되는 개인키를 실제로 가지고 있다는 사실도 증명한다.
결국 TLS에서 인증서는 단순히 데이터를 암호화하기 위한 것이 아니라 상대방의 신원을 확인하는 데 중요한 역할을 한다.
TLS를 공부하다 보면 SHA-256 같은 이름도 자주 등장한다.
해시 함수는 임의 길이의 데이터를 고정 길이 값으로 변환한다.
예를 들어 다음 데이터가 있다고 해보자.
Hello TLS
SHA-256을 적용하면 항상 256비트의 해시 결과가 나온다.
Hello TLS
↓
SHA-256
↓
고정 길이의 해시값
입력 데이터가 조금만 달라져도 결과는 크게 달라진다.
Hello TLS
Hello TLS!
두 데이터의 해시는 전혀 다른 값이 나온다.
그리고 일반적인 암호학적 해시 함수는 해시값으로부터 원래 데이터를 되돌리는 것이 현실적으로 매우 어렵게 설계되어 있다.
TLS에서는 이런 해시 함수가 여러 곳에서 사용된다.
예를 들어 핸드셰이크 메시지들의 내용을 해싱해서 서로 같은 핸드셰이크를 보고 있는지 검증하거나, 키를 파생하거나, Finished 메시지를 계산하는 데 사용된다.
전자서명 과정에서도 해시가 중요한 역할을 한다.
TLS 패킷을 Wireshark로 보면 다음과 같은 이름을 볼 수 있다.
TLS_AES_128_GCM_SHA256
이것을 Cipher Suite, 우리말로 암호 제품군이라고 부른다.
TLS 1.3의 Cipher Suite는 크게 다음과 같이 읽을 수 있다.
TLS_AES_128_GCM_SHA256
└──────┬──────┘ └──┬──┘
│ │
데이터 암호화 해시 함수
AES_128_GCM은 실제 데이터를 암호화하는 방법을 의미한다.
AES는 암호화 알고리즘이고, GCM은 AES를 안전하게 사용하는 방식 중 하나다.
GCM은 단순히 데이터를 암호화하는 것뿐만 아니라 데이터가 중간에서 변조되지 않았는지도 확인할 수 있다.
이런 방식을 AEAD라고 한다.
Authenticated Encryption with Associated Data
SHA256은 TLS 핸드셰이크와 키 파생 등에 사용하는 해시 알고리즘을 나타낸다.
여기서 주의할 점이 하나 있다.
TLS 1.2까지는 Cipher Suite 이름 안에 RSA나 ECDHE 같은 키 교환 방식까지 포함되는 경우가 많았다.
예를 들면 다음과 같다.
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
하지만 TLS 1.3에서는 Cipher Suite와 키 교환 및 전자서명 알고리즘을 분리했다.
그래서
TLS_AES_128_GCM_SHA256
만 보고 ECDHE인지 RSA인지 판단하는 구조가 아니다.
이 부분이 TLS 1.2와 TLS 1.3을 공부할 때 가장 헷갈리기 쉬운 부분 중 하나다.
HTTPS에서는 하나의 암호 알고리즘만 사용하는 것이 아니다.
각 기술이 서로 다른 역할을 맡고 있다.
인증서
↓
서버가 진짜인지 확인
ECDHE
↓
공유 비밀 생성
HKDF
↓
통신에 사용할 여러 키 생성
AES-GCM
↓
실제 HTTP 데이터 암호화
SHA-256
↓
핸드셰이크 해시 및 키 파생 등에 사용
왜 이렇게 복잡하게 구성되어 있을까.
각 기술마다 잘하는 것이 다르기 때문이다.
비대칭 암호와 공개키 기반 기술은 상대방을 인증하거나 안전하게 키를 합의하는 데 유용하지만 많은 데이터를 암호화하기에는 상대적으로 느리다.
AES 같은 대칭 암호는 매우 빠르지만 Client와 Server가 같은 키를 가지고 있어야 한다.
그래서 TLS는 여러 암호 기술을 조합한다.
처음 연결할 때
인증서 + 공개키 기술 + ECDHE
↓
안전하게 공유 비밀 생성
이후 실제 데이터 통신
AES 같은 대칭 암호
↓
빠르게 암호화
이것이 HTTPS 통신의 핵심 구조다.
TLS는 이론만 공부하는 것보다 실제 패킷을 보면 훨씬 이해하기 쉽다.
Wireshark로 HTTPS 연결을 캡처하면 다음과 같은 메시지를 확인할 수 있다.
Client Hello
Server Hello
Application Data
Client Hello를 열어보면 브라우저가 지원하는 TLS 버전과 Cipher Suite, Key Share 같은 값을 확인할 수 있다.
Server Hello에서는 서버가 실제로 선택한 TLS 버전과 Cipher Suite를 볼 수 있다.
TLS 1.3에서는 ServerHello 이후의 많은 핸드셰이크 메시지가 이미 암호화되기 때문에 예전 TLS 버전처럼 모든 내용을 그대로 볼 수 있는 것은 아니다.
그래도 Wireshark를 통해
브라우저가 무엇을 제안했는지
서버가 무엇을 선택했는지
어떤 Cipher Suite가 사용되었는지
어떤 Key Share가 전달되었는지
정도는 직접 확인할 수 있다.
HTTPS를 단순히 "웹 통신을 암호화한다" 정도로만 알고 있을 때는 복잡해 보이지만 구조를 나눠보면 생각보다 명확하다.
서버가 진짜인지 인증하고, 서로 직접 전달하지 않고 공유 비밀을 만들고, 그 비밀에서 세션에 사용할 키를 만든 뒤, 빠른 대칭 암호를 사용해 실제 데이터를 주고받는다.
결국 HTTPS의 핵심은 하나의 강력한 암호 알고리즘을 사용하는 것이 아니다.
서로 다른 역할을 가진 여러 암호 기술을 적절하게 조합하는 것에 있다.