비밀번호는 훔치거나, 유출되거나, 재사용되면 그대로 뚫립니다. WebAuthn(FIDO2)은
아예 "비밀번호를 서버에 보내지 않는" 방식으로 이 문제를 없앱니다.
사이트마다 공개키-개인키 쌍을 새로 만들고, 개인키는 절대 기기 밖으로 나가지 않습니다.
로그인할 때는 서버가 던진 무작위 챌린지(challenge)에 개인키로 서명해서 "내가 그 개인키를
가지고 있다"는 것만 증명합니다. 아래 데모는 브라우저 내장 SubtleCrypto API로
실제 ECDSA(P-256) 키 쌍을 만들고 서명·검증까지 직접 수행합니다.
🔑 ① 패스키 등록 Registration
"등록"을 누르면 브라우저(인증기)가 이 사이트 전용 ECDSA 키 쌍을 생성합니다.
개인키는 로컬에만 저장되고, 공개키만 서버로 전송됩니다.
Credential ID—
서버 저장 공개키 (JWK x)—
기기 내 개인키🔒 추출 불가 (기기 내부)
🔓 ② 패스키 로그인 Authentication
서버가 매번 새로운 랜덤 챌린지를 보내고, 인증기가
개인키로 서명합니다. 서버는 저장해둔 공개키로 서명만 검증하면 됩니다 — 비밀번호는 등장하지 않습니다.
서버 챌린지—
서명 결과—
검증—
💀 ③ 서명 재전송(Replay) 공격 Attack
공격자가 지난번 로그인 때 오간 서명을 훔쳐서 그대로 다시
서버에 보내면 어떻게 될까요? 매번 챌린지가 랜덤으로 바뀌기 때문에 옛 서명은 새 챌린지와 맞지 않습니다.
📋 인증기 ↔ 서버 로그 실시간
// 대기 중... 먼저 패스키를 등록하세요
⚡ 인증 세리모니 흐름
① 서버: 챌린지 생성
→
② 인증기: 개인키 서명
↓
③ 서명값 전송
→
④ 서버: 공개키로 검증
핵심: 서버는 개인키를 본 적조차 없습니다.
데이터베이스가 통째로 유출돼도 공개키만 있으니 로그인은 위조할 수 없습니다.
비밀번호
패스키
서버에 저장되는 값
해시(유출 시 크래킹 시도 가능)
공개키(그 자체로 무해)
피싱 사이트에 입력
그대로 도용 가능
Origin 불일치 → 서명 거부
재사용 위험
여러 사이트 재사용 흔함
사이트별 별도 키 쌍
🐍 서버 측 검증 로직 (개념 코드)
# 등록 시 저장: credential_id, public_key@app.post("/webauthn/login")
async deflogin(cred_id: str, signature: bytes):
challenge = session_store[cred_id]["challenge"]
pubkey = db.get_public_key(cred_id)
# ✅ 서명 검증만 하면 끝 — 비밀번호 비교 없음if notverify(pubkey, signature, challenge):
raiseHTTPException(401)
# ✅ 사용한 챌린지는 즉시 폐기 → 재전송 무력화
session_store[cred_id]["challenge"] = Nonereturn {"status": "authenticated"}