메시지 인증 코드
고친 사람 github-actions[bot]
메시지 인증 코드는 받은 데이터가 오는 길에 바뀌지 않았는지 확인하게 해 줍니다. 약속된 상대가 보낸 것인지도 같이 가려 줍니다. 보내는 쪽은 둘만 아는 비밀 키로 짧은 값을 계산해 데이터에 붙입니다. 받는 쪽은 같은 계산을 다시 해서 두 값이 같은지 확인합니다.
쉽고 빠른 이해
메시지 인증 코드는 데이터에 붙여 보내는 짧은 확인값입니다. GitHub 가 그런 예입니다. 등록된 서버로 알림 요청을 보낼 때 이 값을 헤더에 실어 보냅니다. 받는 서버는 같은 값이 나오는지 보고 남이 흉내 낸 요청을 걸러 냅니다.
이 값이 없으면 받는 쪽은 내용이 오는 길에 바뀌었는지 알 수 없습니다. 키 없는 해시값을 붙여도 소용이 없습니다. 중간에서 내용을 고친 쪽이 해시값도 다시 계산해 붙이면 그만입니다.
어떻게 도나:
- 두 쪽이 같은 비밀 키를 미리 나눠 갖습니다
- 보내는 쪽이 데이터와 키로 확인값을 계산해 같이 보냅니다
- 받는 쪽이 같은 계산을 해 보고 값이 다르면 데이터를 버립니다
대가는 키를 미리 나눠 가져야 한다는 것입니다. 그 키를 지키는 일도 떠맡습니다. 키를 가진 두 쪽이 같은 값을 만들 수 있어서, 제삼자 앞에서 누가 보냈는지는 못 가립니다. 내용을 감춰 주지도 않습니다.
상세
편지를 주고받는 두 사람이 똑같은 계산표를 한 장씩 나눠 가졌다고 해 봅시다. 편지를 부칠 때마다 내용을 그 표에 넣어 나온 짧은 숫자를 편지 끝에 적습니다. 받는 쪽은 자기 표로 같은 숫자가 나오는지 확인합니다. 표가 없는 사람은 편지를 고쳐도 맞는 숫자를 적을 수 없습니다.
메시지 인증 코드(MAC, Message Authentication Code)는 비밀 키와 데이터로 짧은 값을 계산하는 방법입니다. 앞의 계산표가 비밀 키입니다. 편지 끝에 적은 숫자가 계산해 나온 값입니다. 데이터가 길든 짧든 나오는 값의 길이는 정해져 있습니다.
계산해 붙이는 이 값이 태그(tag)입니다. 인증 태그라고도 합니다. 앞에서 확인값이라고 한 것이 이 태그입니다. 다만 「메시지 인증 코드」라는 이름은 계산 방법을 가리킬 때도, 나온 값을 가리킬 때도 씁니다. 이 문서는 방법을 메시지 인증 코드로, 값을 태그로 부릅니다.
한 번의 전송
A 가 B 에게 메시지를 보내는 경우를 따라가 봅니다. 두 쪽은 같은 비밀 키를 이미 나눠 가졌습니다. 보내는 쪽과 받는 쪽이 같은 키를 쓰므로 이런 키가 대칭 키입니다.
sequenceDiagram
participant A as 보내는 쪽
participant B as 받는 쪽
A->>A: 데이터와 비밀 키로 태그 계산
A->>B: 데이터 + 태그
B->>B: 받은 데이터와 비밀 키로 태그를 다시 계산
B->>B: 받은 태그와 비교
Note over B: 같을 때만 오는 길에 안 바뀐 것으로 친다
B 는 A 에게 되물을 필요가 없습니다. 손에 든 데이터와 자기 키만 있으면 판정이 끝납니다. 두 값이 다르면 데이터를 버립니다.
무결성과 인증
태그가 맞으면 받는 쪽은 두 가지를 한꺼번에 확인합니다. 하나는 내용이 오는 길에 바뀌지 않았다는 것입니다. 이것이 무결성입니다. 전선 잡음 같은 우연한 손상도, 누가 일부러 고친 변조도 같이 걸립니다.
다른 하나는 키를 가진 쪽이 만들었다는 것입니다. 이것이 데이터 출처 인증입니다. 줄여서 인증이라고 합니다. 둘을 따로 얻으려고 다른 장치를 덧붙이지 않아도 됩니다.
인증은 「키를 나눠 가진 무리 가운데 누군가가 만들었다」까지입니다. 그 무리 안의 누구인지는 가리지 않습니다. 둘이서만 키를 나눠 가졌다면 상대가 보낸 것으로 믿을 수 있습니다.
위조와 태그
이 방법이 막으려는 것은 위조입니다. 위조는 키가 없는 공격자가 정당한 쪽이 보낸 적 없는 메시지와 그 메시지에 맞는 태그를 지어내는 일입니다. 키 없이는 이 짝을 맞추기 어려워야 메시지 인증 코드로 쓸 수 있습니다.
태그는 데이터를 감추지 않습니다. 내용은 읽을 수 있는 채로 실려 갑니다. 그 옆에 짧은 값이 하나 더 붙을 뿐입니다. 감추는 일은 암호화가 맡습니다.
배경
데이터가 바뀌었는지 보는 오래된 방법은 체크섬이나 순환 중복 검사(CRC, Cyclic Redundancy Check) 같은 오류 검출 부호입니다. 길이가 제각각인 데이터에서 정해진 길이의 값을 내는 해시 함수도 같은 식으로 씁니다. 셋 다 보내는 쪽이 데이터에서 짧은 값을 계산해 붙입니다. 받는 쪽은 다시 계산해 비교합니다. 문제는 이 계산들이 키를 쓰지 않는 공개 함수라는 것입니다.
누구나 같은 계산을 할 수 있어서 우연한 손상만 잡힙니다. 중간에서 데이터를 고친 쪽이 값도 다시 계산해 붙이면 받는 쪽은 알아채지 못합니다.
sequenceDiagram
participant A as 보내는 쪽
participant M as 중간에서 고친 쪽
participant B as 받는 쪽
A->>M: 데이터 + 해시값
M->>M: 데이터를 고친다
M->>M: 고친 데이터로 해시값을 다시 계산
M->>B: 고친 데이터 + 다시 계산한 해시값
B->>B: 다시 계산해 보니 앞뒤가 맞아 통과한다
막으려면 계산에 두 쪽만 아는 비밀을 섞어야 합니다. 비밀 키가 섞이면 가운데에 선 쪽은 고친 데이터에 맞는 값을 만들어 내지 못합니다. 그런데 해시 함수는 처음부터 인증용으로 만든 것이 아니라서 키를 넣는 입력이 따로 없습니다. 키를 데이터 앞이나 뒤에 그냥 이어 붙이는 즉석 구성이 여럿 나왔지만 안전성을 따진 탄탄한 분석이 없었습니다. 뒤에 뚫리는 길이 드러났습니다(아래 「갈래」의 HMAC).
비밀 키에 기댄 이런 무결성 검사 장치를 흔히 메시지 인증 코드라고 부릅니다. 무결성 검사값(integrity check value)이나 암호학적 체크섬(cryptographic checksum)이라는 이름도 씁니다. 초기에는 블록 암호로 만드는 것이 가장 흔했습니다. 블록 암호는 정해진 길이의 덩어리 단위로 데이터를 암호화하는 방식입니다. 1985년 미국 연방 기관용 컴퓨터 데이터 인증 표준이 블록 암호 DES(Data Encryption Standard, 데이터 암호화 표준)를 쓰는 인증 알고리즘을 정한 것이 그런 예입니다.
예시
메시지 인증 코드가 실제 시스템에 박혀 있는 네 군데를 봅니다. 웹 서비스가 보내는 알림 요청 둘, 네트워크 프로토콜 하나, 토큰 형식 하나입니다.
GitHub 웹훅의 X-Hub-Signature-256
웹훅은 어떤 일이 벌어졌을 때 서비스가 미리 등록해 둔 주소로 HTTP(HyperText Transfer Protocol) 요청을 보내 알려 주는 방식입니다. GitHub 는 웹훅을 보낼 때마다 X-Hub-Signature-256 헤더에 태그를 실어 보냅니다. 받는 서버는 이 태그를 확인해서 GitHub 가 보낸 요청인지, 오는 길에 고쳐지지 않았는지를 가립니다.
태그는 HMAC(Hash-based Message Authentication Code, 해시 기반 메시지 인증 코드)으로 계산합니다. HMAC 은 해시 함수로 태그를 만드는 메시지 인증 코드입니다(아래 「갈래」). 쓰는 해시 함수는 SHA-256(Secure Hash Algorithm 256, 256비트 값을 내는 해시 함수)입니다.
키는 웹훅에 설정한 비밀 토큰입니다. 계산 대상은 요청 본문입니다. 값은 언제나 sha256= 으로 시작합니다. 그 뒤에 16진수 결과가 붙습니다. GitHub 문서가 검증 시험용으로 내놓은 값은 이렇습니다.
secret: It's a Secret to Everybody
payload: Hello, World!
X-Hub-Signature-256: sha256=757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17
GitHub 는 받는 쪽에 두 가지를 당부합니다. 하나는 태그를 비교할 때 == 연산자를 쓰지 말라는 것입니다. 대신 상수 시간 비교를 쓰라고 합니다. 상수 시간 비교는 다른 값을 만나도 중간에 멈추지 않고 끝까지 비교하는 함수입니다. 걸린 시간의 차이를 단서로 삼는 타이밍 공격을 누그러뜨리는 데 도움이 됩니다(아래 「보장과 가정」).
다른 하나는 검증 전에 본문이나 헤더가 바뀌지 않게 하라는 것입니다. 프록시나 로드 밸런서가 본문을 고치면 태그가 안 맞습니다. SHA-1 을 쓰는 옛 헤더 X-Hub-Signature 도 오지만 예전 연동을 위해 남겨 둔 것입니다.
Stripe 웹훅의 Stripe-Signature
결제 서비스 Stripe 도 웹훅에 HMAC 을 씁니다. 쓰는 해시 함수는 SHA-256 입니다. 다른 점은 태그를 계산할 때 보낸 시각을 넣는다는 것입니다.
Stripe-Signature 헤더에는 t= 로 시작하는 시각과 v 로 시작하는 서명이 실립니다. 서명 방식은 여럿 있을 수 있습니다. 지금 운영에서 쓸 수 있는 방식은 v1 하나입니다. 아래 예에 같이 실린 v0 은 그 하나에 들지 않아 검증에 쓰지 않습니다.
Stripe-Signature:
t=1492774577,
v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd,
v0=6ffbb59b2300aae63f272406069a9788598b792a944a07aba816edb039989a39
계산 대상은 세 조각을 이어 붙인 문자열입니다. 시각 문자열, 마침표 하나, 요청 본문 순서입니다. 키는 수신 주소마다 받는 서명 비밀입니다. 받는 쪽은 아래 순서로 확인합니다.
flowchart TD
A["t 값 + 마침표 + 요청 본문을 이어 붙인다"] --> B["서명 비밀로 HMAC-SHA256 을 계산한다"]
B --> C{"계산한 값이 v1 값과 같은가"}
C -->|다르다| X["버린다"]
C -->|같다| D{"t 값이 지금 시각에서 허용 범위 안인가"}
D -->|벗어났다| X
D -->|안이다| OK["받아들인다"]
시각을 따로 확인하는 까닭은 재전송 공격입니다. 재전송 공격은 정당한 요청과 그 태그를 가로챈 뒤 그대로 다시 보내는 공격입니다. 시각이 태그 계산에 들어가 있어서 공격자가 시각만 바꾸면 태그가 깨집니다.
그러니 태그가 맞아도 시각이 너무 오래된 요청은 버리면 됩니다. Stripe 라이브러리는 허용 범위를 기본 5분으로 둡니다. 태그를 비교할 때는 Stripe 도 상수 시간 비교를 쓰라고 합니다.
TLS 1.2 레코드의 MAC
TLS(Transport Layer Security, 전송 계층 보안)는 네트워크 통신을 보호하는 프로토콜입니다. TLS 1.2 는 주고받는 데이터를 레코드라는 단위로 잘라 보냅니다. 스트림 암호로 레코드를 보호할 때 레코드마다 붙는 태그의 계산식은 이렇습니다. 「+」는 이어 붙인다는 뜻입니다.
MAC(MAC_write_key, seq_num +
TLSCompressed.type +
TLSCompressed.version +
TLSCompressed.length +
TLSCompressed.fragment);
계산에 들어가는 것은 MAC 전용 키, 레코드 순서 번호(seq_num), 레코드 종류·버전·길이, 그리고 데이터 조각입니다. 순서 번호는 연결 상태가 생길 때 0에서 시작합니다. 레코드 하나를 보낼 때마다 하나씩 올라갑니다. 이 번호가 태그에 섞여 있어서 레코드를 지우거나 순서를 바꾸면 들킵니다.
레코드 계층 위에는 그것을 쓰는 쪽이 여럿 있습니다. 레코드 종류를 태그 계산에 넣으면 한쪽에 갈 레코드를 다른 쪽으로 돌려보낼 수 없습니다.
보내는 방향과 받는 방향은 서로 다른 MAC 키를 씁니다. 그러니 한쪽이 보낸 메시지를 반대쪽 흐름에 끼워 넣을 수 없습니다. 이 MAC 키가 새면 메시지를 고치는 공격이 가능해질 수 있습니다.
TLS 1.3 은 모든 암호를 AEAD(Authenticated Encryption with Associated Data, 연관 데이터 인증 암호화)로 다룹니다. 연관 데이터는 감추지 않고 인증만 하는 데이터입니다. 암호화와 인증을 한 번의 연산으로 묶은 방식입니다. 이런 방식이 인증 암호입니다. 이전 판에서는 모든 암호가 그렇지는 않았습니다.
JWS 의 HS256
JWS(JSON Web Signature)는 JSON(JavaScript Object Notation) 데이터에 서명이나 태그를 붙이는 형식입니다. "alg" 값이 HS256 이면 HMAC 과 SHA-256 으로 태그를 계산한다는 뜻입니다.
"alg" 값 |
계산 |
|---|---|
HS256 |
SHA-256 을 쓰는 HMAC |
HS384 |
SHA-384 를 쓰는 HMAC |
HS512 |
SHA-512 를 쓰는 HMAC |
계산 대상은 JWS 가 서명 입력으로 정한 문자열입니다. 무엇을 어떻게 이어 붙여 그 문자열을 만드는지는 JWS 형식이 정합니다. 키는 두 쪽이 나눠 가진 키입니다. 나온 HMAC 값이 그대로 JWS 의 서명 칸에 들어갑니다. 이름은 서명이지만 계산은 메시지 인증 코드입니다.
이 값은 MAC 키를 가진 쪽이 만들었다는 것을 보여 줍니다. 키는 해시 출력과 같은 길이 이상이어야 합니다. HS256 이면 256비트 이상입니다. 계산한 값과 받은 값을 비교하는 일도 타이밍 공격을 막도록 상수 시간으로 해야 합니다.
갈래
메시지 인증 코드는 무엇을 재료로 태그를 만드나로 갈립니다. 재료가 다르면 키를 쓰는 방식과 깨지는 조건이 다릅니다. 다섯 갈래를 하나씩 짚습니다. 끝에 표로 모읍니다.
HMAC
HMAC 은 해시 함수를 고치지 않고 그대로 가져다 태그를 만듭니다. 해시 함수가 더 빠르거나 안전한 것으로 바뀌면 그것만 갈아 끼우면 됩니다.
해시 함수는 데이터를 블록 단위로 차례로 읽습니다. 블록을 하나 읽을 때마다 중간값을 다음 블록으로 넘깁니다. 마지막 중간값이 해시값입니다.
키를 데이터 앞에 그냥 붙이는 구성은 이 성질에 걸립니다. 「키 + 데이터 x」의 태그를 아는 공격자는 그 값을 중간값으로 되넣어 이어 계산할 수 있습니다. x 뒤에 무엇을 덧붙인 데이터의 태그를 키 없이 얻는 것입니다. 이것이 길이 확장 공격입니다.
키를 뒤에 붙이는 구성은 다른 길로 뚫립니다. 해시값이 같은 두 데이터를 찾아 두면 키가 무엇이든 둘의 태그도 같아집니다.
HMAC 은 두 길을 다 막으려고 해시를 두 겹으로 감쌉니다. 키 K 를 정해진 바이트와 XOR(exclusive OR, 배타적 논리합)합니다. 안쪽에 쓰는 바이트는 0x36 을 블록 길이만큼 되풀이한 ipad 입니다. 바깥쪽은 0x5C 를 그만큼 되풀이한 opad 입니다. 그렇게 만든 두 키를 안쪽 해시와 바깥쪽 해시에 각각 섞습니다.
H(K XOR opad, H(K XOR ipad, text))
식에서 H 는 해시 함수, text 는 태그를 붙일 데이터입니다. 쉼표는 앞뒤를 이어 붙인다는 뜻입니다. 키 길이는 해시 출력 길이 이상을 권합니다. 바깥쪽 해시가 안쪽 해시의 결과를 덮어 가려서, 공격자는 이어 계산할 중간값을 손에 넣지 못합니다.
flowchart TD
subgraph s1["키를 앞에 붙이기 — 이어 계산으로 뚫린다"]
A1["비밀 키 + 데이터 x"] --> A2["블록마다 중간값을 넘긴다"]
A2 --> A3["마지막 중간값 = x 의 태그"]
A3 --> A4["공격자가 그 태그를 중간값으로 되넣는다"]
A4 --> A5["덧붙인 y 를 이어 읽는다"]
A5 --> A6["키 없이 x + y 의 태그가 나온다"]
end
subgraph s2["HMAC — 두 겹"]
B1["안쪽 키(K XOR ipad) + 데이터"] --> B2["안쪽 해시"]
B2 --> B3["바깥쪽 키(K XOR opad) + 안쪽 결과"]
B3 --> B4["바깥쪽 해시"] --> B5["태그"]
end
CMAC
CMAC(Cipher-based Message Authentication Code, 블록 암호 기반 메시지 인증 코드)은 AES(Advanced Encryption Standard, 고급 암호화 표준) 같은 블록 암호로 태그를 만듭니다. 바탕은 CBC-MAC(Cipher Block Chaining MAC, 암호 블록 연쇄 메시지 인증 코드)입니다. 데이터를 블록으로 잘라 차례로 암호화합니다. 블록마다 앞 블록의 암호화 결과를 섞은 뒤 암호화합니다. 마지막 결과가 태그입니다.
기본 CBC-MAC 에는 보안 결함이 있습니다. 그 결함을 고친 변형이 XCBC(eXtended Cipher Block Chaining)입니다. XCBC 의 키 크기를 줄인 것이 OMAC1(One-Key CBC MAC1)입니다. CMAC 은 OMAC1 과 같습니다.
flowchart TD
A["CBC-MAC · 기본"] -->|보안 결함을 고침| B["XCBC"]
B -->|키 크기를 줄임| C["OMAC1"]
C -->|같은 것| D["CMAC"]
이름이 넷이지만 기본 CBC-MAC 에서 두 번 고친 것이 CMAC 입니다. 해시 함수보다 AES 를 쓰기가 더 쉬운 시스템에 맞습니다.
GMAC
GMAC(Galois Message Authentication Code, 갈루아 메시지 인증 코드)은 블록 암호의 운영 방식 가운데 하나인 GCM(Galois/Counter Mode, 갈루아/카운터 모드)에서 나왔습니다. 운영 방식은 정해진 길이의 덩어리만 다루는 블록 암호로 긴 데이터를 처리하려고 덩어리를 엮는 절차를 정한 것입니다.
GCM 은 암호화와 인증을 한꺼번에 하는 방식입니다. 여기에 암호화할 데이터 없이 인증할 데이터만 넣으면 남는 것은 태그를 만드는 일과 확인하는 일뿐입니다. 이것이 GMAC 입니다.
GCM 과 GMAC 은 초기화 벡터(IV, Initialization Vector)를 한 키로 두 번 쓰면 안 됩니다. 초기화 벡터는 호출마다 새로 넣는 값입니다. 한 번이라도 되풀이되면 공격자가 해시 부분 키(hash subkey)를 알아낼 가능성이 높습니다. 해시 부분 키는 GCM 이 내부에서 쓰는 보조 키입니다. 그 값을 손에 넣으면 위조한 암호문을 쉽게 지어낼 수 있습니다(아래 「보장과 가정」).
Poly1305
Poly1305 의 재료는 메시지와 32바이트 일회용 키입니다. 둘을 넣으면 16바이트 태그가 나옵니다. 이름 그대로 한 번만 쓰는 인증기(one-time authenticator)라서 키를 메시지 하나에만 씁니다. 일회용 키는 호출마다 겹치지 않도록 권합니다. 공격자가 미리 알 수 없어야 한다는 요구도 붙습니다.
일회용 키는 의사 난수로 만들어도 됩니다. 의사 난수는 무작위처럼 보이도록 계산해 낸 값입니다. 그 방법으로 스트림 암호 ChaCha20 에 키와 논스를 넣어 일회용 키를 얻는 길이 정해져 있습니다. 스트림 암호는 키로 만들어 낸 바이트 흐름을 데이터에 겹쳐 암호화하는 방식입니다. 논스는 한 번만 쓰고 버리는 값입니다.
flowchart TD
K["오래 쓰는 ChaCha20 키"] --> C["ChaCha20"]
N["호출마다 다른 논스"] --> C
C --> OT["32바이트 일회용 키"]
M["메시지"] --> P["Poly1305"]
OT --> P
P --> T["16바이트 태그"]
이 길을 타면 키가 두 겹입니다. 오래 쓰는 쪽은 ChaCha20 키입니다. 호출마다 새로 나오는 쪽이 Poly1305 의 일회용 키입니다.
같은 ChaCha20 키 아래에서 논스는 호출마다 달라야 합니다. 무작위로 뽑으면 겹칠 수 있으므로 무작위로 만들면 안 됩니다. 카운터를 쓰는 것이 알맞은 방법입니다.
KMAC
KMAC(Keccak Message Authentication Code, Keccak 메시지 인증 코드)은 SHA-3 의 바탕인 Keccak 으로 태그를 만듭니다. KMAC128 과 KMAC256 두 가지가 있습니다.
계산은 키를 정해진 길이로 채워 데이터 앞에 붙이는 것부터 시작합니다. 그 뒤에 원하는 출력 길이를 적어 붙입니다. 이렇게 이은 것을 Keccak 바탕의 함수에 넣습니다. 그 함수에는 출력 길이와 「KMAC」이라는 이름, 쓰는 쪽이 붙일 수 있는 구분 문자열도 같이 들어갑니다.
HMAC 이 피한 「키를 앞에 붙이기」를 KMAC 은 그냥 씁니다. 바탕인 Keccak 에는 길이 확장 공격이 통하지 않기 때문입니다. 출력 길이는 마음대로 정할 수 있습니다. 길이를 바꾸면 앞의 결과와 아무 관계 없는 새 값이 나옵니다.
다섯 갈래를 한 표로 모으면 이렇습니다.
| 갈래 | 재료 | 키를 쓰는 방식 | 지켜야 할 조건 |
|---|---|---|---|
| HMAC | 해시 함수 | 한 키를 오래 쓴다 | 해시를 두 겹으로 감싼다 |
| CMAC | 블록 암호 | 한 키를 오래 쓴다 | 기본 CBC-MAC 의 결함을 고친 변형을 쓴다 |
| GMAC | 블록 암호의 GCM 모드 | 키는 오래, 초기화 벡터는 한 번만 | 초기화 벡터를 다시 쓰지 않는다 |
| Poly1305 | 메시지와 32바이트 일회용 키 | 메시지마다 새 일회용 키 | 같은 일회용 키를 두 번 쓰지 않는다 |
| KMAC | SHA-3 의 바탕인 Keccak | 키를 앞에 붙인다 | 출력 길이를 너무 짧게 잡지 않는다 |
보장과 가정
메시지 인증 코드가 약속하는 것과 그 약속이 기대는 가정을 짝지어 봅니다. 약속은 둘, 주지 않는 것은 셋, 가정은 넷입니다. 가정이 깨지면 약속도 무너집니다.
주는 것과 주지 않는 것
주는 것은 앞서 말한 무결성과 데이터 출처 인증입니다. 받은 태그가 맞으면 내용이 안 바뀐 것입니다. 키를 나눠 가진 무리 가운데 누군가가 만들었다는 것까지 확인됩니다. 주지 않는 것은 셋입니다.
| 주지 않는 것 | 왜 못 주나 | 같이 쓰는 수단 |
|---|---|---|
| 기밀성 | 내용을 감추지 않고 태그만 덧붙인다 | 암호화 · 인증 암호 |
| 부인 방지 | 키를 가진 두 쪽이 같은 태그를 만들 수 있다 | 전자 서명 |
| 재전송 막기 | 예전 데이터와 태그를 그대로 다시 보내도 검증을 통과한다 | 순서 번호 · 타임스탬프 · 논스 |
부인 방지는 보낸 쪽이 나중에 「내가 안 보냈다」고 잡아뗄 수 없게 하는 성질입니다. 키를 나눠 가진 두 쪽은 서로 똑같은 태그를 만들 수 있습니다. 그러니 둘 사이에서는 상대가 보냈다고 믿을 수 있지만, 제삼자에게 저쪽이 보냈다고 증명하지는 못합니다.
재전송을 막으려면 되풀이되면 안 되는 값을 태그 계산에 넣습니다. TLS 1.2 의 레코드 순서 번호와 Stripe 의 시각이 그런 값입니다.
IPsec(Internet Protocol Security, 인터넷 프로토콜 보안)의 ESP(Encapsulating Security Payload, 캡슐화 보안 페이로드)도 패킷마다 순서 번호를 답니다. 받는 쪽이 재전송 검사를 켜 두면 이미 받은 번호가 다시 오는지 확인합니다. 무결성 보호를 켜지 않았으면 이 검사를 켜면 안 됩니다. 태그가 순서 번호를 덮지 않으면 공격자가 번호를 마음대로 바꿀 수 있기 때문입니다. 재전송 막기는 메시지 인증 코드 위에 얹혀야 서는 기능입니다.
네 가지 가정
| 가정 | 깨지면 |
|---|---|
| 키가 나눠 가진 무리 밖으로 새지 않는다 | 키를 손에 넣은 쪽이 무리의 누구로든 행세해 맞는 태그를 만든다 |
| 태그가 충분히 길다 | 아무 값이나 찍어 보내도 맞을 확률이 커진다 |
| 태그 비교가 시간을 흘리지 않는다 | 걸린 시간을 단서로 태그를 맞춰 나갈 수 있다 |
| GMAC 은 초기화 벡터를, Poly1305 는 일회용 키를 다시 쓰지 않는다 | GMAC 은 해시 부분 키가 드러나 위조가 쉬워진다. Poly1305 는 두 메시지가 같은 일회용 키를 쓰게 되어 일회용이라는 전제가 깨진다 |
첫 가정은 키를 비밀로 지키는 것입니다. 키를 아는 공격자는 키를 나눠 가진 누구로든 행세할 수 있습니다. 그렇게 만든 태그는 검증을 통과합니다.
태그 길이
태그를 무작위로 찍어 맞힐 확률은 태그 길이에 달려 있습니다. 길이가 Tlen 비트인 태그를 무작위로 하나 고르면 맞을 확률은 2의 Tlen 제곱 분의 1 입니다. 길수록 찍기에 강합니다. 대가는 태그가 차지하는 전송·저장 공간이 늘어난다는 것입니다.
시스템은 키 하나에 대해 검증 실패를 몇 번까지 허용할지 제한하도록 권합니다. 검증을 통과했다고 진짜임이 완전히 보장되지도 않습니다. 키를 모르는 공격자가 통과하는 짝을 내밀 확률이 조금은 남습니다. 그 확률은 태그 길이와 허용한 실패 횟수가 정합니다.
갈래마다 길이 요구가 다릅니다. 강도도 다르므로 섞어 읽지 않습니다.
| 갈래 | 요구 | 강도 |
|---|---|---|
| HMAC 을 잘라 쓸 때 | 해시 출력의 절반 이상, 그리고 80비트 이상 | 권고 |
| HMAC 을 잘라 쓸 때 | 32비트 미만 금지 | 금지 |
| CMAC | 64비트 이상이면 대부분의 쓰임새에 넉넉하다. 64 미만은 위험을 따져 본 뒤에만 쓴다 | 권고 + 조건부 금지 |
| KMAC | 32비트 미만 금지. 64 미만은 위험을 따져 본 뒤에만 쓴다 | 금지 |
절반 이상이라는 권고는 생일 공격의 한계에 맞춘 것입니다. 생일 공격은 같은 값을 내는 두 입력을 찾아내는 공격입니다. 출력이 n 비트인 해시에서 그런 짝을 찾는 데 드는 시도는 2의 n/2 제곱쯤입니다. 절반이라는 수가 거기서 나왔습니다. 80비트 이상이라는 권고는 공격자가 맞혀야 하는 비트 수의 하한으로 잡은 값입니다.
상수 시간 비교
흔히 쓰는 문자열 비교는 다른 글자를 만나는 순간 멈춥니다. 그러면 앞쪽 글자가 몇 개나 맞았는지에 따라 판정에 걸리는 시간이 달라집니다. 공격자는 태그를 조금씩 바꿔 보냅니다. 그때마다 판정에 걸린 시간을 잽니다. 그 차이를 단서로 태그를 한 글자씩 맞춰 나갑니다.
걸린 시간을 단서로 삼는 이런 공격이 타이밍 공격입니다.
flowchart TD
subgraph s1["흔히 쓰는 비교"]
A1["한 글자를 비교한다"] --> A2{"다른가"}
A2 -->|그렇다| A3["거기서 판정 끝"]
A2 -->|아니다| A1
end
subgraph s2["상수 시간 비교"]
B1["끝까지 다 비교한다"] --> B2["한 번에 판정"]
end
상수 시간 비교는 내용에 따라 중간에 멈추지 않습니다. 끝까지 비교한 뒤 한 번에 판정합니다. 파이썬의 hmac.compare_digest 가 그런 함수입니다.
== 대신 이것을 쓰면 타이밍 공격에 대한 취약성이 줄어듭니다. 다만 두 값의 길이가 다르면 걸린 시간이 값의 종류와 길이를 이론적으로는 흘릴 수 있습니다. 값 자체는 흘리지 않습니다.
일회용 값의 재사용
GMAC 의 초기화 벡터는 같은 키 아래에서 서로 다른 두 입력에 겹쳐 쓰일 확률이 2의 32제곱 분의 1 을 넘으면 안 됩니다. 한 번이라도 겹치면 공격자가 해시 부분 키를 알아낼 가능성이 높습니다. 그러면 위조를 쉽게 만들 수 있습니다. 인증의 보장이 사실상 사라집니다. 이 요구는 키를 비밀로 지키는 것과 거의 같은 만큼 중요합니다.
Poly1305 는 논스가 되풀이되면 두 메시지가 같은 일회용 키를 쓰게 됩니다. 일회용이라는 전제가 깨지는 것입니다. 그러니 논스는 호출마다 달라야 합니다. 일회용 키도 공격자가 미리 알 수 없어야 합니다.
보장이 모자랄 때 고르는 수단
메시지 인증 코드가 주는 것만으로 모자라면 다른 수단으로 넘어갑니다. 무엇이 더 필요한지가 갈림입니다.
flowchart TD
Q1{"고의로 고치는 것까지 막아야 하나"}
Q1 -->|아니다| C["체크섬 · 순환 중복 검사"]
Q1 -->|그렇다| Q2{"제삼자가 나중에 검증해야 하나"}
Q2 -->|그렇다| S["전자 서명"]
Q2 -->|아니다| Q3{"내용도 감춰야 하나"}
Q3 -->|그렇다| E["인증 암호"]
Q3 -->|아니다| M["메시지 인증 코드"]
고의가 없는 손상만 걱정된다면 체크섬이나 순환 중복 검사로 됩니다. 이 둘은 우연한 변경만 잡습니다. 제삼자가 나중에 검증해야 하거나 키를 나눠 가질 수 없는 상대라면 전자 서명으로 갑니다. 전자 서명은 만드는 키와 검증하는 키가 달라서, 검증하는 쪽이 서명을 만들어 내지 못합니다.
내용도 감춰야 한다면 암호화와 인증을 한 번에 하는 인증 암호를 씁니다. TLS 1.3 이 레코드를 보호하는 방식입니다.
관련 항목
메시지 인증 코드를 만드는 방식
HMAC · CMAC · GMAC · Poly1305 · KMAC · CBC-MAC · 인증 암호 · AEAD
태그 계산에 들어가는 재료
해시 함수 · 암호학적 해시 함수 · SHA-256 · SHA-3 · 블록 암호 · AES · DES · GCM · ChaCha20 · 대칭 키 · 비밀 키
같은 역할을 두고 겨루는 무결성 확인 수단
메시지 인증 코드를 겨냥하는 공격과 그 대비
길이 확장 공격 · 타이밍 공격 · 사이드 채널 공격 · 상수 시간 비교 · 재전송 공격 · 중간자 공격 · 변조 · 생일 공격 · 논스 · 초기화 벡터 · 타임스탬프
메시지 인증 코드가 지키는 보안 성질
비밀 키를 나눠 갖고 관리하는 방법
키 교환 · 키 유도 함수 · PBKDF2 · 키 회전 · 대칭키 암호
메시지 인증 코드를 쓰는 프로토콜과 포맷
다른 이름: MAC · Message Authentication Code · 인증 태그 · authentication tag