AUTH가 실행해서 확인하는 주장을 모두 모았습니다. 명세 증명은 표준 문서에 실린 값이 그대로 나오는지 확인하고, 공격 증명은 막아야 할 입력을 실제로 넣어 거부하는지 확인합니다. 각 증명은 모듈 페이지에서 직접 실행할 수 있고, npm test를 돌릴 때마다 전부 다시 실행됩니다.
규격이 정해 둔 verifier와 code_challenge 쌍을 그대로 재현합니다.
형식이 깨진 verifier는 challenge를 유도하는 단계에서 예외로 끊깁니다. PKCE 없는 토큰 교환으로 조용히 돌아가지 않습니다.
AUTH는 사용자 저장소를 소유하지 않습니다. 소비 서비스가 주입한 repository 포트로 계정을 조회하고, 이메일 정규화와 비밀번호 검증만 표준으로 맡습니다.
없는 계정, 틀린 비밀번호, 잠긴 계정은 모두 같은 invalid로 끝납니다. 계정이 없을 때도 더미 해시를 실제로 검증해 응답 시간까지 맞춥니다. 그래서 응답 차이로 회원 목록을 긁어낼 수 없습니다.
6자리 코드는 짧아서 저장소가 유출되면 바로 추측할 수 있습니다. 그래서 원문 대신 digest만 저장하고, 전화번호도 용도별 가명으로만 남깁니다.
6자리 코드는 추측할 수 있는 범위라서 시도 제한이 방어의 핵심입니다. 한도를 넘기면 진짜 코드도 통하지 않습니다. 코드를 가로채도 발급받은 번호가 아니면 검증되지 않습니다.
링크를 요청한 브라우저는 토큰을 받지 못합니다. 메일함에 접근할 수 있는 사람만 링크를 손에 넣습니다. 저장소에는 이메일 원문 대신 가명이 남으므로, 저장소가 유출돼도 수신자를 알아낼 수 없습니다.
한 번 쓴 링크는 바로 무효가 됩니다. 나중에 메일함이 털려도 지난 링크로는 들어올 수 없습니다. 복귀 경로는 해석한 결과가 같은 origin 안에 머무는 로컬 경로만 허용해서, 로그인 직후 외부 사이트로 넘어가는 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 키로 삼아 토큰을 만들어 냅니다. 검증자가 알고리즘을 고정해야만 막을 수 있습니다.
Ed25519 서명은 결정론적이라 규격의 알려진 답과 바로 대조할 수 있습니다. 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로 번지지 않습니다. 안전하지 않은 정책 값은 구성 시점에 거부합니다.
실패 메시지는 호출자가 직접 쓰지 않습니다. 미리 정해 둔 오류 코드 표의 문장만 응답에 나가므로 임의의 문자열이 실릴 수 없습니다.
가장 흔한 유출 경로는 잘못된 입력으로 오류를 일으켰을 때 서버가 오류 메시지로 내부 구조를 드러내는 것입니다. 이 모듈에는 예외 내용이 응답으로 전달되는 경로가 아예 없습니다.
형식은 발급과 분리된 계약입니다. 자릿수가 넘치거나 모자란 표기는 조용히 고치지 않고 거부합니다. 고쳐 주면 같은 문서에 표기가 두 가지 생기기 때문입니다.
이 증명은 같은 조건에서 두 구현을 나란히 실행합니다. 읽은 뒤 쓰는 방식은 동시에 들어온 청구 여덟 건에 모두 같은 번호를 주고, 한 번에 끝나는 원자적 청구는 그렇지 않습니다. 부하가 낮으면 몇 달이 지나도 드러나지 않는 결함입니다.
하루의 시작과 끝은 지역의 시간대 규칙이 정합니다. 24시간을 더해 다음 날을 구하지 않고 다음 날짜의 시작을 새로 계산하므로, 하루가 23시간이나 25시간인 날에도 경계가 맞습니다.
하루 한도의 리셋 시각이 호스트 시간대를 따라가면 운영자가 생각하는 시각과 실제 리셋 시각이 어긋납니다. 한도는 걸려 있지만 풀리는 시점이 다릅니다. 실제로 한국 시각 0시부터 9시 사이의 기록이 전날로 집계되던 기간이 있었습니다.
잘못된 입력은 안전한 기본값으로 바꾸되, 조용히 바꾸지는 않습니다. 버린 항목의 이름을 함께 돌려주므로 잘못 만든 링크가 드러납니다.
정렬 열은 허용 목록에 있는 것만 쓰고, 페이지는 상한에서 자릅니다. 인증 없이 요청 한 번으로 숨긴 열을 읽거나 거대한 건너뛰기를 일으킬 방법이 없습니다.
증명이 가리키는 모듈이 카탈로그에 없거나, `사용 가능` 모듈에 명세 증명과 공격 증명 중 하나라도 빠지면 CI가 실패합니다. 이 목록은 사람이 손으로 고치는 문서가 아닙니다. 전체 모듈 보기