Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

PEP 504 – 기본적으로 시스템 RNG 사용하기

Author:
Alyssa Coghlan <ncoghlan at gmail.com>
Status:
Withdrawn
Type:
Standards Track
Created:
15-Sep-2015
Python-Version:
3.6
Post-History:
15-Sep-2015

Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

초록

Python은 현재 random 모듈의 모듈 수준 API에 결정론적인 Mersenne Twister 난수 제너레이터를 기본으로 사용합니다. 따라서 사용자는 “보안에 민감한” 작업을 할 때 암호학적으로 안전한 os.urandom이나 random.SystemRandom 인터페이스 또는 cryptography 같은 서드파티 라이브러리로 바꿔 써야 한다는 사실을 알고 있어야 합니다.

안타깝게도 이러한 방식 때문에, 자신이 보안에 민감한 작업을 하고 있다는 사실을 인지하지 못한 개발자들이 기본 모듈 수준 API를 사용하고, 그 결과 사용자들을 불필요한 위험에 노출시키는 상황이 발생했습니다.

이것은 급성 문제는 아니지만 만성 문제이며, 보안 결함이 도입된 시점과 그것이 악용되는 시점 사이에 흔히 긴 시간 차이가 있기 때문에 개발자들이 경험을 통해 자연스럽게 배우기 어렵습니다.

이 문제에 대해 궁극적으로 널리 적용될 해결책을 제공하기 위해, 이 PEP는 Python 3.6에서 기본적으로 시스템 난수 제너레이터를 사용하도록 전환하고, 개발자가 프로세스 전체에서 결정론적 난수 제너레이터를 사용하려면 새로운 random.ensure_repeatable() API를 사용하거나 자신만의 random.Random() 인스턴스를 명시적으로 생성하여 옵트인하도록 요구할 것을 제안합니다.

기존 코드에 미치는 영향을 최소화하기 위해, 결정론적 동작을 필요로 하는 모듈 수준 API는 암묵적으로 결정론적 PRNG로 전환됩니다.

PEP 철회

이 PEP를 논의하는 동안 Steven D’Aprano는 기본 비밀번호 및 기타 토큰 생성과 같은 보안에 민감한 작업을 처리하는 “하나의 명확한 방법”을 제공하는 표준화된 secrets 모듈을 도입하자는 더 단순한 대안을 제안했습니다.

Steven의 제안은 기존 random 모듈 API에 어떠한 호환성 위험도 도입하지 않으면서, 그러한 토큰을 생성하는 쉬운 방법과 올바른 방법을 일치시키는 바람직한 효과를 가지므로, 이 PEP는 철회되었으며 대신 Steven의 제안을 PEP 506으로 다듬는 후속 작업이 진행됩니다.

제안

현재 보안에 민감한 애플리케이션에서 random 모듈의 모듈 수준 함수를 사용하는 것은 어떤 경우에도 올바르지 않습니다. 이 PEP는 Python 3.6 이상에서 이 권고를, 해당 프로세스에서 random.ensure_repeatable()이 (직접 또는 간접적으로) 한 번이라도 호출된 경우에는 보안에 민감한 애플리케이션에서 random 모듈의 모듈 수준 함수를 사용하는 것이 올바르지 않다는 내용으로 바꿀 것을 제안합니다.

이를 위해 random의 모듈 수준 호출 가능 객체는 지금처럼 random.Random 인스턴스의 바운드 메서드가 되는 대신, 기존 random._inst 모듈 속성의 대응하는 메서드에 위임하는 함수로 바뀝니다.

이 속성은 기본적으로 random.SystemRandom 인스턴스에 바인딩됩니다.

그런 다음 새로운 random.ensure_repeatable() API는 random._inst 속성을 system.Random 인스턴스에 다시 바인딩합니다. 이렇게 하면 (간접 참조 단계가 하나 추가된 점을 제외하면) 이전 Python 버전과 같은 모듈 수준 API 동작이 복원됩니다:

def ensure_repeatable():
    """Switch to using random.Random() for the module level APIs

    This switches the default RNG instance from the cryptographically
    secure random.SystemRandom() to the deterministic random.Random(),
    enabling the seed(), getstate() and setstate() operations. This means
    a particular random scenario can be replayed later by providing the
    same seed value or restoring a previously saved state.

    NOTE: Libraries implementing security sensitive operations should
    always explicitly use random.SystemRandom() or os.urandom in order to
    correctly handle applications that call this function.
    """
    if not isinstance(_inst, Random):
        _inst = random.Random()

기존 코드에 미치는 영향을 최소화하기 위해, 다음 모듈 수준 함수 중 어느 것을 호출하더라도 암묵적으로 random.ensure_repeatable()이 호출됩니다:

  • random.seed
  • random.getstate
  • random.setstate

random.Random 또는 random.SystemRandom 클래스 API에 대해서는 제안된 변경 사항이 없습니다. 자신만의 난수 생성기를 명시적으로 인스턴스화하는 애플리케이션은 이 제안의 영향을 전혀 받지 않습니다.

암묵적 옵트인에 대한 경고

Python 3.6에서는 결정론적 PRNG 사용에 암묵적으로 옵트인하면 다음 검사를 사용하여 폐지 경고를 발생시킵니다.:

if not isinstance(_inst, Random):
    warnings.warn(DeprecationWarning,
                  "Implicitly ensuring repeatability. "
                  "See help(random.ensure_repeatable) for details")
    ensure_repeatable()

print 호출에 괄호가 빠졌을 때를 위해 추가한 사용자 정의 오류 메시지에 대해 그랬던 것처럼 [10], 이 경고의 구체적인 문구에 대해서도 Stack Overflow(스택 오버플로)에 적절한 답변을 추가해야 합니다.

Python 2.7이 보안 수정 전용 모드로 전환된 후의 첫 번째 Python 3 릴리스에서는, 폐지 경고가 기본적으로 표시되도록 RuntimeWarning으로 격상됩니다.

이 PEP는 프로세스 전역에서 사용되는 기본 RNG가 특정 시드가 주어졌을 때 동일한 일련의 출력을 생성하는 결정론적 PRNG임을 보장하는 기능을 언젠가 제거하자고 제안하지 않습니다. 그 기능은 모델링 및 시뮬레이션 시나리오에서 널리 사용되며, ensure_repeatable()을 직접 또는 간접적으로 호출하도록 요구하는 것만으로도, 결정론적 PRNG 사용이 가져올 수 있는 보안상의 영향을 충분히 고려하지 않은 채 웹 애플리케이션에서 보안에 민감한 작업에 모듈 수준 random API를 사용하는 경우를 해결하기에 충분한 개선입니다.

성능 영향

random.Random과 random.SystemRandom 사이의 큰 성능 차이로 인해, Python 3.6으로 이식된 애플리케이션은 다음과 같은 경우에 상당한 성능 저하를 겪게 됩니다:

  • 애플리케이션이 모듈 수준 random API를 사용하는 경우
  • 암호학적 품질의 무작위성이 필요하지 않은 경우
  • 애플리케이션이 random.seed, random.getstate, 또는 random.setstate를 호출하여 이미 암묵적으로 결정론적 PRNG에 다시 옵트인하고 있지 않은 경우
  • 애플리케이션이 random.ensure_repeatable을 명시적으로 호출하도록 갱신되지 않은 경우

이 내용은 Python 3.6 What’s New 안내서의 이식(Porting) 절에 기재되며, 영향을 받는 애플리케이션의 __main__ 모듈에 다음 코드를 포함하라는 권고가 함께 제시될 것입니다:

if hasattr(random, "ensure_repeatable"):
    random.ensure_repeatable()

암호학적 품질의 무작위성이 실제로 필요한 애플리케이션은 속도와 관계없이 시스템 난수 제너레이터를 사용해야 합니다. 따라서 그런 경우에는 이 PEP에서 제안하는 변경이 이전에 잠재해 있던 보안 결함을 고치게 됩니다.

문서 변경

random 모듈 문서는 seed, getstate, setstate 인터페이스에 대한 설명을 모듈 문서의 뒷부분으로 옮겨, 새 ensure_repeatable 함수에 대한 설명 및 관련 보안 경고와 함께 배치하도록 갱신될 것입니다.

모듈 문서의 해당 절에는 ensure_repeatable로 활성화되는 결정론적 PRNG(게임, 모델링 및 시뮬레이션, 소프트웨어 테스트)와 기본으로 사용되는 시스템 RNG(암호학, 보안 토큰 생성)의 사용 사례를 각각 다루는 논의도 추가됩니다. 이 설명에서는 후자의 작업에 서드파티 보안 라이브러리를 사용할 것도 권장할 것입니다.

근거

마감과 예산의 압박 속에서 안전한 소프트웨어를 작성하는 일은 어려운 문제입니다. 이는 개인 식별 정보가 관련된 데이터 유출 사고 [1]가 정기적으로 보고되는 데서도 드러나고, 자동차 [2] 같은 새로운 시스템을 인터넷에 연결할 때 보안 고려 사항을 반영하지 못하는 사례에서도 드러납니다. 또한 인터넷 [4]에서 쉽게 찾을 수 있는 프로그래밍 조언 중 상당수가 컴퓨터 보안의 수학적 난해함을 전혀 고려하지 않는 것도 사실입니다. 이러한 문제를 더욱 악화시키는 것은 방어하는 쪽이 잠재적 취약점을 모두 막아야 한다는 사실입니다. 한 번의 실수만으로도 다른 방어 수단을 무력화할 수 있기 때문입니다 [11].

이 마지막 측면을 특히 어렵게 만드는 요인 중 하나는 부적절하게 사용하면 조용한 보안 실패가 발생하는 API입니다. 이러한 실패에서 자신이 잘못하고 있다는 사실을 알아내는 방법은 코드를 검토하는 누군가가 “잠재적인 보안 문제입니다”라고 말해 주거나, 자신이 책임지는 시스템이 그러한 실수 때문에 침해되는 것뿐입니다(게다가 시스템이 침해되더라도 그 시스템에 대한 책임은 여전히 자신에게 있으며, 침입 탐지 및 감사 메커니즘이 충분히 갖춰져 있어야만 사후에 침해가 어떻게 일어났는지 파악할 수 있습니다).

이러한 상황은 “보안 피로”를 낳는 중대한 기여자입니다. 보안 피로란 개발자들이 보안 엔지니어가 “그렇게 쉬운 방법으로 하지 마십시오, 보안 취약점이 생깁니다”라는 말만 늘 한다고 느끼는 현상이며, 이러한 느낌은 대개 옳습니다 [9].

세계에서 가장 인기 있는 언어 중 하나 [8]를 설계하는 사람으로서, 우리는 더 많은 상황에서 쉬운 방법이 올바른 방법(또는 적어도 “틀리지 않은” 방법)이 되도록 만들어 이 문제를 줄이는 데 기여할 수 있습니다. 그러면 개발자와 보안 엔지니어는 언어의 기본 동작과 씨름하는 시간을 줄이고, 실제로 주목할 만한 위협을 완화하는 데 더 많은 시간을 쓸 수 있습니다.

논의

왜 “ensure_deterministic”이 아니라 “ensure_repeatable”입니까?

이는 기술적으로는 같은 뜻인데도 전문 용어로서의 단어 의미가 일반적인 의미와 충돌하는 경우입니다.

기술적인 관점에서 “결정론적 RNG”란 알고리즘과 현재 상태를 알면 임의의 미래 상태를 확실하게 계산할 수 있다는 뜻입니다.

문제는 “결정론적”이라는 말만으로는 이러한 전제 조건이 전달되지 않는다는 점입니다. 그래서 일반적인 의미에는 익숙하지만 기술적 의미에 붙는 추가 전제 조건에는 익숙하지 않은 사람들은 이를 “예측 가능한”이나 “무작위가 아닌”이라는 뜻으로 해석하기 쉽습니다.

전통적인 RNG를 “결정론적”이라고 설명할 때의 두 번째 문제는, 시스템 RNG로는 할 수 없지만 전통적인 RNG로는 할 수 있는 일이 무엇인지 제대로 알려 주지 못한다는 점입니다.

“ensure_repeatable”은 이 두 문제를 모두 해결하려는 이름입니다. 이 단어의 일반적인 의미가 시스템 RNG보다 결정론적 PRNG를 선호하는 주된 이유를 정확히 설명하기 때문입니다. 그 이유란 같은 시드 값을 제공하거나 이전에 저장한 PRNG 상태를 복원하여 같은 출력 시퀀스를 반복할 수 있도록 보장하는 것입니다.

Python 3.6 이상에서만 기본값 변경

ssl 모듈의 기능 향상이나 기본적으로 HTTPS 인증서를 올바르게 검증하도록 한 전환 등 최근의 다른 보안 변경 사항 중 일부는 현재 지원되는 모든 Python 버전에 백포트할 만큼 중요하다고 판단되었습니다.

이 경우의 차이는 정도의 차이입니다. 이 변경을 원래 예정보다 2년쯤 일찍 도입해서 얻는 추가 이점은, 유지보수 릴리스에서 이처럼 침습적인 변경을 하는 데 드는 추가 노력이나 안정성 위험을 정당화할 만큼 크지 않습니다.

모듈 수준 함수 유지

일반적인 하위 호환성 고려 사항에 더해, Python은 교육 목적으로 널리 쓰이고 있으며, 우리는 특히 현재 random 모듈 API를 사용할 수 있다고 전제하는 폭넓은 배열의 교육 자료를 무용지물로 만들고 싶지 않습니다. 이에 따라 이 제안은 공개 API의 대부분을 수정 없이 계속 사용할 수 있을 뿐 아니라 새로운 경고도 발생하지 않도록 보장합니다.

결정론적 RNG를 암묵적으로 선택할 때의 경고

결정론적 PRNG를 암묵적으로 선택하는 방식이 필요합니다. Python은 결정론적 PRNG가 올바른 선택인 모델링과 시뮬레이션 목적으로 널리 사용되며, 이러한 소프트웨어 모델 중 상당수에는 최신 Python 버전에서도 계속 동작하도록 관리하는 전담 유지 보수 팀이 없기 때문입니다.

안타깝게도 os.urandom에서 얻은 데이터로 random.seed를 명시적으로 호출하는 것 역시 온라인에서 쉽게 찾을 수 있는 잘못된 “Python에서 보안 토큰을 생성하는 방법” 안내서 여러 곳에 나오는 실수입니다.

처음에는 DeprecationWarning을, 이후에는 결국 RuntimeWarning을 사용하여 결정론적 PRNG로 암묵적으로 전환하지 말라고 권고하는 목적은 다음과 같습니다. 앞으로 암호학적으로 안전한 RNG가 필요한 사용자는 random.seed()를 호출하지 않도록 하고, 결정론적 제너레이터가 정말로 필요한 사용자는 random.ensure_repeatable()을 명시적으로 호출하도록 유도하는 것입니다.

사용자 공간 CSPRNG 도입 피하기

python-ideas에서 있었던 이 제안에 대한 최초 논의 [5]에서는 상대적으로 느린 시스템 난수 제너레이터를 기본값으로 사용하는 대신, 암호학적으로 안전한 의사 난수 제너레이터를 도입하여 이를 기본으로 사용하자고 제안했습니다.

이 접근 방식의 문제 [7]는 난수 생성이 성능상 중요한 경로에 있지도 않을 수 있는 애플리케이션을 위해, 보안에 민감한 상황에 추가적인 장애 지점을 도입한다는 것입니다.

암호학적 품질의 무작위성이 실제로 필요한 애플리케이션은 속도와 관계없이 시스템 난수 제너레이터를 사용해야 하므로, 그러한 경우에는 그렇습니다.

결정론적 PRNG는 “충분히 안전”하지 않습니까?

한마디로 “아니요”입니다. 그렇기 때문에 모듈 문서에 보안에 민감한 목적으로는 이를 사용하지 말라는 경고가 있는 것입니다. 현재 Python의 난수 제너레이터만을 다룬 연구는 알지 못하지만, PHP의 난수 제너레이터에 대한 연구 [3]에서는 해당 하위 시스템의 약점을 이용해 널리 쓰이는 PHP 웹 애플리케이션의 비밀번호 복구 토큰을 실제로 공격할 수 있음을 입증했습니다.

그러나 안전한 소프트웨어 개발의 규칙 중 하나는 “공격은 발전할 뿐, 결코 퇴보하지 않는다”는 것입니다. 따라서 Python 3.6이 릴리스될 무렵에는 Python의 결정론적 PRNG를 실제로 공격하는 방법이 공개적으로 문서화되어 있을 수도 있습니다.

Python 생태계의 보안 피로

지난 몇 년 동안 컴퓨팅 업계 전체는 우리 모두가 의존하는 공유 네트워크 인프라를 “기본적으로 안전한(secure by default)” 방향으로 업그레이드하기 위해 공동으로 노력해 왔습니다. Python은 (OpenStack IaaS(Infrastructure-as-a-Service) 플랫폼을 포함한) 네트워크 서비스 개발과 Linux 시스템 전반의 시스템 관리에 가장 널리 쓰이는 프로그래밍 언어 중 하나입니다. 그래서 그 부담의 상당 부분을 Python 생태계가 떠안게 되었습니다. 이러한 문제가 그다지 중요하지 않은 다른 맥락에서 Python을 사용하는 Python 개발자들이 이를 답답하게 여기는 것도 이해할 만합니다.

이러한 고려 사항은 python-ideas에 게시된 초기 초안 개념 [6]에 비해 이 제안에서 하위 호환성이 크게 개선된 주요 요인 중 하나입니다.

감사의 글

  • Theo de Raadt: 암호학적으로 안전한 난수 제너레이터를 기본값으로 삼는 방안을 진지하게 고려해 보라고 Guido van Rossum에게 제안해 주었습니다.
  • 결정론적 RNG에서만 의미가 있는 함수 중 하나가 호출될 때 random.Random 구현으로 투명하게 전환하는 방식을 제안해 주신 Serhiy Storchaka, Terry Reedy, Petr Viktorin 및 python-ideas 스레드의 모든 분께 감사드립니다.
  • Nathaniel Smith: PHP의 난수 제너레이터를 비밀번호 재설정 토큰 생성에 사용할 때의 실제 공격에 관한 참고 자료를 제공해 주었습니다.
  • 네트워크 보안 전문가들과 추가 논의를 진행하여, 사용자 공간 CSPRNG를 도입하면 시스템 RNG를 직접 사용하는 것에 비해 얻는 이득은 부족한 채 복잡성만 늘어난다는 의견을 이끌어 내 주신 Donald Stufft에게 감사드립니다.
  • Python 생태계의 현재 보안 피로 수준에 대해 설득력 있게 주장해 준 Paul Moore

참고 문헌