키를 바꿀 수 있게 서명한다
목표
서명된 내려받기 링크의 키 ID·상수 시간 비교·키 회전과 은퇴를 요구사항 시험으로 적고, 키 묶음을 읽는 코드로 고칩니다.
왜 중요한가
키가 소스에 하나뿐이면 소스를 본 모두가 키를 갖고, 키를 바꾸는 순간 나간 링크가 모두 죽어서 아무도 바꾸지 않습니다. kid 와 겹치는 기간을 두면 회전이 평범한 운영 작업이 되고, 사고 날에도 같은 절차를 쓸 수 있습니다.
단계
/opt/app/appsec/sk-keyring-lab/의 파일을/root/appsec/sk-keyring-lab/로 복사하고python3 -m pytest -q test_functional.py로 기존 기능 시험이 통과하는지 봅니다.REQUIREMENTS.md를 끝까지 읽습니다./root/appsec/sk-keyring-lab/test_security.py에 요구 A(링크는 활성 키로 kid 를 실어 서명하고, 확인은 그 kid 의 키·compare_digest·만료로)를 확인하는 시험을test_a_로 시작하는 이름으로 씁니다. 지금 코드에서 실패하고, 올바르게 고친 코드에서 통과해야 합니다.service.py를 고쳐 요구 A 를 지킵니다. 채점기는 숨은 요구 A 시험과 여러분의test_a_시험을 함께 돌립니다.- 요구 B(회전하면 옛 키는 retire_at 전까지 살고, 그 뒤에는 거절되며 prune 으로 빠진다)를 확인하는 시험을
test_b_로 시작하는 이름으로 더합니다. 판정은 2단계와 같습니다. service.py를 고쳐 요구 B 도 지킵니다.test_functional.py가 그대로 통과하는지 봅니다. 채점기는 숨은 기능 시험도 함께 돌립니다.REQUIREMENTS.md의 '감사 로그' 표대로audit(...)을 부릅니다(거절은 link.rejected, 회전은 key.rotated, 정리는 key.pruned, 키와 서명은 남기지 않음).- 채점기가 그럴듯한 오답을 하나씩 끼워 여러분의
test_security.py를 돌립니다. 오답마다 시험이 하나 이상 실패해야 합니다.
참고
- 키 묶음은 시험 안에서 사전으로 만들어도 됩니다. 비밀값은
base64.b64encode(os.urandom(32)).decode()로 만드세요. - 링크의 쿼리는
urlsplit·parse_qs·urlencode로 고쳐 쓰면 kid·exp·sig 를 바꿔 보기 쉽습니다. - 정답 보기의
del SECRET줄은 apply_fix.py 에게 '소스의 SECRET 정의를 지워라' 라는 뜻입니다. - 흔한 실수: 모르는 kid 를 활성 키로 대신 확인하는 것, 회전하면서 옛 키를 바로 지우는 것, retire_at 을 적고 확인하지 않는 것.
- 시험은
cd /root/appsec/sk-keyring-lab && python3 -m pytest -q test_security.py로 돌립니다.-k test_a_를 붙이면 요구 A 시험만 돕니다. - 정답 보기의
python3 /opt/app/appsec/apply_fix.py service.py는 조각에 든 함수만 이름으로 찾아 바꿉니다. 직접 고친 다른 함수는 그대로 남습니다. - 실습 파드에는 볼륨이 없어 세션이 끝나면
/root/appsec가 사라집니다. 남기고 싶은 시험은 세션 안에서 옮겨 두세요.
재료를 복사하고 기능 시험을 돌린다
/opt/app/appsec/sk-keyring-lab/ 의 파일을 모두 /root/appsec/sk-keyring-lab/ 로 복사하고, 그 디렉터리에서 python3 -m pytest -q test_functional.py 로 기존 기능 시험이 통과하는 것을 본다. REQUIREMENTS.md 를 끝까지 읽는다.
cp 로 디렉터리의 파일을 통째로 옮깁니다. 고치기 전에 지금 무엇이 되는지부터 확인해 두면, 나중에 무엇을 깨뜨렸는지 가릴 수 있습니다.
요구 A 를 시험으로 쓴다
/root/appsec/sk-keyring-lab/test_security.py 에 요구 A(링크는 활성 키로 kid 를 실어 서명하고, 확인은 그 kid 의 키·compare_digest·만료로)를 확인하는 시험을 test_a_ 로 시작하는 이름으로 쓴다. 이 시험은 지금 코드에서 실패하고, 올바르게 고친 코드에서 통과해야 한다.
링크의 쿼리에 kid 가 있는지, kid 를 모르는 값으로 바꾸면 거절되는지, 같은 kid 의 다른 키 묶음으로는 확인되지 않는지(키가 소스에 있으면 확인돼 버립니다), now 가 exp 와 같을 때 거절되는지 보세요.
요구 A 를 지키도록 고친다
/root/appsec/sk-keyring-lab/service.py 를 고쳐 요구 A 를 지킨다. 채점기는 숨은 요구 A 시험과 여러분의 test_a_ 시험을 고친 코드에 돌린다.
서명할 글은 경로, 만료 시각, kid 를 줄바꿈으로 이은 것이고 서명은 HMAC-SHA256 의 16진수입니다. 확인할 때는 쿼리를 parse_qs 로 읽어 모양을 먼저 보고, kid 로 키를 고르고, hmac.compare_digest 로 비교한 뒤에 만료를 봅니다. SECRET 은 지웁니다.
요구 B 를 시험으로 쓴다
/root/appsec/sk-keyring-lab/test_security.py 에 요구 B(회전하면 옛 키는 retire_at 전까지 살고, 그 뒤에는 거절되며 prune 으로 빠진다)를 확인하는 시험을 test_b_ 로 시작하는 이름으로 더한다. 02 단계처럼 지금 코드에서 실패하고 올바른 코드에서 통과해야 한다.
링크를 만든 뒤 rotate 로 새 키를 올리고, 겹치는 기간 안과 밖에서 옛 링크를 확인해 보세요. prune 이 은퇴한 키만 빼고 활성 키는 남기는지도 시험할 거리입니다.
요구 B 를 지키도록 고친다
/root/appsec/sk-keyring-lab/service.py 를 고쳐 요구 B 도 지킨다. 채점기는 숨은 요구 B 시험과 여러분의 test_b_ 시험을 고친 코드에 돌린다.
rotate 는 옛 활성 키에 retire_at = now + overlap 을 적고 지우지 않습니다. verify_link 는 retire_at 이 지난 키를 거절하고, prune 은 활성 키가 아니면서 retire_at 이 지난 키를 뺍니다.
기존 기능이 그대로인지 본다
고친 service.py 로 python3 -m pytest -q test_functional.py 가 그대로 통과하는지 본다. 채점기는 재료의 원본 기능 시험에 숨은 기능 시험을 더해 돌린다.
보안 수정이 정상 사용자를 막으면 그 수정은 되돌려집니다. 실패한 기능 시험의 이름이 어떤 정상 동작을 막았는지 알려 줍니다. 막을 것만 막았는지 요구사항의 '그대로 된다' 문장과 견주어 보세요.
감사 로그를 남긴다
REQUIREMENTS.md 의 '감사 로그' 표대로 audit(...) 을 부른다(거절은 link.rejected, 회전은 key.rotated, 정리는 key.pruned, 키와 서명은 남기지 않음). 남기지 말라고 한 값은 어떤 항목에도 넣지 않는다.
거절하는 자리마다 audit('link.rejected', reason=..., kid=...) 를 부르는 작은 함수를 두면 편합니다. rotate 와 prune 에서도 기록하고, secret 과 sig 는 어디에도 넣지 않습니다.
그럴듯한 오답을 시험이 붙잡는지 본다
채점기가 기준 코드에 그럴듯한 오답을 하나씩 끼워 /root/appsec/sk-keyring-lab/test_security.py 를 돌린다. 오답마다 시험이 하나 이상 실패하도록 필요한 시험을 더한다.
채점 메시지가 어떤 오답이 빠져나갔는지 알려 줍니다. 한 경로만 고친 수정이면 다른 경로를, 비교 방법이 틀린 수정이면 그 비교를 확인하는 시험을 더하세요. 요구사항의 문장 하나하나가 시험 하나가 될 수 있습니다.