40. 세션 암호화 스트림의 nonce/IV 재사용 (C#)
난이도 최상해설 — 세션 암호화 스트림의 nonce/IV 재사용 (C#)
난이도: 최상
요약
스트림 암호(CTR)는 같은 (key, nonce) 로 만든 keystream 을 두 번 쓰면 두 평문의
XOR 이 그대로 노출된다(C1 ⊕ C2 = P1 ⊕ P2). 이 코드는 세 곳에서 nonce 재사용을
일으킨다: (1) ResetForReconnect 가 키는 그대로 두고 _nonce=0 으로 리셋 (A),
(2) 송신과 수신이 같은 _nonce 필드를 공유 (B), (3) 재접속마다 같은 낮은
nonce 구간을 반복. 결과적으로 기밀성이 붕괴되고 위조·재생(replay)이 가능해진다.
문제점
- [분류: nonce/IV 재사용 (keystream reuse) · 암호 사용 오류]
- 증상: 서로 다른 두 패킷이 같은 keystream 으로 암호화되어, 공격자가 두 암호문을 XOR 하면 평문 간 XOR 을 얻고(구조/알려진 평문으로 복원 가능), keystream 을 알아내면 임의 패킷을 위조·재생할 수 있다.
- 재현:
- 재접속 재사용: 세션 중 몇 패킷 주고받아 nonce=5 까지 진행 → 재접속으로
ResetForReconnect()→ nonce=0..5 구간을 같은 키로 다시 사용 →E(key,0)keystream 이 첫 세션 첫 패킷과 새 세션 첫 패킷에 이중 사용. - 송수신 공유: 서버가 nonce=3 으로 송신, 클라 패킷 수신 시 같은
_nonce를 증가시키므로 송신·수신 스트림이 nonce 공간을 잠식·중첩 → 같은 nonce 가 한쪽은 송신 keystream, 한쪽은 수신 keystream 으로 겹칠 수 있음.
- 재접속 재사용: 세션 중 몇 패킷 주고받아 nonce=5 까지 진행 → 재접속으로
- 근본 원인: nonce 유일성 불변식(같은 키에서 nonce 절대 재사용 금지)을 세션 수명·방향·재협상에 걸쳐 보장하지 않음. 카운터를 리셋하면서 키를 바꾸지 않았다.
수정안
- 재키(rekey): 재접속/재협상마다 새 세션 키(또는 키 유도 시 새 salt/epoch)를 쓴다. 키가 바뀌면 nonce 0 부터 재시작해도 안전하다.
- 방향 분리: 송신/수신 nonce(또는 키)를 분리한다. 흔히 nonce 최상위 비트나 별도 상위 필드로 방향을 인코딩한다.
- 단조 증가 보장 + 소진 처리: nonce 는 세션 내 절대 재사용 금지, 64/96비트가 소진에 근접하면 강제 재키.
private byte[] _sendKey, _recvKey; // 방향별 키 (HKDF 로 유도)
private ulong _sendCtr, _recvCtr; // 방향별 단조 카운터, 절대 리셋 금지
public void InitSession(byte[] master, uint epoch)
{
// epoch(재협상 세대)를 salt 로 섞어 세션마다 다른 키 유도
_sendKey = Hkdf(master, epoch, "c2s");
_recvKey = Hkdf(master, epoch, "s2c");
_sendCtr = 0; _recvCtr = 0; // 키가 달라졌으므로 0 재시작 안전
}
public byte[] EncryptOutbound(ReadOnlySpan<byte> plain)
{
if (_sendCtr == ulong.MaxValue) throw new InvalidOperationException("rekey 필요");
var ks = BlockCipher.Ctr(_sendKey, _sendCtr++, plain.Length);
return Xor(plain, ks);
}
ResetForReconnect 는 반드시 epoch 증가 + 재키를 동반해야 한다(카운터만 리셋 금지).
실무에선 AEAD(AES-GCM/ChaCha20-Poly1305)를 써서 무결성 태그로 위조·재생까지 막고,
nonce 는 (방향 ‖ 단조 시퀀스)로 구성한다.
더 나은 설계
- 검증된 AEAD + 표준 nonce 규약(예: TLS 1.3 record nonce = static IV ⊕ seq)을 쓰고 직접 CTR 을 굴리지 않는다.
- 재생 방어를 위해 시퀀스 번호를 AAD 에 포함하고 수신측이 윈도우로 중복/역행 거부.
- 트레이드오프: AEAD 태그(16B) 오버헤드 + nonce 관리 복잡도. 그러나 기밀성·무결성을 동시에 얻는다.
면접 포인트
- CTR/스트림 암호에서 nonce 재사용이 왜 치명적인가(
C1⊕C2=P1⊕P2, 다중 시간 패드). - "카운터 리셋"이 아니라 "재키"가 정답인 이유(키가 유일성 도메인).
- 송/수신 방향 분리와 nonce 소진 시 재키의 필요성.
해설 — 세션 암호화 스트림의 nonce/IV 재사용 (C++)
난이도: 최상
요약
CTR 스트림 암호는 같은 (key, nonce) keystream 을 재사용하면 C1 ⊕ C2 = P1 ⊕ P2
로 두 평문의 XOR 이 노출된다. 이 코드는 (1) ResetForReconnect 가 키를 유지한 채
nonce_=0 리셋 (A), (2) 송신/수신이 nonce_ 한 필드를 공유 (B), (3) 재접속마다
낮은 nonce 를 반복 사용 — 세 경로로 nonce 를 재사용해 기밀성이 붕괴된다. C++ 특유의
추가 위험으로, nonce_ 가 여러 IO 스레드에서 무보호로 증가되면 데이터 레이스로
서로 다른 패킷이 같은 nonce 값을 읽어 재사용이 더 쉽게 발생한다.
문제점
- [분류: nonce/IV 재사용 · 암호 오용 · (동시성 시) nonce 카운터 데이터 레이스]
- 증상: 서로 다른 패킷이 동일 keystream 으로 암호화 → 공격자가 XOR 로 평문 관계 복원, keystream 추출 후 임의 패킷 위조/재생.
- 재현:
- 재접속 재사용 — nonce 5까지 진행 후
ResetForReconnect()로 0 재시작 →Ctr(key,0)keystream 이 두 번 사용. - 송수신 공유 — 송신·수신이 같은
nonce_를 증가시켜 방향 간 nonce 중첩. - (멀티스레드)
++nonce_가 원자적이지 않아 두 스레드가 같은 값을 읽고 각자 암호화 → 같은 nonce 로 두 평문 암호화.
- 재접속 재사용 — nonce 5까지 진행 후
- 근본 원인: nonce 유일성(같은 키에서 재사용 금지) 불변식을 방향·재협상·동시성에 걸쳐 보장하지 않음. 카운터 리셋 시 키를 교체하지 않음.
수정안
방향별 키 분리 + epoch 기반 재키 + 단조 카운터(원자). 카운터만 리셋 금지.
#include <atomic>
#include <stdexcept>
class SessionCrypto {
std::vector<uint8_t> sendKey_, recvKey_;
std::atomic<uint64_t> sendCtr_{0}, recvCtr_{0}; // 방향별, 절대 리셋 금지
public:
void InitSession(const std::vector<uint8_t>& master, uint32_t epoch) {
sendKey_ = Hkdf(master, epoch, "c2s"); // epoch 를 섞어 세션마다 다른 키
recvKey_ = Hkdf(master, epoch, "s2c");
sendCtr_.store(0); recvCtr_.store(0); // 키가 달라져 0 재시작 안전
}
std::vector<uint8_t> EncryptOutbound(const uint8_t* p, size_t len) {
uint64_t n = sendCtr_.fetch_add(1, std::memory_order_relaxed); // 유일 nonce 원자 발급
if (n == UINT64_MAX) throw std::runtime_error("rekey 필요");
auto ks = BlockCipher::Ctr(sendKey_, n, len);
return Xor(p, ks);
}
// DecryptInbound 는 recvKey_/recvCtr_ 사용
};
핵심: (1) 재접속은 epoch 증가 + 재키로 처리(카운터만 리셋 금지), (2) 송/수신
키·카운터 분리, (3) fetch_add 로 동시 송신에도 nonce 유일성 보장, (4) 소진 임박 시
강제 재키. 실무에선 AES-GCM/ChaCha20-Poly1305 같은 AEAD 로 무결성·재생 방어까지 확보.
더 나은 설계
- libsodium/OpenSSL 의 검증된 AEAD 를 쓰고 nonce 는 (방향 ‖ 단조 시퀀스)로 구성, 직접 CTR 구현을 피한다.
- 시퀀스를 AAD 에 넣고 수신 윈도우로 재생/역행 거부.
- 트레이드오프: AEAD 태그 오버헤드와 nonce 관리 비용 대신 기밀성+무결성 동시 확보.
면접 포인트
- CTR nonce 재사용의 수학적 결과(
C1⊕C2=P1⊕P2)와 위조 가능성. - "카운터 리셋 대신 재키", 방향 분리, nonce 소진 시 재키.
- 멀티스레드에서 nonce 발급이 원자여야 하는 이유(
fetch_add).