PEP 506 – 표준 라이브러리에 secrets 모듈 추가하기
- Author:
- Steven D’Aprano <steve at pearwood.info>
- Status:
- Final
- Type:
- Standards Track
- Created:
- 19-Sep-2015
- Python-Version:
- 3.6
- Post-History:
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
이 PEP는 토큰 생성처럼 흔히 쓰이는 보안 관련 함수를 제공하는 모듈을 Python 표준 라이브러리에 추가할 것을 제안합니다.
정의
이 제안에서 사용하는 몇 가지 일반적인 약어는 다음과 같습니다.
- PRNG:
의사 난수 제너레이터(Pseudo Random Number Generator)입니다. 바람직한 통계적 성질을 갖추고 무작위처럼 보이는 수를 만들어 내는 결정론적 알고리즘입니다.
- CSPRNG:
암호학적으로 강력한 의사 난수 제너레이터(Cryptographically Strong Pseudo Random Number Generator)입니다. 무작위처럼 보이면서도 예측하기 어려운 수를 만들어 내는 알고리즘입니다.
- MT:
메르센 트위스터(Mersenne Twister)입니다. 폭넓게 연구된 PRNG로, 현재
random모듈이 기본값으로 사용하고 있습니다.
근거
이 제안은 Python 표준 라이브러리 때문에 개발자가 의도치 않게 심각한 보안 오류를 저지르기가 너무 쉽다는 우려에서 출발했습니다. OpenBSD의 창시자인 Theo de Raadt는 Guido van Rossum에게 연락하여, 비밀번호, 보안 토큰, 세션 키와 같은 민감한 정보를 생성하는 데 MT를 사용하는 것에 대해 우려 [1] 를 표명했습니다.
random 모듈의 문서에는 기본값이 보안 용도에 적합하지 않다고 명시되어 있지만 [2], 많은 Python 개발자가 이 경고를 놓치거나 무시하거나 잘못 이해할 수 있다는 것이 중론입니다. 구체적으로는 다음과 같습니다.
- 개발자가 문서를 읽지 않아 경고를 보지 못했을 수 있습니다;
- 자신이 모듈을 사용하는 방식이 보안에 영향을 미친다는 사실을 깨닫지 못할 수 있습니다; 또는
- 문제가 생길 수 있다는 사실을 모른 채 모범 사례를 따르지 않는 웹사이트에서 코드를 복사했을(또는 기법을 배웠을) 수 있습니다.
Google에서 “python how to generate passwords”를 검색하면 첫 번째 [3] 로 나오는 결과는 random 모듈의 기본 함수를 사용하는 튜토리얼입니다 [4]. 이 튜토리얼은 웹 애플리케이션용으로 작성된 것은 아니지만, 비슷한 기법이 웹 애플리케이션에서도 쓰이고 있을 가능성이 높습니다. 두 번째 결과는 비밀번호 생성에 관한 StackOverflow 질문입니다 [5]. 채택된 답변을 포함해 대부분의 답변이 기본 함수를 사용합니다. 한 사용자가 기본값은 쉽게 뚫릴 수 있다고 경고하자, “너무 걱정하시는 것 같습니다.”라는 답이 돌아왔습니다 [6].
이는 (예를 들어) 비밀번호나 보안 토큰을 생성할 때 기존 random 모듈이 매력적인 함정(attractive nuisance)이 된다는 점을 강하게 시사합니다.
(좀 더 철학적인 성격의) 추가적인 동기는 이 아이디어를 처음 제안한 게시물 [7] 에서 찾아볼 수 있습니다.
제안
다른 제안들은 random 모듈의 기본 PRNG에 초점을 맞추어, 개발자가 보안을 신경 쓰지 않고도 그 위에 코드를 쌓아 올릴 수 있는 “기본적으로 안전한” 암호학적으로 강력한 기본 요소를 제공하는 것을 목표로 했습니다. (아래의 대안 항목을 참고하십시오.) 이 제안은 그와 다른 접근 방식을 취합니다.
- 표준 라이브러리는 이미 암호학적으로 강력한 기본 요소를 제공하지만, 많은 사용자가 그런 기본 요소가 있다는 사실도, 언제 사용해야 하는지도 모릅니다.
- 암호에 익숙하지 않은 사용자에게 안전한 코드를 작성하도록 요구하는 대신, 표준 라이브러리에 안전한 토큰 생성처럼 가장 흔한 요구에 맞춰 바로 쓸 수 있는 “배터리” 모음을 포함해야 합니다. 이 코드는 요구(“비밀번호 재설정 토큰은 어떻게 생성합니까?”)를 직접 충족하는 동시에, 개발자가 배울 수 있는 바람직한 관행의 예시 역할도 할 것입니다 [8].
이를 위해 이 PEP는 표준 라이브러리에 새 모듈을 추가할 것을 제안하며, 제안하는 이름은 secrets입니다. 이 모듈에는 보안과 관련된 흔한 작업을 위해 바로 쓸 수 있는 함수 모음과 몇 가지 저수준 기본 요소가 함께 들어갈 것입니다.
random 모듈은 하위 호환성을 유지하고, secrets는 비밀로 유지해야 하는 모든 것(비밀번호, 토큰 등)을 다룰 때 가장 먼저 찾는 모듈이 되도록 하자는 것이 제안의 내용입니다.
API와 구현
이 PEP는 secrets 모듈에 다음 함수들을 제안합니다:
- (예를 들어) 비밀번호 복구나 세션 키 등에 쓰기에 적합한 토큰을 다음 형식으로 생성하는 함수:
- 바이트열로,
secrets.token_bytes; - 16진수 숫자를 사용한 텍스트로,
secrets.token_hex; - URL에 안전한 base-64 인코딩을 사용한 텍스트로,
secrets.token_urlsafe.
- 바이트열로,
os.urandom을 직접 사용하거나random.SystemRandom을 사용하는, 시스템 CSPRNG에 대한 제한된 인터페이스.random모듈과 달리, 이 인터페이스는 시드 설정, 상태 조회나 설정, 또는 비균등 분포(distributions) 배포를 위한 메서드를 제공할 필요가 없습니다. 이 인터페이스는 다음을 제공해야 합니다:- 시퀀스에서 항목을 고르는 함수,
secrets.choice. - 주어진 개수의 무작위 비트 및/또는 바이트를 정수로 생성하는 함수,
secrets.randbits. - 0부터 주어진 상한까지의 반열린 구간에서 무작위 정수를 반환하는 함수,
secrets.randbelow[9].
- 시퀀스에서 항목을 고르는 함수,
- 타이밍 공격에 강하면서 텍스트나 바이트 다이제스트가 동등한지 비교하는 함수인
secrets.compare_digest.
이러한 용도를 지원하려고 random 모듈에 새 CSPRNG를 추가할 필요는 없으며 SystemRandom으로 충분하다는 것이 대체적인 합의로 보입니다.
Alyssa (Nick) Coghlan이 몇 가지 예시 구현을 제시했고 [10] Tim Peters가 최소한의 API를 제시했습니다 [11]. 이 아이디어는 “cryptography” 모듈의 이슈 트래커에서도 논의되었습니다 [12]. 다음 의사 코드를 실제 구현의 출발점으로 삼아야 합니다:
from random import SystemRandom
from hmac import compare_digest
_sysrand = SystemRandom()
randbits = _sysrand.getrandbits
choice = _sysrand.choice
def randbelow(exclusive_upper_bound):
return _sysrand._randbelow(exclusive_upper_bound)
DEFAULT_ENTROPY = 32 # bytes
def token_bytes(nbytes=None):
if nbytes is None:
nbytes = DEFAULT_ENTROPY
return os.urandom(nbytes)
def token_hex(nbytes=None):
return binascii.hexlify(token_bytes(nbytes)).decode('ascii')
def token_urlsafe(nbytes=None):
tok = token_bytes(nbytes)
return base64.urlsafe_b64encode(tok).rstrip(b'=').decode('ascii')
secrets 모듈 자체는 순수 Python으로 작성되며, 다른 Python 구현은 이를 수정 없이 쉽게 활용하거나 필요에 따라 맞게 고쳐 쓸 수 있습니다. 구현은 BitBucket에서 찾을 수 있습니다 [13].
기본 인자
어려운 질문 하나는 “내 토큰은 몇 바이트여야 합니까?”입니다. “token_*” 함수들에 기본 엔트로피 양을 제공하여 이 질문에 도움을 줄 수 있습니다. nbytes 인자가 None이거나 주어지지 않으면 기본 엔트로피가 사용됩니다. 이 기본값은 중간 수준의 보안 용도에 안전하다고 볼 수 있을 만큼 충분히 커야 하지만, 앞으로 바뀔 것으로 예상되며 유지보수 릴리스에서 바뀔 수도 있습니다 [14].
명명 규칙
한 가지 질문은 모듈에서 사용할 명명 규칙으로 [15], “randrange” 같은 C 스타일 명명 규칙을 쓸지, 아니면 “random_range” 같은 더 파이썬다운 이름을 쓸지입니다.
비공개 SystemRandom 인스턴스의 단순한 바운드 메서드(예: randrange)이거나 그러한 메서드를 얇게 감싼 래퍼인 함수는 익숙한 이름을 유지해야 합니다. 새로운 기능을 하는 함수(다양한 token_* 함수 등)에는 더 파이썬다운 이름을 사용할 것입니다.
대안
한 가지 대안은 random 모듈이 제공하는 기본 PRNG를 변경하는 것입니다 [16]. 이 대안은 상당한 회의론과 노골적인 반대에 부딪혔습니다:
- CSPRNG가 현재의 PRNG보다 느릴 수 있다는 우려가 있습니다(MT의 경우 현재 PRNG도 이미 상당히 느립니다).
- 일부 애플리케이션(과학 시뮬레이션, 게임플레이 재현 등)에는 PRNG에 시드를 주어 알려진 상태로 만드는 기능이 필요한데, CSPRNG에는 설계상 이 기능이 없습니다.
random모듈의 또 다른 주요 용도는 초보자가 작성하는 간단한 “숫자 맞히기” 게임이며, 많은 사람이 그런 게임을 만들기 더 어렵게 할 수 있는random모듈 변경을 매우 꺼립니다.random모듈에서 MT를 제거하자는 제안은 없지만, CSPRNG가 아닌 생성기를 명시적으로 선택해야 한다는 발상이나 하위 호환성을 깨는 변경에 대해서는 상당한 반감이 있었습니다.- 실제로 입증된 MT 공격은 대개 PHP 애플리케이션을 대상으로 합니다. PHP의 MT 버전은 부실한 시드 설정 기법 때문에 Python 버전보다 훨씬 공략하기 쉬운 대상이라고 여겨집니다 [17]. 따라서 Python 애플리케이션에 대한 공격이 입증되지 않은 상황에서는 많은 사람이 하위 호환성을 깨는 변경에 반대합니다.
Alyssa Coghlan은 기본적으로 시스템 CSPRNG를 사용하는, 전역적으로 설정 가능한 PRNG에 관한 이전 제안을 내놓았으나, 이후 이 제안을 지지하며 그 제안을 철회했습니다.
다른 언어와의 비교
- PHP
PHP에는 기본적으로 마이크로초 단위의 현재 시각을 기반으로 13자 문자열을 반환하는
uniqid[18] 함수가 있습니다. Python 문법으로 옮기면 다음과 같은 시그니처를 가집니다:def uniqid(prefix='', more_entropy=False)->str
PHP 문서는 이 함수가 보안 목적에 적합하지 않다고 경고합니다. 그럼에도 불구하고 성숙하고 잘 알려진 여러 PHP 애플리케이션이 이 함수를 보안 목적으로 사용합니다(출처 필요).
PHP 5.3 이상에는
openssl_random_pseudo_bytes[19] 함수도 포함되어 있습니다. Python 문법으로 옮기면 대략 다음과 같은 시그니처를 가집니다:def openssl_random_pseudo_bytes(length:int)->Tuple[str, bool]
이 함수는 주어진 길이의 의사 난수 바이트 문자열과, 그 문자열이 암호학적으로 강력한 것으로 간주되는지를 나타내는 불리언 플래그를 반환합니다. PHP 매뉴얼에 따르면 오래되었거나 결함이 있는 플랫폼을 제외하면 True 이외의 값이 반환되는 경우는 드물어야 합니다.
- JavaScript
다소 피상적인 검색 결과 [20], JavaScript에는 강력한 난수 값을 생성하는 잘 알려진 표준 함수가 없는 것으로 보입니다.
Math.random은 심각한 약점 때문에 암호학적 용도에 적합하지 않은데도 자주 사용됩니다 [21]. 최근 몇 년 사이 대부분의 브라우저가window.crypto.getRandomValues[22]를 지원하게 되었습니다.Node.js는 풍부한 암호화 모듈인
crypto[23]를 제공하며, 그 대부분은 이 PEP의 범위를 벗어납니다. 이 모듈에는 무작위 바이트를 생성하는 단일 함수인crypto.randomBytes가 포함되어 있습니다. - Ruby
Ruby 표준 라이브러리에는 다음 메서드를 포함하는
SecureRandom[24] 모듈이 있습니다:- base64 - Base64로 인코딩된 무작위 문자열을 반환합니다.
- hex - 무작위 16진수 문자열을 반환합니다.
- random_bytes - 무작위 바이트 문자열을 반환합니다.
- random_number - 인자에 따라 range(0, n) 범위의 무작위 정수 또는 0.0과 1.0 사이의 무작위 부동소수점 수를 반환합니다.
- urlsafe_base64 - URL에 안전한 무작위 Base64 인코딩 문자열을 반환합니다.
- uuid - 버전 4 무작위 범용 고유 식별자(Universally Unique IDentifier)를 반환합니다.
모듈의 이름은 무엇이어야 합니까?
Python의 선(Zen of Python)에 나오는 “네임스페이스는 정말 훌륭한 아이디어입니다(Namespaces are one honking great idea)”라는 공안을 인용하며 “random.safe” 하위 모듈을 추가하자는 제안이 있었습니다. 그러나 Zen의 저자인 Tim Peters는 이 아이디어에 반대 의사를 밝혔으며 [25], 최상위 모듈을 권장합니다.
지금까지 python-ideas 메일링 리스트의 논의에서 “secrets”라는 이름은 어느 정도 지지를 받았고, 강한 반대는 없었습니다.
같은 이름의 서드파티 모듈이 이미 존재하지만 [26], 사용되지 않고 방치된 것으로 보입니다.
자주 묻는 질문
- Q: 이것이 실제로 문제입니까? MT는 누구도 그 출력을 예측할 수 없을 만큼 충분히 무작위적이지 않습니까?
A: 보안 전문가들의 공통된 의견은 MT가 보안 맥락에서 안전하지 않다는 것입니다. MT의 내부 상태를 재구성하여 [27] [28] 과거와 미래의 모든 값을 예측하는 것은 어렵지 않습니다. 무작위성을 위해 MT를 사용하는 시스템에 대해 알려진 실제 공격이 여러 가지 있습니다 [29].
- Q: PHP에 대한 공격은 그렇다 치더라도, Python 소프트웨어에 대해 알려진 공격도 있습니까?
A: 그렇습니다. 적어도 Zope와 Plone에는 취약점이 있었습니다. Hanno Schlichting은 다음과 같이 언급했습니다 [30]:
"In the context of Plone and Zope a practical attack was demonstrated, but I can't find any good non-broken links about this anymore. IIRC Plone generated a random number and exposed this on each error page along the lines of 'Sorry, you encountered an error, your problem has been filed as <random number>, please include this when you contact us'. This allowed anyone to do large numbers of requests to this page and get enough random values to reconstruct the MT state. A couple of security related modules used random instead of system random (cookie session ids, password reset links, auth token), so the attacker could break all of those."
Christian Heimes는 2012년에 이 문제를 Zope 보안 팀에 보고했으며 [31], 관련 CVE 취약점이 최소 두 건 있고 [32], Django에는 이 문제에 대한 우회책이 최소 하나 있습니다 [33].
- Q: 이것은 SSL과 같은 전문 암호화 소프트웨어의 대안입니까?
A: 아닙니다. 이것은 “배터리 포함(batteries included)” 해법이지, 모든 기능을 갖춘 “원자로”가 아닙니다. 이것은 몇 가지 기본적인 보안 오류를 완화하기 위한 것이지, 보안과 관련된 모든 문제의 해결책이 되려는 것이 아닙니다. Alyssa Coghlan이 자신의 이전 제안을 언급하며 한 말을 인용하면 다음과 같습니다 [34]:
"...folks really are better off learning to use things like cryptography.io for security sensitive software, so this change is just about harm mitigation given that it's inevitable that a non-trivial proportion of the millions of current and future Python developers won't do that."
- Q: 비밀번호 제너레이터는 어떻습니까?
A: 비밀번호 생성기에 대한 요구 사항은 너무 다양해서 표준 라이브러리에 잘 맞지 않는다는 것이 공통된 의견입니다 [35]. 모듈의 최초 릴리스에는 비밀번호 제너레이터가 포함되지 않으며, 대신 문서에 레시피 형태로 제공될 것입니다(
itertools모듈의 레시피와 같은 방식) [36]. - Q: Linux에서
secrets는 /dev/random(블로킹됨)을 사용합니까, 아니면 /dev/urandom(블로킹되지 않음)을 사용합니까? 다른 플랫폼은 어떻습니까?A:
secrets는os.urandom과random.SystemRandom을 기반으로 하며, 이들은 운영 체제가 제공하는 최선의 암호학적 무작위성 소스에 대한 인터페이스입니다. Linux에서는/dev/urandom일 수 있고 [37], Windows에서는CryptGenRandom()일 수 있지만, 자세한 구현 세부 사항은 문서 및/또는 소스 코드를 참조하십시오.
참고 문헌
Copyright
This document has been placed in the public domain.