[Security] 고수준에서 암호학 이해하기
암호화·해시·서명·키 교환이 각각 무엇을 해결하는지 다시 정리해보기
암호 기술을 개별 알고리즘보다 “어떤 문제를 해결하는 블록인가”로 공부해보자
암호학을 처음 볼 때는 AES, RSA, SHA 같은 알고리즘 이름부터 보이니까 “어떤 게 더 센 알고리즘이지?”부터 생각하게 됐다. 그런데 다시 공부해보니, 실제 시스템에서는 하나의 알고리즘보다 여러 도구를 왜, 어떤 순서로 연결했는지가 더 중요했다.
예를 들어 데이터를 암호화했다고 해서 끝이 아니다.
- 남이 내용을 못 보게 하는 것
- 데이터가 바뀌지 않았는지 확인하는 것
- 보낸 사람이 진짜 그 사람인지 확인하는 것
이 세 가지는 비슷해 보이지만 서로 다른 문제다. 그래서 암호학은 먼저 “무엇을 보호할 것인가?”를 정하고, 그에 맞는 도구를 골라야 한다.
먼저 전체 지도부터 보자
| 해결하고 싶은 문제 | 주로 사용하는 도구 |
|---|---|
| 내용을 못 보게 하고 싶다 | 암호화 |
| 데이터가 변조되지 않았는지 확인하고 싶다 | MAC, 인증된 암호화 |
| 특정 사람이 작성한 것인지 확인하고 싶다 | 전자서명 |
| 데이터의 고정 길이 요약값이 필요하다 | 암호학적 해시 |
| 통신 상대와 공통의 비밀키를 만들고 싶다 | 키 교환 |
| 원본 키에서 용도별 키를 만들고 싶다 | 키 유도 함수(KDF) |
| 예측할 수 없는 키와 난수가 필요하다 | CSPRNG |
이 표를 머릿속에 넣고 하나씩 살펴보자.
암호화는 무엇을 보호하나
기밀성, 무결성, 인증
내가 가장 먼저 헷갈렸던 개념들이다.
- 기밀성(Confidentiality): 권한이 없는 사람이 데이터의 내용을 읽지 못하게 한다.
- 무결성(Integrity): 데이터가 중간에서 변경되지 않았는지 확인한다.
- 인증(Authentication): 내가 통신하는 상대가 내가 생각한 그 상대가 맞는지 확인한다.
암호화는 기본적으로 기밀성을 제공한다. 그런데 “암호화됐으니 변조도 못 하겠지”라고 생각하면 위험하다. 암호문을 변경하는 시도를 따로 잡아내지 못하는 방식도 있기 때문이다. 실전에서는 기밀성과 무결성을 같이 보장하는 인증된 암호화(AEAD)를 우선적으로 고려하는 편이 좋다.
암호학의 재료: 난수, 해시, 키 유도
CSPRNG: 키를 만드는 출발점
CSPRNG(Cryptographically Secure Pseudorandom Number Generator)는 암호학적으로 안전한 유사난수 생성기다. 일반적인 난수 함수와 달리, 일부 결과를 보더라도 앞이나 뒤의 값을 예측하기 어려워야 한다.
암호 키는 물론이고 nonce, salt, 일회용 토큰 등에도 안전한 난수가 필요하다. 언어가 제공하는 일반 난수 함수를 그냥 사용하면 안 되고, 운영체제의 안전한 난수 소스를 사용하는 표준 보안 API를 써야 한다.
여기서 자주 헷갈리는 세 가지를 나눠보면:
- key: 비밀이어야 한다. 노출되면 암호 시스템 자체가 무너질 수 있다.
- nonce: 보통 비밀일 필요는 없지만, 같은 키에서 재사용하지 않는 것이 핵심인 알고리즘이 많다.
- salt: 비밀일 필요는 없다. 비밀번호마다 다른 값을 사용해 같은 비밀번호의 해시 결과가 같아지는 것과 미리 계산한 공격을 막는다.
암호학적 해시: 데이터의 지문
해시 함수는 어떤 길이의 데이터든 고정된 길이의 digest로 바꿔준다. 입력이 조금만 달라져도 결과는 크게 달라져야 하고, 다음 성질을 기대한다.
- digest만 보고 원본 입력을 찾기 어려워야 한다.
- 특정 입력과 같은 digest를 만드는 다른 입력을 찾기 어려워야 한다.
- 같은 digest가 나오는 서로 다른 두 입력을 찾기 어려워야 한다.
파일 다운로드 후 checksum을 비교하거나, 큰 데이터를 서명하기 전에 요약할 때 이런 성질을 활용한다.
하지만 해시는 암호화가 아니다. 복호화를 전제로 하지 않고, 해시 값이 있다고 해서 작성자나 변조 여부까지 자동으로 알 수 있는 것도 아니다. 인증이 필요하면 MAC이나 전자서명 같은 도구가 추가로 필요하다.
비밀번호에는 빠른 해시를 쓰면 안 된다
일반적인 해시 함수는 빠른 것이 장점이다. 그런데 비밀번호를 저장할 때는 이 장점이 그대로 약점이 된다. 공격자도 짧은 시간에 엄청난 수의 비밀번호를 대입해볼 수 있기 때문이다.
그래서 SHA-256(password) 같은 방식 대신, 의도적으로 느리고 메모리 비용도 조절할 수 있는 비밀번호 해싱 알고리즘을 사용한다. 현재는 Argon2id를 우선 고려하고, 환경에 따라 scrypt, bcrypt, PBKDF2 등을 사용한다. 각 비밀번호에 고유한 salt를 넣고, 서비스의 성능에 맞게 cost parameter를 정해야 한다.
KDF: 하나의 비밀에서 용도별 키 만들기
키 교환으로 얻은 공유 비밀값을 바로 AES 키로 쓰는 것은 좋은 설계가 아니다. 길이나 형식이 맞지 않을 수도 있고, 하나의 키를 여러 목적에 재사용하면 서로 다른 기능 사이에서 예상하지 못한 영향이 생길 수 있다.
KDF(Key Derivation Function)는 초기 키 재료에서 암호학적으로 강한 키를 하나 이상 만든다. 예를 들어 하나의 공유 비밀값에서 client 송신용 키, server 송신용 키, 추가 인증용 키를 각각 만들 수 있다.
HKDF는 이런 용도에 많이 사용되는 KDF다. 용도를 나타내는 context 정보를 같이 넣어주면, 같은 초기 키 재료에서도 서로 독립적인 키를 유도할 수 있다.
여기서 주의할 점은 일반적인 키 유도와 비밀번호 해싱은 다른 문제라는 것이다. 사람이 만든 비밀번호처럼 엔트로피가 낮은 입력에는 비밀번호 전용 알고리즘이 필요하다.
데이터를 숨기는 방법
대칭 암호화
대칭 암호화는 보내는 쪽과 받는 쪽이 같은 키를 사용한다. 일반적으로 빠르기 때문에 파일, 네트워크 패킷, 데이터베이스의 필드 같이 실제 데이터를 암호화하는 데 사용한다. AES가 대표적인 블록 암호다.
그런데 AES라는 이름만 골랐다고 설계가 끝나는 것은 아니다. 운용 모드, nonce·IV 생성 방식, 무결성 검증, 키 저장과 교체 전략까지 같이 맞아야 한다. 이 조합을 직접 설계하려고 하면 급격히 어려워진다.
AEAD: 암호화와 변조 탐지를 한 번에
AEAD(Authenticated Encryption with Associated Data)는 데이터를 숨기는 기능과 변조를 확인하는 기능을 하나의 구조로 묶어준다. AES-GCM과 ChaCha20-Poly1305가 대표적인 예다.
AEAD를 사용하면 plaintext, ciphertext, authentication tag, nonce를 다루게 된다. 노출되어도 되지만 변조는 감지해야 하는 부가 데이터는 AAD로 함께 인증할 수 있다.
특히 nonce 규칙은 중요하다. 알고리즘마다 요구사항이 다르지만, 같은 키에서 nonce를 재사용하면 보안성이 크게 무너질 수 있다. 직접 규칙을 만들기보다 검증된 라이브러리의 고수준 API를 사용하고, authentication tag 검증이 성공하기 전에는 복호화된 데이터를 사용하지 않는 것이 좋다.
비대칭 암호화와 하이브리드 암호화
비대칭 암호화는 공개키와 비밀키 쌍을 사용한다. 공개키는 다른 사람에게 공개해도 되지만, 비밀키는 소유자만 가지고 있어야 한다.
원리만 보면 대용량 파일도 공개키로 모두 암호화하면 될 것 같지만, 비대칭 암호는 대칭 암호보다 비싸고 입력 크기에도 제약이 있다. 그래서 실제로는 임시 대칭키를 만들고, 실제 데이터는 빠른 대칭 암호로 보호하고, 그 대칭키만 공개키 기반 방식으로 전달하는 하이브리드 구조를 자주 사용한다.
데이터를 누가 만들었고, 바꾸지 않았는지 확인하기
MAC: 같은 비밀을 아는 사람끼리의 도장
MAC(Message Authentication Code)은 공유키와 메시지를 입력으로 받아 짧은 인증 코드를 만든다. 받는 쪽은 같은 키로 MAC을 검증해 메시지가 변조되지 않았는지, 공유키를 아는 상대가 만든 것인지 확인할 수 있다. HMAC이 대표적인 구조다.
단, 두 사람이 같은 키를 알고 있으므로 누구든 올바른 MAC을 만들 수 있다. 따라서 제3자에게 “이 메시지를 정확히 A가 보냈다”고 증명하는 용도에는 맞지 않다.
전자서명: 비밀키로 서명하고 공개키로 검증하기
전자서명은 서명자가 비밀키로 서명을 만들고, 다른 사람들이 공개키로 검증하는 구조다. 전체 데이터를 그대로 서명하기보다 데이터의 해시를 계산한 뒤 그 값을 서명하는 구조가 일반적이다.
전자서명으로는 서명 후 데이터가 바뀌지 않았는지, 해당 비밀키를 가진 사람이 서명했는지 확인할 수 있다. 하지만 전자서명은 데이터를 숨기지 않는다. 내용이 노출되면 안 된다면 암호화를 별도로 적용해야 한다.
MAC과 전자서명을 비교하면
| MAC | 전자서명 | |
|---|---|---|
| 키 | 양쪽이 같은 비밀키를 공유 | 서명자는 비밀키, 검증자는 공개키 사용 |
| 주요 목적 | 공유키를 아는 상대가 만든 메시지인지 확인 | 특정 서명자가 만든 서명인지 확인 |
| 검증 범위 | 비밀키를 아는 주체 | 공개키를 가진 누구나 |
| 주의점 | 검증자도 MAC을 만들 수 있음 | 비밀키 보호와 공개키의 신뢰성이 필요 |
처음 만난 두 사람이 같은 키를 만드는 방법
키 교환
키 교환은 통신 상대 둘이 네트워크 위에서 공통의 비밀값을 만드는 과정이다. Diffie-Hellman 계열이 대표적이다. 중간의 공격자가 오가는 데이터를 모두 보더라도, 정상적으로는 최종 공유 비밀값을 계산하지 못하게 하는 것이 목표다.
그런데 여기에 함정이 있다. 기본적인 키 교환만으로는 상대의 정체를 알 수 없다. 공격자가 중간에 들어와 양쪽과 각각 키를 교환하면, 양쪽은 공격자와 암호화된 통신을 하면서도 서로와 직접 통신한다고 착각할 수 있다. 이게 중간자 공격(MITM)이다.
인증된 키 교환
이 문제를 막으려면 키 교환 과정에 인증을 결합해야 한다. 전자서명, 인증서, 사전에 공유한 키 등을 이용해 지금 받은 키 교환 메시지가 정말 원하는 상대에게서 왔는지 확인한다.
일회성 키 쌍을 사용하는 ephemeral Diffie-Hellman 방식은, 세션이 끝난 뒤 임시 비밀값을 제거했다면 나중에 장기 비밀키가 노출되더라도 과거 세션 내용까지 복호화되는 위험을 줄일 수 있다. 이를 전방향 비밀성(Forward Secrecy)이라고 한다.
실제 통신에서는 이 블록들이 어떻게 연결될까
개념을 따로 보면 또 헷갈리므로, TLS와 비슷한 안전한 통신을 아주 단순화해서 연결해보자.
- client와 server가 암호학적으로 안전한 난수와 임시 키 재료를 만든다.
- server는 인증서와 서명을 이용해 자신의 정체와 키 교환 메시지를 인증한다.
- client와 server는 키 교환을 통해 공유 비밀값을 만든다.
- KDF로 공유 비밀값에서 통신 방향과 용도별 세션 키를 만든다.
- 실제 데이터는 세션 키와 AEAD로 암호화하고, 수신자는 tag를 검증한 뒤 데이터를 사용한다.
- 프로토콜은 순서 번호와 nonce 규칙을 통해 동일한 메시지가 다시 사용되는 replay 공격까지 관리한다.
결국 “TLS는 암호화를 쓴다”는 말은 맞지만, 그 안에는 난수 생성, 서명, 인증서, 키 교환, KDF, AEAD 등이 한꺼번에 연결되어 있다. 하나라도 잘못 조합하면 안전한 알고리즘을 쓰고도 전체 시스템은 취약해질 수 있다.
실전에서 기억할 것
이론을 보고 직접 암호 프로토콜을 만들어야겠다고 생각하면 곤란하다. 오히려 왜 직접 만들면 안 되는지 이해하는 게 이 공부의 목적에 가깝다.
- 암호 알고리즘과 프로토콜을 직접 설계하지 않는다.
- 검증된 라이브러리의 고수준 API를 사용한다.
- 기밀성만 필요한지, 무결성과 인증까지 필요한지 먼저 정의한다.
- 대칭 암호가 필요하다면 AEAD를 우선 고려한다.
- nonce 생성·재사용 규칙을 알고리즘 설명서대로 지킨다.
- 비밀번호를 일반 해시 함수로 단순 해싱하지 않는다.
- 키를 코드와 로그에 남기지 않고, 저장·접근 제어·교체·폐기 절차까지 관리한다.
- tag나 서명 검증이 실패한 데이터는 사용하지 않는다.
다시 공부하고 난 뒤의 정리
암호학을 알고리즘 목록으로 볼 때는 해시, MAC, 서명, 키 교환이 다 비슷한 보안 기술처럼 보였다. 그런데 이제는 아래처럼 나누니까 훨씬 이해하기 편해졌다.
- 해시는 데이터를 요약한다.
- 암호화는 내용을 숨긴다.
- MAC은 공유키를 아는 상대가 만든 메시지인지 확인한다.
- 전자서명은 비밀키 소유자가 서명했는지 확인한다.
- 키 교환은 통신할 두 주체가 공유 비밀값을 만든다.
- KDF는 그 비밀값을 실제 용도에 맞는 키로 바꿔준다.
- AEAD는 암호화와 무결성 검증을 같이 처리한다.
- CSPRNG는 이 모든 과정에 필요한 예측 불가능한 값을 만든다.
각 도구가 무엇을 하는지 알면 “이 알고리즘이 안전한가?”에서 한 발 더 나아가, “이 도구가 지금 해결하려는 문제에 맞나?”를 물을 수 있게 된다. 내가 이번에 다시 공부하며 얻은 가장 큰 수확도 그거였다.