TT Lab
블로그 시작하기
배우기 러닝패스 코스

비밀번호·키·비밀값 다루기

비밀번호를 느리게, 사람마다 다르게 저장한다

TT Lab 에서 이어서 보기

목표

회원 비밀번호 저장을 요구사항 시험으로 적고, PBKDF2 나 scrypt 와 솔트, 상수 시간 비교, 옛 저장값 바꾸기로 고칩니다.

왜 중요한가

저장값은 언젠가 새어 나갑니다. 빠른 해시에 솔트가 없으면 흔한 비밀번호부터 순식간에 맞혀지고 같은 비밀번호를 쓰는 사람이 한눈에 드러납니다. 저장값의 모양을 흉내 낸 빠른 해시도 있으니, 시험은 저장값의 매개변수로 다시 계산해 확인합니다.

단계

  1. /opt/app/appsec/sk-password-lab/ 의 파일을 /root/appsec/sk-password-lab/ 로 복사하고 python3 -m pytest -q test_functional.py 로 기존 기능 시험이 통과하는지 봅니다. REQUIREMENTS.md 를 끝까지 읽습니다.
  2. /root/appsec/sk-password-lab/test_security.py 에 요구 A(저장값은 PBKDF2(60만 번 이상) 또는 scrypt(n≥2^17·r≥8)에 매번 새 16바이트 솔트, 다시 계산하면 같다)를 확인하는 시험을 test_a_ 로 시작하는 이름으로 씁니다. 지금 코드에서 실패하고, 올바르게 고친 코드에서 통과해야 합니다.
  3. service.py 를 고쳐 요구 A 를 지킵니다. 채점기는 숨은 요구 A 시험과 여러분의 test_a_ 시험을 함께 돌립니다.
  4. 요구 B(비교는 hmac.compare_digest, 옛 SHA-256 과 낮은 비용의 저장값은 로그인에 성공할 때 새 해시로)를 확인하는 시험을 test_b_ 로 시작하는 이름으로 더합니다. 판정은 2단계와 같습니다.
  5. service.py 를 고쳐 요구 B 도 지킵니다.
  6. test_functional.py 가 그대로 통과하는지 봅니다. 채점기는 숨은 기능 시험도 함께 돌립니다.
  7. REQUIREMENTS.md 의 '감사 로그' 표대로 audit(...) 을 부릅니다(로그인은 auth.login, 비밀번호·저장값·없는 이메일은 남기지 않음).
  8. 채점기가 그럴듯한 오답을 하나씩 끼워 여러분의 test_security.py 를 돌립니다. 오답마다 시험이 하나 이상 실패해야 합니다.

참고

재료를 복사하고 기능 시험을 돌린다

/opt/app/appsec/sk-password-lab/ 의 파일을 모두 /root/appsec/sk-password-lab/ 로 복사하고, 그 디렉터리에서 python3 -m pytest -q test_functional.py 로 기존 기능 시험이 통과하는 것을 본다. REQUIREMENTS.md 를 끝까지 읽는다.

cp 로 디렉터리의 파일을 통째로 옮깁니다. 고치기 전에 지금 무엇이 되는지부터 확인해 두면, 나중에 무엇을 깨뜨렸는지 가릴 수 있습니다.

요구 A 를 시험으로 쓴다

/root/appsec/sk-password-lab/test_security.py 에 요구 A(저장값은 PBKDF2(60만 번 이상) 또는 scrypt(n≥2^17·r≥8)에 매번 새 16바이트 솔트, 다시 계산하면 같다)를 확인하는 시험을 test_a_ 로 시작하는 이름으로 쓴다. 이 시험은 지금 코드에서 실패하고, 올바르게 고친 코드에서 통과해야 한다.

hash_password 의 결과를 $ 로 나눠 알고리즘·반복 수·솔트·해시를 읽고, hashlib 로 직접 다시 계산해 같은지 보세요. 같은 비밀번호를 두 번 해시해 결과가 다른지도 확인합니다. 지금 코드는 64자 16진수를 돌려줍니다.

요구 A 를 지키도록 고친다

/root/appsec/sk-password-lab/service.py 를 고쳐 요구 A 를 지킨다. 채점기는 숨은 요구 A 시험과 여러분의 test_a_ 시험을 고친 코드에 돌린다.

os.urandom(16) 솔트와 hashlib.pbkdf2_hmac("sha256", 비밀번호 바이트, 솔트, 600000, dklen=32) 를 쓰고, 솔트와 해시는 base64 로 적습니다. verify_password 는 저장값에서 매개변수를 읽어 다시 계산하고 hmac.compare_digest 로 비교합니다.

요구 B 를 시험으로 쓴다

/root/appsec/sk-password-lab/test_security.py 에 요구 B(비교는 hmac.compare_digest, 옛 SHA-256 과 낮은 비용의 저장값은 로그인에 성공할 때 새 해시로)를 확인하는 시험을 test_b_ 로 시작하는 이름으로 더한다. 02 단계처럼 지금 코드에서 실패하고 올바른 코드에서 통과해야 한다.

compare_digest 를 감싸 부른 횟수를 세는 시험, 옛 SHA-256 값으로 로그인한 뒤 저장값이 새 모양으로 바뀌었는지 보는 시험, 틀린 비밀번호로는 바뀌지 않는지 보는 시험을 생각해 보세요.

요구 B 를 지키도록 고친다

/root/appsec/sk-password-lab/service.py 를 고쳐 요구 B 도 지킨다. 채점기는 숨은 요구 B 시험과 여러분의 test_b_ 시험을 고친 코드에 돌린다.

옛 값(소문자 16진수 64자)은 SHA-256 을 compare_digest 로 비교합니다. needs_rehash 를 만들고, login 이 성공했을 때만 그 값을 보고 다시 저장합니다.

기존 기능이 그대로인지 본다

고친 service.py 로 python3 -m pytest -q test_functional.py 가 그대로 통과하는지 본다. 채점기는 재료의 원본 기능 시험에 숨은 기능 시험을 더해 돌린다.

보안 수정이 정상 사용자를 막으면 그 수정은 되돌려집니다. 실패한 기능 시험의 이름이 어떤 정상 동작을 막았는지 알려 줍니다. 막을 것만 막았는지 요구사항의 '그대로 된다' 문장과 견주어 보세요.

감사 로그를 남긴다

REQUIREMENTS.md 의 '감사 로그' 표대로 audit(...) 을 부른다(로그인은 auth.login, 비밀번호·저장값·없는 이메일은 남기지 않음). 남기지 말라고 한 값은 어떤 항목에도 넣지 않는다.

login 의 세 갈래(없는 이메일, 틀린 비밀번호, 성공)마다 audit('auth.login', ...) 을 부릅니다. 없는 이메일이면 user 대신 known=False 만 남깁니다.

그럴듯한 오답을 시험이 붙잡는지 본다

채점기가 기준 코드에 그럴듯한 오답을 하나씩 끼워 /root/appsec/sk-password-lab/test_security.py 를 돌린다. 오답마다 시험이 하나 이상 실패하도록 필요한 시험을 더한다.

채점 메시지가 어떤 오답이 빠져나갔는지 알려 줍니다. 한 경로만 고친 수정이면 다른 경로를, 비교 방법이 틀린 수정이면 그 비교를 확인하는 시험을 더하세요. 요구사항의 문장 하나하나가 시험 하나가 될 수 있습니다.