← 문제로

9. 압축/암호화 협상과 폴백 (전송 변환 파이프라인)

난이도 상
내 리뷰 · C#
해설 · C#

해설 — 압축/암호화 협상과 폴백 (전송 변환 파이프라인)

난이도: 상

답변 프레임워크: 요약 → 문제 분류 → 원인 → 수정안 → 더 나은 설계

요약

핸드셰이크에서 압축/암호화 알고리즘을 협상하고, 패킷 헤더 1바이트 flags 로 적용 여부를 표시하는 파이프라인이다. 골격(서버 우선순위 협상, 압축→암호화 순서)은 맞지만, 보안 다운그레이드(협상 실패 시 조용한 평문 폴백), flags 만 있고 "어떤 알고리즘인지"가 와이어에 없음(Comp/Crypt 가 세션 상태에만 존재) → 버전 공존 시 송수신 알고리즘 불일치, 역변환 순서와 인증 검증 누락, **수신 측 flags 무신뢰(공격자 강제 다운그레이드)**가 문제다. 핵심: "변환을 무엇으로 적용했는가"는 세션 합의 + 와이어 식별이 모두 필요하고, 보안 변환의 협상은 절대 조용히 약화되면 안 된다.


문제점

(B) 협상 실패 시 조용한 평문/무압축 폴백 — 보안(다운그레이드)

  • 증상: 공통 알고리즘이 없으면 x.Crypt = None 으로 남고 그냥 진행 → 암호화 없이 게임 전체가 평문으로 흐른다. 로그/거부도 없다.
  • 재현조건: MITM 이 핸드셰이크의 clientCrypt 목록을 비우거나, 의도적으로 "약한 것만" 남기는 다운그레이드 공격. 또는 버전 차로 공통 알고리즘이 없을 때.
  • 근본원인: 암호화는 협상 대상이되 "암호화 없음"은 허용 정책이 되면 안 된다. 실패는 폴백이 아니라 연결 거부여야 한다(또는 명시적 안전한 최소선 보장).

(핵심) 와이어에 "알고리즘 식별자" 부재 — 호환성/정확성

  • 증상: 헤더 flags 는 "압축됨/암호화됨" 비트만 있고 어떤 알고리즘인지는 없다. 복호화는 x.Crypt(세션 상태)에 의존. 그런데 세션의 Comp/Crypt 는 협상 결과를 양쪽이 각자 따로 저장한다. 신/구 버전이 협상 메시지 포맷을 다르게 해석하거나 우선순위가 어긋나면, 송신은 Zstd 로 압축했는데 수신은 Deflate 로 풀어 데이터가 깨진다.
  • 재현조건: 신버전 서버(Zstd 우선) ↔ 구버전 클라(협상 메시지에 Zstd 없음/오해석). flags 의 Compressed 비트만 보고 세션에 저장된 (잘못된) 알고리즘으로 해동.
  • 근본원인: "적용된 변환의 정확한 정체"가 와이어에 실려있지 않고 세션 합의에만 의존. 합의가 어긋나는 순간 silent corruption. flags 의 표현력이 부족.

(D)+(C) 역변환 순서·인증 검증 — 보안/정확성

  • 증상: Encode 는 압축→암호화 순, Decode 는 복호화→해동 순(대칭은 맞다). 하지만
    • AES-CBC 같은 인증 없는 암호화(unauthenticated) 를 쓰면 변조 탐지 불가 (padding oracle 등). AES-GCM(AEAD)이 아닌 폴백이 위험.
    • "압축 후 암호화"는 CRIME/BREACH 류 압축 사이드채널에 노출될 수 있다 (압축률로 평문 추론). 비밀이 섞인 채널이면 순서/정책 재고 필요.
  • 근본원인: 변환 조합의 보안 속성(AEAD 여부, compress-then-encrypt 위험)을 정책으로 못박지 않음.

(D) 수신 측이 flags 를 무조건 신뢰 — 보안

  • 증상: Decode 는 패킷 flags 만 보고 변환을 적용/생략. 협상에서 "암호화 필수"로 합의했어도, 공격자가 Encrypted 비트를 끈 평문 패킷을 주입하면 서버가 그대로 수용.
  • 근본원인: 협상된 정책과 수신 패킷 flags 의 일치를 강제하지 않음. "암호화 켜기로 했으면, 암호화 안 된 패킷은 거부"가 빠졌다.

(부수) 길이/널 검증 — 견고성

  • Decodepacket.Length >= 1 검사 없이 packet[0] 접근. 빈 패킷에서 예외. 매 호출 배열 신규 할당(GC 압력).

수정안

(B) 폴백 금지 — 협상 실패는 거부

public bool Negotiate(SessionXform x, CompAlgo[] cc, CryptAlgo[] cy)
{
    foreach (var c in ServerComp)
        if (Array.IndexOf(cc, c) >= 0) { x.Comp = c; break; }

    x.Crypt = CryptAlgo.None;
    foreach (var c in ServerCrypt)
        if (c != CryptAlgo.None && Array.IndexOf(cy, c) >= 0) { x.Crypt = c; break; }

    // 암호화는 필수 — 공통 AEAD 가 없으면 연결 거부 (조용한 평문 금지)
    if (x.Crypt == CryptAlgo.None) return false;
    return true;
}

(핵심) 협상 결과를 양측이 "확정"하고, 필요한 경우 와이어에 알고리즘 ID

  • 협상은 단순 "각자 추론"이 아니라 서버가 고른 결과를 클라에 명시 통보 → 클라 ack (3-way 가 아니면 최소 서버 결정 통보). 양쪽 세션 상태가 같음을 보장.
  • flags 1바이트로 부족하면 헤더에 알고리즘 enum 바이트를 넣어 self-describing 하게:
[byte flags][byte compAlgo][byte cryptAlgo][body]   // 또는 협상 시 고정하고 세션에 ID 봉인

세션에 고정하더라도, 협상 단계에서 algo id 까지 확정해 송수신이 같은 값을 쓰게 한다.

(D) 정책 강제 + 인증 암호화

public byte[] Decode(SessionXform x, byte[] packet, SessionPolicy pol)
{
    if (packet.Length < 1) throw new ProtocolException("empty");
    var flags = (XformFlags)packet[0];

    // 협상 정책과 불일치하면 거부 — 다운그레이드 주입 방지
    if (pol.RequireEncryption && (flags & XformFlags.Encrypted) == 0)
        throw new ProtocolException("unencrypted packet rejected");

    var body = packet.AsSpan(1);
    if ((flags & XformFlags.Encrypted) != 0)
        body = DecryptAead(x.Crypt, body);   // AEAD: 복호화+무결성 검증 동시
    if ((flags & XformFlags.Compressed) != 0)
        body = Decompress(x.Comp, body, maxOut: pol.MaxDecompressed); // 압축폭탄 상한
    return body.ToArray();
}
  • AEAD(AES-GCM/ChaCha20-Poly1305) 강제, flags 도 AAD 에 포함해 비트 변조 탐지.
  • 압축 해제 상한(decompression bomb 방어): 풀린 크기를 제한.

더 나은 설계

  • TLS/DTLS/QUIC 같은 검증된 전송 보안을 우선 고려. 직접 만들 때만 아래.
  • 암호화는 필수·AEAD·다운그레이드 불가를 정책 상수로 못박고, 협상 실패는 거부.
  • 협상 결과를 명시적으로 확정·교환하고, 가능하면 패킷이 self-describing 하게 알고리즘을 식별(또는 키 ID/epoch). 키 롤오버 시 epoch 로 송수신 동기화.
  • flags/헤더를 AAD 로 무결성에 포함 → 비트 플립 변조 탐지.
  • 압축은 비밀 섞인 채널에선 주의(CRIME/BREACH). 게임 좌표 등 저민감 데이터에 한정하거나 per-record 분리.

트레이드오프

  • 패킷에 알고리즘 ID 를 실으면 1~2바이트 오버헤드, 대신 버전 공존/키 롤오버에 견고. 세션 고정 + 협상 확정으로 갈음하면 오버헤드 0 이지만 협상 정확성에 더 의존.
  • AEAD 강제는 약한 클라(구형 디바이스)를 배제할 수 있어, 최소 보안선을 정책으로 합의해야.

면접 포인트

  1. "협상 실패 시 평문 폴백이 왜 위험한가?" → 다운그레이드 공격. MITM 이 협상을 약화시켜 평문/약한 암호로 떨어뜨림. 암호화는 폴백 대상이 아니라 실패 시 연결 거부여야.
  2. "패킷에 압축됨/암호화됨 비트만 있고 알고리즘은 세션에 둔다. 무슨 문제?" → 송수신 세션 합의가 어긋나면(버전/협상 오류) flags 만으로는 같은 알고리즘 보장 못 함 → silent corruption. self-describing 또는 협상 확정 필요.
  3. "압축과 암호화 순서, AEAD 가 왜 중요한가?" → compress-then-encrypt 는 CRIME/BREACH 사이드채널 위험. 비인증 암호(CBC)는 변조/패딩 오라클 취약. AEAD 로 기밀성+무결성, 헤더는 AAD 로 묶어 검증.

내가 놓친 항목 (복습용)

  • [ ] (B) 협상 실패 → 조용한 평문 폴백(다운그레이드)
  • [ ] 와이어에 알고리즘 식별 부재 → 세션 합의 어긋나면 송수신 변환 불일치
  • [ ] (D) flags 무신뢰 수용 → 다운그레이드 패킷 주입, 정책 미강제
  • [ ] (D)(C) 비AEAD 암호/압축 사이드채널, 압축폭탄 상한 부재
  • [ ] (D) 길이 검증/할당 비용