← 문제로

40. 세션 암호화 스트림의 nonce/IV 재사용 (C#)

난이도 최상
내 리뷰 · C#
해설 · 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 을 알아내면 임의 패킷을 위조·재생할 수 있다.
    • 재현:
      1. 재접속 재사용: 세션 중 몇 패킷 주고받아 nonce=5 까지 진행 → 재접속으로 ResetForReconnect() → nonce=0..5 구간을 같은 키로 다시 사용 → E(key,0) keystream 이 첫 세션 첫 패킷과 새 세션 첫 패킷에 이중 사용.
      2. 송수신 공유: 서버가 nonce=3 으로 송신, 클라 패킷 수신 시 같은 _nonce 를 증가시키므로 송신·수신 스트림이 nonce 공간을 잠식·중첩 → 같은 nonce 가 한쪽은 송신 keystream, 한쪽은 수신 keystream 으로 겹칠 수 있음.
    • 근본 원인: nonce 유일성 불변식(같은 키에서 nonce 절대 재사용 금지)을 세션 수명·방향·재협상에 걸쳐 보장하지 않음. 카운터를 리셋하면서 키를 바꾸지 않았다.

수정안

  1. 재키(rekey): 재접속/재협상마다 새 세션 키(또는 키 유도 시 새 salt/epoch)를 쓴다. 키가 바뀌면 nonce 0 부터 재시작해도 안전하다.
  2. 방향 분리: 송신/수신 nonce(또는 키)를 분리한다. 흔히 nonce 최상위 비트나 별도 상위 필드로 방향을 인코딩한다.
  3. 단조 증가 보장 + 소진 처리: 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 소진 시 재키의 필요성.