암호학 · 네트워크 보안
지금 이 글을 읽는 순간에도, 브라우저는 수십 번에 걸쳐 같은 작업을 반복하고 있다. 데이터를 남이 못 읽게 가리는 동시에, 누가 중간에서 손대지 못하도록 봉인하는 일이다. 그 일을 도맡은 주인공이 바로 AES-GCM이다.
웹 브라우저 주소창의 자물쇠를 눌러 보안 정보를 열어 보면 TLS_AES_128_GCM_SHA256 같은 알 수 없는 문자열이 보인다. 이 길고 낯선 이름의 한가운데에 있는 GCM이 이 글의 주제다. GCM은 갈루아/카운터 모드(Galois/Counter Mode)의 약자로, 고급 암호화 표준(AES, Advanced Encryption Standard)을 실제 인터넷 트래픽에 쓸 수 있게 엮어 주는 방식이다.
여기서 한 가지를 먼저 짚어 두자. 좋은 암호 시스템은 두 가지를 동시에 해내야 한다. 첫째, 내용을 못 읽게 만드는 것(기밀성). 둘째, 내용이 중간에 바뀌지 않았음을 보장하는 것(무결성). 많은 사람이 암호화라고 하면 첫 번째만 떠올리지만, 사실 두 번째가 빠지면 첫 번째도 쓸모가 절반으로 줄어든다. GCM이 널리 쓰이는 이유가 바로 이 둘을 한 번에, 빠르게 처리하기 때문이다. 이 글은 GCM이 그 두 가지를 어떻게 해내는지를, 암호학 비전공자도 따라올 수 있도록 차근차근 풀어 본다.
AES는 블록 암호다. 한 번에 딱 128비트(16바이트)만 암호화한다. 문제는 우리가 보내려는 데이터가 정확히 128비트인 경우가 거의 없다는 점이다. 웹페이지 하나, 이미지 한 장, 메시지 한 통은 모두 128비트보다 훨씬 길거나 어중간하다. 그래서 AES를 여러 번 반복해서 적용하는 운용 모드(mode of operation)가 필요하다. AES가 '벽돌 한 장을 찍어 내는 틀'이라면, 운용 모드는 '그 틀로 벽을 어떻게 쌓을 것인가'에 대한 설계도다.
가장 단순한 발상은 데이터를 128비트씩 잘라 각 토막을 똑같은 방식으로 암호화하는 것이다. 이것을 전자 코드북(ECB, Electronic Code Book) 모드라고 부르는데, 치명적인 약점이 있다. 같은 평문 토막은 항상 같은 암호문 토막으로 변환되기 때문에, 원본의 반복 패턴이 암호문에 그대로 비친다.
ECB는 무늬가 있는 유리창에 색종이를 붙이는 것과 같다. 색을 입혀 안이 안 보이게 했다고 믿지만, 창에 새겨진 격자무늬는 색종이 위로 그대로 드러난다. 실제로 ECB로 그림을 암호화하면 원본의 윤곽이 암호문에서 알아볼 수 있을 만큼 남는다. 가렸으되 가리지 못한 셈이다.
그래서 안전한 운용 모드는 같은 평문이라도 매번 다른 암호문이 나오도록 무작위성을 끌어들인다. 그 대표 격이 카운터 모드다.
카운터 모드의 발상은 영리하다. 놀랍게도 평문을 직접 암호화하지 않는다. 대신 '난스(nonce)'라는 일회용 무작위 값과 1씩 증가하는 카운터를 이어 붙인 블록을 AES로 암호화한다. 난스는 'number used once', 곧 '한 번만 쓰는 수'라는 뜻이다. 이렇게 카운터를 암호화해서 나온 무작위처럼 보이는 비트열을 키스트림(keystream)이라 부른다.
그다음, 이 키스트림과 실제 평문을 비트 단위 배타적 논리합(XOR, 같으면 0 다르면 1)으로 결합하면 암호문이 된다. 복호화는 정확히 같은 키스트림을 다시 만들어 암호문과 XOR하면 끝이다. XOR을 같은 값으로 두 번 하면 원래대로 돌아오는 성질을 이용하는 것이다.
카운터 모드는 무한히 긴 일회용 난수 종이테이프를 만들어 메시지 위에 겹쳐 놓는 것과 비슷하다. 키와 난스만 알면 양쪽이 똑같은 테이프를 다시 만들 수 있다. 보내는 쪽은 테이프를 덧씌워 가리고, 받는 쪽은 같은 테이프를 다시 덧씌워 걷어 낸다. 키와 난스를 모르는 제3자에게 키스트림은 그저 무작위 잡음이라, 암호문만 봐서는 원문을 추측할 길이 없다.
카운터 모드에는 매력적인 장점이 있다. 각 블록의 키스트림이 서로 독립적이라 병렬로 동시에 계산할 수 있고, 속도가 빠르다. 다만 반드시 지켜야 할 철칙이 하나 있다. 같은 키와 같은 난스의 조합을 두 번 써서는 절대 안 된다. 이 약속이 깨지면 어떤 일이 벌어지는지는 글 뒤에서 다시 다룬다.
지금까지의 카운터 모드는 기밀성은 훌륭하게 지킨다. 그런데 무결성은 전혀 보장하지 못한다. 모든 스트림 암호가 공유하는 약점이다. 왜 그런지 복호화 과정을 떠올려 보자. 복호화는 암호문을 키스트림과 XOR하는 것이다. 그런데 XOR에는 위험한 성질이 있다. 공격자가 암호문의 어떤 비트를 뒤집으면, 복호화된 평문에서 정확히 같은 위치의 비트가 그대로 뒤집힌다.
공격자는 평문을 읽지는 못한다. 하지만 암호문의 어느 자리가 어떤 의미를 갖는지 짐작할 수 있다면, 내용을 읽지 않고도 바꿔치기할 수 있다. 예를 들어 송금 메시지의 특정 위치가 받는 사람 계좌번호임을 안다면, 그 자리의 비트를 조작해 자기 계좌로 돈이 가도록 만들 수 있다. 더 무서운 점은, 받는 쪽에서 복호화가 오류 없이 성공한다는 것이다. 메시지가 깨져 보이지 않으니 변조를 눈치채기 어렵다.
암호화가 됐다고 해서 '바뀌지 않았다'는 보장이 따라오는 것은 아니다. 기밀성과 무결성은 별개의 문제이며, 무결성을 따로 챙기지 않으면 공격자는 내용을 읽지 못한 채로도 메시지를 자기 뜻대로 조작할 수 있다.
그렇다면 필요한 것은 분명하다. 암호문이 전송 도중 단 한 비트도 바뀌지 않았음을 받는 쪽이 확인할 수 있는 일종의 '봉인'이다. 이 봉인을 인증 태그(authentication tag)라 부르고, 카운터 모드에 이 태그 계산을 결합한 것이 바로 갈루아/카운터 모드, GCM이다.
GCM의 목표를 한 문장으로 줄이면 이렇다. 카운터 모드로 암호화하면서, 동시에 암호문 전체를 요약하는 작은 봉인 값(태그)을 하나 만들어 함께 보낸다. 받는 쪽은 같은 절차로 태그를 다시 계산해, 자기가 계산한 태그가 받은 태그와 일치하는지 확인한다. 한 비트라도 어긋나면 메시지를 거부한다. 실제 암호 라이브러리는 이때 아예 예외를 던지며 복호화 결과를 내주지 않는다.
인증 태그는 봉랍 도장이 찍힌 편지 봉투와 같다. 봉투를 뜯지 않고는 안을 볼 수 없고(기밀성), 누군가 뜯었다면 도장이 깨져 흔적이 남는다(무결성). 결정적인 점은 그 도장을 원본 인장을 가진 사람만 다시 찍을 수 있다는 것이다. 공격자가 봉투 안 내용을 바꾼 뒤 똑같은 봉랍 도장을 위조해 다시 찍을 수 없어야, 비로소 봉인이 의미를 갖는다.
그렇다면 무엇을 봉인해야 할까? 평문은 받는 쪽으로 보내지 않으므로 봉인 대상이 아니다. 대신 실제로 전송되는 암호문을 인증한다. 함께 난스도 누군가 손대지 못하게 보호하고, 메시지의 길이까지 인증한다. 길이를 봉인해야 공격자가 블록을 슬쩍 빼거나 끼워 넣는 장난을 막을 수 있다.
태그 계산의 출발점은 H라 불리는 해시 서브키다. 만드는 법은 의외로 단순하다. 128비트를 전부 0으로 채운 블록을 암호화 키 K로 AES 암호화한 결과가 곧 H다.
0을 암호화했는데 무슨 의미가 있느냐고 물을 수 있다. 핵심은 AES가 매우 뛰어나서, 0을 넣어도 출력은 0과 전혀 닮지 않은 무작위처럼 보이는 비트열이 나온다는 데 있다. 그리고 이 H는 키 K에서만 결정론적으로 나오는 비밀값이다. 키를 모르면 H가 무엇인지 알 수 없다. 이 '비밀'이 봉인을 위조 불가능하게 만드는 열쇠다. 잠시 뒤에 보겠지만, 공격자가 H를 알아내는 순간 GCM의 무결성은 통째로 무너진다.
이제 H를 가지고 태그를 만든다. 그 절차를 GHASH라 부른다. 원리는 반복적이다. 누적값을 0에서 시작해, 봉인할 블록을 하나씩 가져와 누적값에 XOR한 뒤 H를 곱한다. 이 'XOR하고 H 곱하기'를 모든 블록에 대해 되풀이한다.
여기서 '곱하기'는 우리가 아는 정수 곱셈이 아니다. 128비트 두 수를 곱하는, 갈루아 체(Galois field) GF(2¹²⁸) 위에서의 특수한 곱셈이다. 이름이 거창하지만 발상은 어렵지 않다.
시계 산수를 떠올려 보자. 시계에서 10시에 5시간을 더하면 15시가 아니라 3시다. 12를 넘으면 12로 나눈 나머지로 되돌아온다(모듈러 연산). 갈루아 체의 곱셈도 이와 닮았다. 비트열을 다항식처럼 취급해 곱한 뒤, 정해진 기준 다항식으로 나눈 나머지만 남긴다. GCM이 쓰는 기준은 x¹²⁸ + x⁷ + x² + x + 1이라는 다항식이다. 덕분에 128비트끼리 곱해도 결과는 다시 128비트 안에 깔끔히 들어맞는다. 사실 AES 내부에서 쓰는 산수의 더 큰 형제뻘이다.
봉인하는 순서는 정해져 있다. 먼저 부가 인증 데이터(잠시 뒤 설명한다)를, 그다음 암호문 블록들을, 마지막으로 길이 블록을 차례로 누적한다. 모든 블록을 빨아들인 뒤, 앞서 만든 또 다른 마스크 값과 한 번 더 XOR하면 최종 태그가 완성된다. 그 마스크는 카운터의 첫 값을 암호화한 EK(카운터₀)인데, 이것이 비밀 H가 식 밖으로 직접 새어 나가지 않도록 한 겹 더 가려 준다.
받는 쪽의 절차는 보내는 쪽의 거울상이다. 암호문을 키스트림과 XOR해 평문을 복원하는 동시에, 같은 GHASH 계산을 돌려 태그를 다시 만든다. 그리고 사용자에게 평문을 돌려주기 직전에 묻는다. "내가 방금 계산한 태그가 함께 받은 태그와 일치하는가?" 일치하지 않으면 무언가 잘못된 것이다. 단순한 전송 오류일 수도, 악의적인 변조일 수도 있다. 어느 쪽이든 그 메시지는 폐기된다.
이 검증이 왜 위조에 강한지 정리해 보자. 공격자는 암호문도 알고 난스도 안다(난스는 비밀이 아니다). 비트를 몇 개 뒤집은 뒤 그에 맞는 새 태그를 만들어 끼워 넣고 싶을 것이다. 그러려면 H를 알아야 한다. 그런데 H는 0블록을 비밀 키로 암호화한 값이다. 키를 모르는 공격자가 H를 알아내기란 극히 어렵다. H를 모르면 변조된 암호문에 들어맞는 태그를 다시 계산할 수 없고, 따라서 위조는 검증 단계에서 걸린다.
키를 바꾸면 H가 달라져 모든 값이 어긋난다. 암호문을 도중에 한 비트라도 바꾸면 GHASH 누적 합이 달라져 태그가 어긋난다. 난스를 바꿔도 마찬가지다. 그래서 복호화 측은 "읽을 수 없게 가려졌고, 동시에 손대지 않았음"을 한 번에 확신할 수 있다.
현실의 통신에는 묘한 데이터가 있다. 가려서는 안 되지만, 바뀌어서도 안 되는 정보다. 대표적인 예가 네트워크 패킷의 헤더다. 헤더에는 이 패킷을 어디로 보낼지가 적혀 있어 암호화하면 배달 자체가 불가능하다. 그러나 공격자가 헤더를 위조해 엉뚱한 곳으로 돌리거나 내용을 바꾸는 것은 막아야 한다. 데이터베이스에서 기본 키는 검색을 위해 그대로 두되 변조는 막고 싶은 경우도 같은 상황이다.
GCM은 이런 요구를 깔끔하게 처리한다. 이런 데이터를 부가 인증 데이터(AAD, Additional Authenticated Data)라 부르고, 암호화는 하지 않은 채 GHASH 계산에는 똑같이 집어넣어 H를 곱한다. 그 결과 AAD도 태그의 보호 아래 들어온다. 이렇게 '암호화된 부분 + 암호화는 안 했지만 인증된 부분'을 함께 다루는 방식을 연관 데이터를 포함한 인증 암호화(AEAD, Authenticated Encryption with Associated Data)라 한다. 앞의 GHASH 그림에서 맨 처음 누적되던 초록색 블록이 바로 이 AAD다.
택배 상자를 떠올리자. 상자 안의 물건은 포장해 가린다(암호화). 그러나 겉면의 주소 라벨은 배달원이 읽어야 하므로 가릴 수 없다(AAD). 대신 상자 전체에 위조 불가능한 봉인 스티커를 한 장 붙여, 안의 물건이든 겉의 주소든 누가 손대면 스티커가 찢어지게 한다(인증 태그). 주소는 공개하되 바꿔치기는 막는 것, 그것이 AEAD다.
앞에서 미뤄 둔 철칙으로 돌아오자. 같은 키와 같은 난스를 두 번 쓰면 안 된다고 했다. 이를 어기면 단지 기밀성만 깨지는 게 아니라, 무결성까지 통째로 무너진다.
난스가 같으면 두 메시지의 키스트림이 동일해진다. 두 암호문을 서로 XOR하면 키스트림이 상쇄되어 사라지고, 평문끼리의 관계가 드러난다. 더 나아가, 공격자는 이 상황에서 태그를 둘러싼 방정식을 풀 수 있게 된다. 본질적으로 다항식의 근을 찾는 표준 알고리즘으로 비밀값 H를 복원해 버린다. H가 손에 들어오면 그때부터는 길이를 바꾸든 내용을 바꾸든, 어떤 변조에도 들어맞는 태그를 마음대로 만들어 낼 수 있다. 봉인을 위조할 인장을 통째로 빼앗기는 셈이다.
난스 재사용은 이론적 위험이 아니라 실제 구현에서 반복적으로 터지는 사고다. 난스를 무작위로 뽑되 충분히 길게(보통 96비트) 관리하거나, 메시지마다 증가하는 카운터로 엄격히 다루어야 한다. 한 사람이 메시지를 변조하는 것보다 더 나쁜 일은, 변조된 사실을 받는 쪽이 영영 알아채지 못하는 것이다.
태그는 결국 128개의 0과 1, 즉 128비트짜리 값이다. 트래픽을 아끼려고 96비트 정도로 잘라 쓰기도 한다. 다만 너무 짧게 자르면 위험하다. 극단적으로 태그를 1비트만 쓴다면, 변조된 메시지에 우연히 들어맞는 태그를 만들 확률이 절반이나 된다. 인터넷 트래픽에서 몇 바이트는 그리 비싼 자원이 아니므로, 특별한 이유가 없다면 128비트를 온전히 쓰는 편이 안전하다.
현대적으로 널리 쓰이는 또 하나의 AEAD 방식은 ChaCha20-Poly1305다. ChaCha20이라는 스트림 암호와 Poly1305라는 메시지 인증 코드(MAC, Message Authentication Code)를 묶은 것으로, GCM과 발상은 같다. 데이터를 가리는 부분과, 끝에 붙여 변조 여부를 검증하는 태그를 함께 만든다. ChaCha20-Poly1305는 AES 전용 하드웨어 가속이 없는 기기, 가령 일부 저가형 모바일 칩에서 GCM보다 빠른 경우가 많아, 그런 환경에서 선호된다.
전송 계층 보안(TLS, Transport Layer Security)의 최신 버전인 TLS 1.3은 이 흐름을 제도적으로 못 박았다. 과거 버전에서 여러 공격의 빌미가 되었던 옛 방식들을 모두 걷어 내고, 오직 AEAD 방식만 허용한다. AES-GCM과 ChaCha20-Poly1305가 그 중심이며(제약이 큰 기기를 위한 변형이 일부 더 있다), 그 덕분에 TLS 1.3에서는 안전하지 않은 조합을 고르는 것 자체가 거의 불가능해졌다. 브라우저의 보안 탭에서 보이던 그 문자열은, 이 글을 읽는 동안에도 수없이 같은 봉인을 찍고 풀어 온 흔적이다.
정리하면 이렇다. 카운터 모드는 AES를 빠른 스트림 암호로 바꿔 내용을 가린다. 그러나 그것만으로는 누가 중간에서 손댔는지 알 수 없다. GCM은 여기에 갈루아 체 곱셈으로 빚어낸 위조 불가능한 봉인, 인증 태그를 더해 두 가지를 한 번에 해결한다. 가려져 읽을 수 없고, 동시에 손대면 들통난다. 우리가 매 순간 웹을 신뢰할 수 있는 작지만 단단한 토대가, 바로 이 봉인 한 장이다.