Proof index
AUTH가 지금 실행으로 붙들고 있는 주장 전부입니다. 명세 증명은 규격이 인쇄한 값을 재현하고, 공격 증명은 막아야 할 입력을 실제로 넣어 거부를 보입니다. 각 증명은 모듈 페이지에서 직접 실행할 수 있고, npm test가 전부 다시 실행합니다.
규격이 고정한 verifier → code_challenge 쌍을 그대로 재현한다.
형식이 깨진 verifier 는 challenge 유도 단계에서 예외로 끊긴다 — PKCE 없는 토큰 교환으로 조용히 되돌아가지 않는다.
AUTH 는 사용자 저장소를 소유하지 않는다 — 소비 서비스가 주입한 repository 포트로 조회하고, 이메일 정규화와 비밀번호 검증만 표준화한다.
없는 계정·틀린 비밀번호·잠긴 계정이 모두 같은 invalid 로 끝난다. 계정이 없어도 더미 해시를 실제로 검증해 응답 시간까지 맞춘다 — 회원 목록을 응답 차이로 긁어낼 수 없다.
6자리 코드는 짧아서 저장소가 유출되면 곧바로 추측 가능해진다 — 그래서 원문 대신 digest 만 저장하고, 전화번호도 용도별 가명으로만 남긴다.
6자리는 추측 가능한 공간이라 시도 제한이 방어의 핵심이다 — 한도를 넘기면 진짜 코드도 통하지 않는다. 코드를 가로채도 발급받은 번호가 아니면 검증되지 않는다.
링크를 요청한 브라우저는 토큰을 받지 못한다 — 메일함 접근을 증명한 사람만 링크를 쥔다. 저장소에는 이메일 원문 대신 가명이 남아 유출 시에도 수신자를 되찾을 수 없다.
한 번 쓴 링크는 즉시 죽는다 — 메일함이 나중에 털려도 지나간 링크로는 들어올 수 없다. 복귀 경로는 로컬 경로로만 제한해 로그인 직후 외부 사이트로 튕기는 open redirect 를 막는다.
규격이 고정한 시각 T=59 의 8자리 코드와 시간 구간(counter)을 그대로 재현한다.
검증에 성공한 counter 는 즉시 소비된다 — 어깨너머로 코드를 훔쳐봐도 같은 30초 안에서 재사용할 수 없다.
인증기에 건네는 challenge 는 서버가 발급한 일회용 값이고, 검증은 같은 값을 저장소에서 소비하면서 이뤄진다 — 서명 검증(표준 engine)은 이 결속 위에서만 의미를 갖는다.
서명이 유효해도 결속이 깨지면 거부한다 — 지나간 challenge, 다른 사이트의 rpId, 소유자가 다른 자격증명, 되감긴 counter 는 모두 인증 실패다. RP 구성 자체도 HTTPS 가 아니면 만들어지지 않는다.
이 증명은 흉내가 아니라 운영과 같은 PostgreSQL 테이블에 행을 쓰고 지운다 — 원자적 소비는 SQL 이 보장하며, 원문 토큰은 저장되지 않고 digest 만 남는다.
틀린 토큰은 시도 횟수를 올리고 한도를 넘기면 올바른 토큰마저 거부된다 — 무한 추측이 불가능하다. 용도가 다르면 digest 가 달라져 같은 토큰도 통하지 않는다.
토큰 종류를 헤더 typ 과 claim 양쪽에 못박고 서명 키 id 를 함께 실어, 검증 시점에 종류·키·발급자를 모두 확인할 수 있다.
refresh 토큰을 access 로 제시하는 종류 혼동, 서명 변조, 다른 발급자·청중, 폐기된 토큰이 모두 검증 단계에서 끊긴다.
기본값이 안전해야 한다 — HttpOnly 로 스크립트 접근을 막고, Secure 로 평문 전송을 막고, __Host- 접두사로 하위 도메인이 쿠키를 심을 수 없게 한다.
보안 속성을 빼는 구성은 조용히 허용되지 않는다 — 잘못 만든 쿠키가 나가는 것보다 만들지 못하는 편이 낫다.
issuer 를 기준으로 신뢰 정의를 찾고, 필수 claim 을 모두 요구한 뒤 이메일·검증 상태·표시 이름을 공통 계약으로 옮긴다.
서명이 유효해도 신뢰 목록에 없는 발급자면 거부하고, 청중과 nonce 가 어긋나면 재생·전달 공격으로 보고 끊는다.
왕복이 맞는 것과 규격대로 계산하는 것은 다른 주장이다 — 자작 알고리즘도 왕복은 맞는다. 여기서는 규격 문서에 인쇄된 키·nonce·AAD·평문을 넣어 인쇄된 암호문과 태그가 바이트 단위로 나오는지 본다.
암호문은 스스로를 설명한다 — 어떤 키로 어떤 IV 를 써서 만들었는지 담고 있어 키 교체 중에도 이전 값을 그대로 복호할 수 있다. 같은 평문도 매번 다른 암호문이 된다.
GCM 은 암호화와 무결성을 함께 보장한다 — 한 비트만 손대도 복호가 실패하고, 값이 묶인 맥락(AAD)이 다르면 같은 키로도 열리지 않는다. 다른 계정의 암호문을 가져다 쓸 수 없다.
AES-GCM과 같은 보장을 다른 내부 구조로 제공한다 — 규격이 인쇄한 키·nonce·AAD·평문을 넣으면 인쇄된 114바이트 암호문과 태그가 바이트 단위로 나온다. 룩업 테이블이 없어 가속 없는 CPU에서도 상수 시간으로 돈다.
Poly1305 태그는 GCM 태그와 같은 일을 한다 — 한 비트만 손대도 열리지 않는다. 그리고 접두사가 알고리즘을 못박으므로 AES 형식과 ChaCha 형식을 섞어 열려는 시도는 형식 단계에서 끊긴다.
저장 형식이 아니라 알고리즘 자체를 규격에 맞춘다 — 규격이 인쇄한 비밀번호·salt·파라미터를 넣으면 인쇄된 64바이트 키가 그대로 나온다. 메모리를 1 GiB 요구하는 규격의 마지막 벡터는 정책이 거부한다.
PBKDF2는 반복 횟수만 늘린다 — 메모리는 반복을 아무리 올려도 그대로다. 그래서 GPU·ASIC처럼 연산을 대량 병렬화하는 하드웨어에 약하다. scrypt는 메모리를 함께 요구해 병렬화 단가 자체를 올린다. 같은 '비밀번호 해시'라도 무엇을 비싸게 만드는지가 다르다.
검증값은 알고리즘·버전·비용 파라미터·salt 를 함께 실어 나른다 — 정책을 올려도 기존 해시를 그대로 검증할 수 있고, 같은 비밀번호라도 저장값이 서로 달라 한 번에 다량 대조가 불가능하다.
검증은 상수 시간 비교로 이뤄지고, 저장값을 손대면 검증이 통과하지 않는다. 정책 자체도 하한 아래로는 구성되지 않는다.
두 층을 따로 고정한다 — 아래층 HMAC-SHA-256 은 규격이 인쇄한 값을 재현하고, 위층 envelope 은 고정 키에서 항상 같은 문자열을 낸다. 위층이 고정돼야 다른 언어로 재구현해도 서로의 서명을 검증할 수 있다.
HMAC 은 내용을 숨기지 않는다 — 출처와 무결성만 보장하며, 키 교체 중에도 이전 키로 서명된 값을 식별해 재서명 대상으로 표시한다.
서명 검증은 상수 시간 비교로 이뤄지고, 같은 키라도 purpose 가 다르면 서명이 통하지 않는다 — 한 용도의 서명값을 다른 용도로 옮겨 쓸 수 없다.
직관적으로 보이는 `SHA-256(비밀키 + 메시지)`는 MAC이 아니다. 공격자가 비밀키를 몰라도 길이만 알면 뒤에 내용을 덧붙인 메시지의 MAC을 계산해 낼 수 있다 — 여기서 viewer 권한 토큰이 admin으로 바뀐다. 같은 입력을 HMAC에 넣으면 막힌다.
해시 함수 자체는 규격이 인쇄한 값을 재현하고, 그 위에 얹은 가명 구성은 고정 키에서 항상 같은 문자열을 낸다. 가명은 해시 원본이 아니라 용도와 키에 결속된 값이다.
대소문자·공백·표기가 달라도 같은 사람은 같은 가명으로 모인다 — 원문을 저장하지 않고도 동일인 판단이 가능하다.
가명은 키 기반 HMAC 이라 원문이 남지 않고, 용도(purpose)가 다르면 같은 사람이라도 값이 갈라진다 — 한 시스템의 가명 표를 훔쳐도 다른 용도의 가명과 대조할 수 없다.
용도마다 키를 따로 넣는 대신 마스터 하나에서 유도한다 — 같은 마스터라도 용도나 key id가 다르면 완전히 다른 키가 나오고, 유도된 키에서 마스터를 되돌릴 수 없다. 키 분리가 문서가 아니라 기본 동작이 되는 지점이다.
유도는 마스터의 엔트로피를 늘려주지 않는다 — 짧은 마스터로 아무리 많은 하위 키를 뽑아도 전부 그 마스터만큼만 안전하다. 그래서 마스터 하한을 유도 시점에 막는다.
새 서명은 항상 활성 키로 나가고 이전 키는 검증용으로만 남는다 — 교체 시점에 기존 값이 한꺼번에 무효가 되지 않는다.
잘못된 키 구성은 사용 시점이 아니라 구성 시점에 끊는다 — 약한 키로 서명된 값이 애초에 생기지 않게 한다.
검증자는 공개키만 있으면 된다 — 개인키 없이 서명을 확인할 수 있고, 그래서 검증할 수 있는 쪽이 위조는 못 한다. HMAC 과 갈리는 지점이 정확히 여기다. kid 는 이름이 아니라 공개키에서 유도된 지문이다.
공개키는 공개돼 있다 — 검증자가 토큰의 alg 헤더를 믿고 검증 방식을 고르면, 공격자가 alg 를 HS256 으로 바꾸고 그 공개키를 HMAC 키로 써서 토큰을 찍어낼 수 있다. 검증자가 알고리즘을 못박아야만 막힌다.
서명이 결정론적이라 규격의 알려진 답과 직접 대조할 수 있다 — ECDSA는 매번 다른 값을 내서 이 비교 자체가 불가능하다. 같은 키·같은 메시지는 몇 번을 서명해도 같은 64바이트가 나온다.
검증은 공개키·메시지·서명 셋이 정확히 맞을 때만 통과한다. 메시지를 한 바이트 바꾸거나 서명을 한 비트 뒤집으면 즉시 거부되고, 다른 키로 만든 서명은 형태가 유효해도 통과하지 못한다.
비밀을 보내지 않고 나눠 갖는다 — 공개키만 주고받았는데 양쪽이 같은 32바이트에 도달한다. 그 값은 회선에 한 번도 실리지 않으므로 도청자는 공개키 둘을 다 봐도 계산할 수 없다.
특별하게 고른 공개키를 보내면 상대의 개인키와 무관하게 공유 비밀이 전부 0이 된다 — 그러면 공격자도 같은 값을 안다. 이 결과를 그대로 키로 쓰면 합의가 무의미해지므로 계산 단계에서 끊는다.
base64url은 base64의 알파벳 두 글자를 바꾸고 패딩을 없앤 형태다 — URL에 그대로 실을 수 있다. 규격 벡터를 재현하고, 모든 바이트열이 인코딩 후 디코딩하면 원래대로 돌아오는지 확인한다.
"QQ"와 "QR"은 둘 다 0x41로 디코딩된다 — 마지막 문자의 남는 비트가 무시되기 때문이다. 문자열로 사용 완료 토큰을 관리한다면 마지막 글자만 바꿔 재사용할 수 있다. 왕복 검사가 이 부류를 한 번에 닫는다.
비교 결과는 같다·다르다 두 가지뿐이어야 한다 — 어느 위치에서 갈렸는지, 앞에서 몇 바이트가 맞았는지는 답의 일부가 아니라 비밀이다. 조기 종료 비교는 그 정보를 처리 시간으로 내보낸다.
공격자는 비밀을 한 번도 보지 못한다. 오직 비교가 몇 바이트를 봤는지만 관찰한다. 그런데도 8바이트 MAC이 2,048번 시도로 완전히 복원된다 — 전수 탐색이었다면 10^19번이 필요했다. 상수 시간 비교로 바꾸면 이 통로가 닫힌다.
엔트로피는 문자열 길이가 아니라 원본 바이트 수가 정한다 — 16진수 32자와 base64url 32자는 길이가 같아 보여도 담는 비트가 다르다. 이 모듈은 토큰에 128비트 하한을 걸고 그보다 짧으면 생성 자체를 거부한다.
올바른 생성기를 써도 범위로 접는 방법이 틀리면 편향이 생긴다. randomBytes(1)[0] % 10은 0~5를 26번, 6~9를 25번 낸다 — 통계가 아니라 산술이라 전수 열거로 확인된다. 해법은 나눠 접는 대신 벗어난 값을 버리고 다시 뽑는 것이다.
감사 로그는 사람이 읽는 문자열이 아니라 질의 가능한 구조다 — 중립적인 점 표기 이름과 UUID 상관관계 id 로 한 흐름을 이어 붙일 수 있다.
토큰·비밀번호·쿠키 같은 필드는 중첩·표기 변형까지 따라가며 거부한다 — 로그는 유출 경로가 되기 가장 쉬운 곳이라, 실수로 넣는 것 자체가 실패해야 한다.
권한 판단은 저장소가 아니라 정책 함수가 한다 — 같은 역할 표를 어떤 실행 환경에 두어도 같은 결정이 나온다.
권한 판단은 열림이 아니라 닫힘에서 출발한다 — 요구사항을 빠뜨린 호출은 통과가 아니라 거부이고, 주체를 못 찾아도 거부다.
한도까지는 허용하고 초과하면 차단하며, 차단은 윈도우가 아니라 cooldown 이 끝나야 풀린다 — 시간만 흘려보내서는 우회할 수 없다.
저장소가 유출되어도 어떤 IP·계정이 얼마나 시도했는지 읽어낼 수 없고, 한 scope 의 차단이 다른 scope 로 번지지 않는다. 안전하지 않은 정책 값은 구성 시점에 거부된다.
증명이 가리키는 모듈이 카탈로그에 없거나, `사용 가능` 모듈에 명세·공격 증명이 모두 있지 않으면 CI가 실패합니다 — 이 목록은 손으로 관리하는 문서가 아닙니다. 전체 모듈 보기