PEP 524 – Linux에서 os.urandom()을 블로킹하도록 변경합니다
- Author:
- Victor Stinner <vstinner at python.org>
- Status:
- Final
- Type:
- Standards Track
- Created:
- 20-Jun-2016
- Python-Version:
- 3.6
Table of Contents
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
보안을 강화하기 위해 os.urandom()을 Linux 3.17 이상에서 OS의 urandom이 초기화될 때까지 블로킹하도록 수정합니다.
또한 Linux에서 os.urandom()이 블로킹될 때 처리 방법을 선택할 수 있도록 Linux 및 Solaris용 새 os.getrandom() 함수를 추가합니다.
버그
원래의 버그
Python 3.5.0은 Linux 3.17 및 Solaris 11.3에 도입된 새로운 getrandom() 시스템 호출을 사용하도록 개선되었습니다. 문제는 사용자가 가상 머신과 임베디드 장치의 Linux에서 Python 3.5가 시작 시 블로킹된다고 불평하기 시작했다는 것입니다. 다음 이슈를 참조하십시오: #25420 및 #26839.
Linux에서 getrandom(0)은 커널이 128비트의 엔트로피로 urandom을 초기화할 때까지 블로킹됩니다. 이슈 #25420은 import random에서 블로킹되는 Linux 빌드 플랫폼을 설명합니다. 이슈 #26839는 MD5 해시를 계산하는 데 사용되는 짧은 Python 스크립트인 systemd-cron이 초기화 과정의 매우 이른 시점에 호출되는 스크립트임을 설명합니다. 시스템 초기화는 Python을 초기화하기 위해 getrandom(0)에서 블로킹되는 이 스크립트에서 함께 블로킹됩니다.
Python 초기화는 해시 서비스 거부 공격(hash DoS)에 대한 대응책을 구현하기 위해 임의의 바이트를 필요로 합니다. 다음을 참조하십시오:
random 모듈을 임포트하면 random.Random의 인스턴스인 random._inst가 생성됩니다. Python 3.5에서는 random.Random 생성자가 os.urandom()에서 2500바이트를 읽어 Mersenne Twister RNG(난수 제너레이터)의 시드로 사용합니다.
다른 플랫폼도 이 버그의 영향을 받을 수 있지만, 실제로 시스템을 초기화하는 데 Python 스크립트를 사용하는 시스템은 Linux뿐입니다.
Python 3.5.2에서의 상태
Python 3.5.2는 Python 2.7 및 Python 3.4와 동일하게 동작합니다. 시스템 urandom이 초기화되지 않은 경우 시작 시 블로킹되지 않지만, os.urandom()은 품질이 낮은 엔트로피를 반환할 수 있습니다(쉽게 추측할 수 없더라도 그렇습니다).
사용 사례
다음 사용 사례는 보안성과 실용성 사이에서 적절한 절충안을 선택하는 데 도움이 됩니다.
사용 사례 1: 초기화 스크립트
systemd-cron과 같은 Python 3 스크립트를 사용하여 시스템을 초기화합니다. 스크립트가 블로킹되면 시스템 초기화도 중단됩니다. 이슈 #26839는 이 사용 사례의 좋은 예입니다.
사용 사례 1.1: 비밀이 필요하지 않음
초기화 스크립트가 안전한 비밀을 생성할 필요가 없다면, 이 사용 사례는 Python 3.5.2에서 이미 올바르게 처리됩니다. Python 시작 시 더 이상 시스템 urandom에서 블로킹되지 않습니다.
사용 사례 1.2: 안전한 비밀이 필요함
초기화 스크립트가 안전한 비밀을 생성해야 한다면 안전한 해결책이 없습니다.
약한 엔트로피로 대체하는 것은 허용되지 않으며, 그렇게 하면 프로그램의 보안이 저하됩니다.
Python은 자체적으로 안전한 엔트로피를 생성할 수 없으며, 시스템 urandom이 초기화될 때까지 기다릴 수밖에 없습니다. 그러나 이 사용 사례에서는 이 스크립트가 전체 시스템 초기화를 차단하므로 시스템이 부팅되지 않습니다.
진정한 해답은 이러한 스크립트가 시스템 초기화를 차단해서는 안 된다는 것입니다. 시스템 초기화 과정에서 스크립트를 매우 일찍 시작하는 것은 괜찮지만, 스크립트는 비밀을 생성할 수 있을 때까지 몇 초 동안 차단될 수 있습니다.
참고로, 경우에 따라 시스템 urandom의 초기화가 결코 발생하지 않으므로 시스템 urandom을 기다리는 프로그램이 영원히 차단됩니다.
사용 사례 2: 웹 서버
HTTP 및 HTTPS 프로토콜을 사용하여 웹 페이지를 제공하는 Python 3 웹 서버를 실행합니다. 서버는 가능한 한 빨리 시작됩니다.
해시 DoS 공격의 첫 번째 대상은 웹 서버였습니다. 공격자가 해시 비밀을 쉽게 추측할 수 없도록 하는 것이 중요합니다.
웹 페이지를 제공하려면 쿠키를 생성하거나 암호화 키를 생성하는 등의 작업에 비밀이 필요한 경우, 해당 비밀은 충분히 좋은 엔트로피를 사용하여 생성해야 합니다. 다시 말해, 비밀을 추측하기 어려워야 합니다.
웹 서버에는 보안이 필요합니다. 보안과 낮은 엔트로피로 서버를 실행하는 것 중 하나를 선택해야 한다면 보안이 더 중요합니다. 충분히 좋은 엔트로피가 없다면 서버는 차단되거나 오류와 함께 실패해야 합니다.
시스템 urandom이 초기화되기 전에 호스트에서 웹 서버를 시작하는 것이 의미가 있는지에 대한 질문입니다.
이슈 #25420과 #26839는 시스템 urandom이 초기화되기 전에 비밀을 생성하는 것이 아니라 Python 시작 과정에만 국한됩니다.
시스템 urandom 수정
부팅 시 디스크에서 엔트로피 로드
엔트로피를 수집하는 데 최대 몇 분이 걸릴 수 있습니다. 시스템 초기화를 가속하기 위해 운영 체제는 종료 시 엔트로피를 디스크에 저장한 다음 부팅 시 디스크에서 엔트로피를 다시 로드합니다.
시스템이 한 번이라도 충분한 엔트로피를 수집하면, 디스크에서 엔트로피를 다시 로드하는 즉시 시스템 urandom이 빠르게 초기화됩니다.
가상 머신
가상 머신은 하드웨어에 직접 액세스할 수 없으므로 베어 메탈보다 엔트로피의 원천이 적습니다. 한 가지 해결책은 호스트에서 가상 머신으로 엔트로피를 전달하도록 virtio-rng device를 추가하는 것입니다.
임베디드 장치
임베디드 장치의 한 가지 해결책은 하드웨어 RNG를 연결하는 것입니다.
예를 들어 Raspberry Pi에는 하드웨어 RNG가 있지만 기본적으로 사용되지 않습니다. Hardware RNG on Raspberry Pi를 참조하십시오.
난수를 읽을 때의 서비스 거부
/dev/random이 아니라 /dev/urandom을 사용하십시오.
/dev/random 장치는 매우 특정한 사용 사례에서만 사용해야 합니다. Linux에서 /dev/random을 읽으면 블로킹될 가능성이 높습니다. 애플리케이션이 비밀값을 생성하기 위해 5초보다 오래 블로킹되는 것을 사용자는 좋아하지 않습니다. 암호화 키를 명시적으로 생성하는 경우와 같은 특정 사례에서만 필요합니다.
시스템에 사용 가능한 엔트로피가 없을 때 엔트로피를 사용할 수 있을 때까지 블로킹할지, 아니면 품질이 낮은 엔트로피로 대체할지를 선택하는 것은 보안과 실용성 사이에서 절충해야 하는 문제입니다. 선택은 사용 사례에 따라 달라집니다.
Linux에서 /dev/urandom은 안전하므로 /dev/random 대신 사용해야 합니다. Thomas Hühn의 Myths about /dev/urandom을 참조하십시오: “사실: /dev/urandom은 UNIX 계열 시스템에서 암호학적 무작위성의 권장 소스입니다.”
Linux에서 getrandom(size, 0)은 영원히 블로킹될 수 있습니다.
Python 이슈 #26839의 기원은 Debian bug report #822431입니다. 실제로 가상 머신에서 getrandom(size, 0)이 영원히 블로킹됩니다. systemd가 90초 후 블로킹된 프로세스를 종료했기 때문에 시스템이 부팅에 성공했습니다.
Load entropy from disk at boot과 같은 해결책은 이 버그의 위험을 줄입니다.
근거
Linux에서 /dev/urandom을 읽으면 urandom이 완전히 초기화되기 전, 즉 커널이 128비트의 엔트로피를 수집하기 전에 “약한” 엔트로피가 반환될 수 있습니다. Linux 3.17에는 urandom이 초기화될 때까지 블로킹할 수 있는 새로운 getrandom() 시스템 호출이 추가되었습니다.
Python 3.5.2에서 os.urandom()은 getrandom(size, GRND_NONBLOCK)을 사용하지만, getrandom(size, GRND_NONBLOCK)이 EAGAIN과 함께 실패하면 블로킹되지 않는 /dev/urandom을 읽는 방식으로 대체합니다.
보안 전문가들은 Cryptographically secure pseudo-random number generator (CSPRNG)로 구현되어 있기 때문에 암호화 키를 생성하는 데 os.urandom()을 사용할 것을 권장합니다. 또한 여러 가지 이유로 os.urandom()이 ssl.RAND_bytes()보다 선호됩니다.
이 PEP는 약한 엔트로피를 반환하지 않도록 os.urandom()이 블로킹 모드에서 getrandom()을 사용하게 수정하면서도 Python이 시작 시 블로킹되지 않도록 보장할 것을 제안합니다.
변경 사항
Linux에서 os.urandom()을 블로킹 방식으로 만들기
이 절에서 설명하는 모든 변경 사항은 Linux 플랫폼에만 해당합니다.
변경 사항:
- 시스템 urandom이 초기화될 때까지 os.urandom()을 블로킹하도록 수정합니다:
os.urandom()(C 함수_PyOS_URandom())이 Linux와 Solaris에서 항상getrandom(size, 0)(블로킹 모드)을 호출하도록 수정됩니다. - 새로운 비공개
_PyOS_URandom_Nonblocking()함수를 추가합니다: Linux와 Solaris에서getrandom(size, GRND_NONBLOCK)호출을 시도하지만,EAGAIN과 함께 실패하면/dev/urandom을 읽는 방식으로 대체합니다. - 블로킹되지 않는 시스템 urandom에서 해시 비밀값을 초기화합니다:
_PyRandom_Init()이_PyOS_URandom_Nonblocking()을 호출하도록 수정됩니다. - 이제
random.Random생성자는 블로킹되지 않는 시스템 urandom을 사용합니다. RNG의 시드 설정에 새_PyOS_URandom_Nonblocking()함수를 내부적으로 사용하도록 수정됩니다.
새로운 os.getrandom() 함수를 추가합니다.
새로운 os.getrandom(size, flags=0) 함수가 추가됩니다. Linux에서는 getrandom() 시스템 호출을, Solaris에서는 getrandom() C 함수를 사용합니다.
이 함수에는 새로운 플래그 2개가 제공됩니다.
os.GRND_RANDOM:/dev/urandom을 읽는 대신/dev/random에서 바이트를 읽습니다.os.GRND_NONBLOCK:os.getrandom()이 블로킹될 경우 BlockingIOError를 발생시킵니다.
os.getrandom()은 getrandom() 시스템 호출/C 함수의 얇은 래퍼이므로 해당 동작을 그대로 상속합니다. 예를 들어 Linux에서는 시스템 호출이 시그널에 의해 중단되면 요청한 바이트 수보다 적은 바이트를 반환할 수 있습니다.
os.getrandom()을 사용하는 예제
최선 노력 RNG
이식 가능한 비블로킹 RNG 함수의 예제입니다. OS urandom에서 무작위 바이트를 가져오고, 실패하면 random 모듈로 대체합니다.
def best_effort_rng(size):
# getrandom() is only available on Linux and Solaris
if not hasattr(os, 'getrandom'):
return os.urandom(size)
result = bytearray()
try:
# need a loop because getrandom() can return less bytes than
# requested for different reasons
while size:
data = os.getrandom(size, os.GRND_NONBLOCK)
result += data
size -= len(data)
except BlockingIOError:
# OS urandom is not initialized yet:
# fallback on the Python random module
data = bytes(random.randrange(256) for byte in range(size))
result += data
return bytes(result)
os.getrandom()을 사용할 수 없지만 os.urandom()은 차단될 수 있는 플랫폼에서는 이 함수가 이론적으로 차단될 수 있습니다.
wait_for_system_rng()
Linux 또는 Solaris에서 OS urandom이 초기화될 때까지 timeout초 동안 대기하는 함수의 예제입니다.:
def wait_for_system_rng(timeout, interval=1.0):
if not hasattr(os, 'getrandom'):
return
deadline = time.monotonic() + timeout
while True:
try:
os.getrandom(1, os.GRND_NONBLOCK)
except BlockingIOError:
pass
else:
return
if time.monotonic() > deadline:
raise Exception('OS urandom not initialized after %s seconds'
% timeout)
time.sleep(interval)
이 함수는 이식 가능하지 않습니다. 예를 들어 시스템 초기화 초기 단계의 FreeBSD에서는 os.urandom()이 이론적으로 블로킹될 수 있습니다.
최선 노력 RNG 생성
Linux에서 비블로킹 RNG를 생성하는 더 간단한 예제입니다. getrandom(size)가 블로킹될지 여부에 따라 Random.SystemRandom과 Random.Random 중 하나를 선택합니다.
def create_nonblocking_random():
if not hasattr(os, 'getrandom'):
return random.Random()
try:
os.getrandom(1, os.GRND_NONBLOCK)
except BlockingIOError:
return random.Random()
else:
return random.SystemRandom()
이 함수는 이식 가능하지 않습니다. 예를 들어 시스템 초기화 초기 단계의 FreeBSD에서는 random.SystemRandom이 이론적으로 블로킹될 수 있습니다.
대안
os.urandom()은 변경하지 않고 os.getrandom()을 추가합니다.
os.urandom()은 변경되지 않습니다. 절대 블로킹되지 않지만 시스템 urandom이 아직 초기화되지 않은 경우 약한 엔트로피를 반환할 수 있습니다.
새로운 os.getrandom()함수만 추가합니다(getrandom() 시스템 호출/C 함수의 래퍼입니다).
이식 가능한 코드를 작성하려면 secrets.token_bytes()함수를 사용해야 합니다.
이 변경의 문제는 사용자가 보안을 잘 이해하고 각 플랫폼을 잘 알고 있어야 한다는 점입니다. Python에는 “구현 세부 사항”을 숨기는 전통이 있습니다. 예를 들어 os.urandom()은 /dev/urandom 장치의 얇은 래퍼가 아닙니다. Windows에서는 CryptGenRandom()을 사용하고, OpenBSD에서는 getentropy()를 사용하며, Linux와 Solaris에서는 getrandom()을 시도하거나 /dev/urandom을 읽는 방식으로 대체합니다. Python은 이미 플랫폼에 따라 사용 가능한 최상의 시스템 RNG를 사용합니다.
이 PEP는 API를 변경하지 않습니다.
- 보안을 위해
os.urandom(),random.SystemRandom및secrets를 사용합니다. - 그 밖의 모든 용도에는
random모듈(random.SystemRandom제외)을 사용합니다.
os.urandom()에서 BlockingIOError 발생
제안
PEP 522: Linux에서 보안에 민감한 API에 BlockingIOError 허용입니다.
Python은 개발자를 대신하여 The bug 를 처리하는 방법을 결정해서는 안 됩니다. os.urandom()이 블로킹될 경우 즉시 BlockingIOError를 발생시키면 개발자가 이 상황을 처리할 방법을 선택할 수 있습니다.
- 예외를 포착하고 보안되지 않은 엔트로피 소스로 대체합니다. Linux에서는
/dev/urandom을 읽고, 전혀 안전하지 않은 Pythonrandom모듈을 사용하며, 시간을 사용하거나 프로세스 식별자를 사용하는 등의 방법이 있습니다. - 오류를 포착하지 않으면 전체 프로그램이 이 치명적인 예외와 함께 실패합니다.
보다 일반적으로, 이 예외는 무언가 잘못되었을 때 이를 알리는 데 도움이 됩니다. 애플리케이션은 os.urandom()을 기다리기 시작할 때 경고를 발생시킬 수 있습니다.
비판
사용 사례 2(웹 서버)의 경우, 보안되지 않은 엔트로피로 대체하는 것은 허용할 수 없습니다. 애플리케이션은 BlockingIOError를 처리해야 합니다. 완료될 때까지 os.urandom()을 폴링해야 합니다. 예:
def secret(n=16):
try:
return os.urandom(n)
except BlockingIOError:
pass
print("Wait for system urandom initialization: move your "
"mouse, use your keyboard, use your disk, ...")
while 1:
# Avoid busy-loop: sleep 1 ms
time.sleep(0.001)
try:
return os.urandom(n)
except BlockingIOError:
pass
정확성을 위해, 보안 비밀을 생성해야 하는 모든 애플리케이션은 The bug가 발생할 가능성이 낮더라도 BlockingIOError를 처리하도록 수정해야 합니다.
os.urandom()을 사용하지만 실제로 보안을 필요로 하지 않는 애플리케이션의 경우는 명확히 정의되어 있지 않습니다. 어쩌면 이러한 애플리케이션은 애초에 os.urandom()을 사용하지 말고, 항상 블로킹되지 않는 random 모듈을 사용해야 합니다. 보안을 위해 os.urandom()을 사용한다면, 위에서 설명한 사용 사례 2인 Use Case 2: Web server로 돌아갑니다. 개발자가 os.urandom()을 제거하고 싶지 않다면 코드를 수정해야 합니다. 예:
def almost_secret(n=16):
try:
return os.urandom(n)
except BlockingIOError:
return bytes(random.randrange(256) for byte in range(n))
문제는 The bug가 많은 애플리케이션을 수정해야 할 만큼 흔한지 여부입니다.
또 다른 더 간단한 선택지는 시스템 urandom이 초기화되기 전에는 시작을 거부하는 것입니다.:
def secret(n=16):
try:
return os.urandom(n)
except BlockingIOError:
print("Fatal error: the system urandom is not initialized")
print("Wait a bit, and rerun the program later.")
sys.exit(1)
Linux에서 os.urandom()이 차단되지도 않고 예외를 발생시키지도 않았던 Python 2.7, Python 3.4 및 Python 3.5.2와 비교하면, 이러한 동작 변경은 중대한 회귀로 볼 수 있습니다.
os.urandom()에 선택적 block 매개변수 추가
issue #27250: Add os.urandom_block()을 참조하십시오.
os.urandom()에 선택적 block 매개변수를 추가합니다. 기본값은 True(기본적으로 차단) 또는 False(블로킹되지 않음)일 수 있습니다.
첫 번째 기술적 문제는 모든 플랫폼에서 os.urandom(block=False)를 구현하는 것입니다. 잘 정의된 블로킹되지 않는 API(getrandom(size, GRND_NONBLOCK))를 제공하는 것은 Linux 3.17(및 이후 버전)과 Solaris 11.3(및 이후 버전)뿐입니다.
Raise BlockingIOError in os.urandom()의 경우, 이론적인 사용 사례(또는 적어도 매우 드문 사용 사례) 때문에 API를 더 복잡하게 만드는 것은 가치가 있어 보이지 않습니다.
Leave os.urandom() unchanged, add os.getrandom()의 경우, 문제는 API가 더 복잡해져 오류가 발생하기 쉬워진다는 것입니다.
승인
이 PEP는 accepted on 2016-08-08 by Guido van Rossum되었습니다.
부록
운영 체제 난수 함수
os.urandom()은 다음 함수를 사용합니다.
- OpenBSD: getentropy() (OpenBSD 5.6)
- Linux: getrandom() (Linux 3.17) – A system call for random numbers: getrandom()도 참조하십시오.
- Solaris: getentropy(), getrandom() (둘 다 Solaris 11.3이 필요합니다)
- UNIX, BSD: /dev/urandom, /dev/random
- Windows: CryptGenRandom() (Windows XP)
리눅스에서 /dev/random의 상태를 얻는 명령 (결과는 바이트 수):
$ cat /proc/sys/kernel/random/entropy_avail
2850
$ cat /proc/sys/kernel/random/poolsize
4096
os.urandom()을 사용하는 이유는 무엇입니까?
os.urandom()은 커널에서 구현되어 있으므로 사용자 공간 RNG의 문제가 없습니다. 예를 들어, 그 상태를 알아내기가 훨씬 더 어렵습니다. 일반적으로 CSPRNG를 기반으로 구축되므로, 상태가 “도난”당하더라도 이전에 생성된 숫자를 계산하기 어렵습니다. 커널은 엔트로피 소스에 대해 잘 알고 있으며 엔트로피 풀을 정기적으로 채웁니다.
이것이 바로 os.urandom()이 ssl.RAND_bytes()보다 선호되는 이유이기도 합니다.
Copyright
This document has been placed in the public domain.