Sarah는 피싱 이메일 링크를 클릭했다. 구글 로그인 페이지처럼 보이는 사이트가 열렸다.
도메인이 evil-google-security-alert.com이라는 것을 미처 확인하지 못한 채 서둘러 비밀번호를 입력하고 TOTP 6자리 코드도 넣었다. 그 순간 공격자는 중간 서버에서 그 값을 받아 실제 구글에 그대로 입력했다. 인증이 통과됐다. 비밀번호와 MFA를 모두 갖췄어도 AiTM(Adversary-in-the-Middle) 피싱 앞에선 아무 소용이 없었다.
이 공격이 성립하는 이유는 구조적이다. Sarah가 입력한 값들이 어디서든 재사용 가능하다는 것이다. 비밀번호는 서버에 저장된 해시와 대조하기 위해 전송되는 값이고, TOTP는 30초 유효 기간의 숫자 코드다. 두 값 모두 공격자 서버를 경유해도 원본 값이 그대로 살아있다. 채널이 오염됐어도 값 자체는 유효하다.
FIDO2는 이 "재사용 가능하다"는 구조를 바꾼다. 자격증명이 특정 도메인에 암호학적으로 묶이도록 설계되어 있어서, 공격자가 중간에서 아무리 값을 가로채도 다른 도메인에서 재사용할 수 없다. 피싱 사이트에서는 서명 생성 자체가 거부된다.
비밀번호가 가진 구조적 결함
비밀번호 인증 모델은 세 가지 전제 위에 서 있다. 인증할 때 그 값이 채널을 통해 전송된다. 서버는 그 값(또는 파생값)을 저장해야 한다. 사용자만 그 값을 알고 있다.
이 세 전제가 각각 공격 표면이다.
- "채널을 통해 전송된다"는 전제는 방금 본 AiTM 시나리오의 근거다. HTTPS가 전송 구간을 암호화하지만, 그 보호는 피싱 공격을 막아주지 않는다. 사용자 → 공격자 서버 구간은 암호화되어 있어도, 공격자는 값을 그대로 읽어 실제 서버에 재전송할 수 있다.
- "서버가 저장해야 한다"는 전제는 서버가 침해되면 수백만 계정의 자격증명이 한 번에 노출된다는 뜻이다. LinkedIn은 2012년 침해 이후 2016년 재조사에서 1억 1700만 건 전체 규모가 드러났고, Adobe 2013년 사건에서도 1억 5천만 개 계정이 유출됐다. bcrypt나 Argon2로 해시해도 오프라인 크래킹 공격 시간을 늘릴 뿐이다.
- "사용자만 안다"는 전제는 소셜 엔지니어링(피싱, 사칭 전화 등), 키로거 앞에서 너무 쉽게 깨진다.
TOTP 기반 MFA는 일부를 완화한다. 비밀번호 단독보다는 낫다. 하지만 "채널을 통해 전송된다"는 전제는 그대로다. 코드를 실시간으로 가로채는 AiTM 공격 앞에서 TOTP는 30초짜리 방어막에 불과하다.
근본 문제는 비밀번호와 OTP 코드가 서버와 공유되는 재사용 가능한 값(shared secret)이라는 구조다. 패치가 아니라 구조 교체가 필요한 이유다. 그 교체의 방향이 Passwordless(비밀번호 없는 인증)을 통해 서버와 공유되는 비밀 자체를 없애는 것이다.
FIDO2는 이 원칙을 브라우저, 인증기, 서버 세 구성 요소에 걸쳐 표준화한 스펙이다.
FIDO2의 구조 — WebAuthn과 CTAP의 역할 분리
FIDO2는 두 개의 독립된 스펙으로 구성된다.
WebAuthn은 W3C 표준으로, 브라우저(또는 앱)와 서버(Relying Party) 사이의 인터페이스를 정의한다. navigator.credentials.create()와 navigator.credentials.get()이 WebAuthn의 핵심 인터페이스다.
CTAP(Client to Authenticator Protocol)은 FIDO Alliance 표준으로, 클라이언트(브라우저 또는 플랫폼)와 인증기 사이의 통신 방식을 정의한다. USB, NFC, 블루투스 등 물리 전송 계층과 무관하게 동작하는 프로토콜 레이어다.

이 분리에는 이유가 있다. WebAuthn은 인증기가 물리적으로 어떻게 연결되어 있는지 알 필요가 없다. CTAP은 서버가 자격증명을 어떻게 검증하는지 알 필요가 없다. 인증기가 USB 하드웨어 키이든, 기기 내부 Secure Enclave이든, CTAP 레이어가 동일한 인터페이스를 제공한다. 서버 입장에서는 인증기 종류에 무관하게 동일한 WebAuthn 흐름으로 처리된다.
등록과 인증 — 내부에서 일어나는 일
등록(Registration)

등록 과정에서 생성되는 Authenticator Data가 핵심이다. 인증기가 서명해서 반환하는 구조체로, 다음 필드를 포함한다.
| 필드 | 내용 | 역할 |
| rpIdHash | rpId의 SHA-256 해시 | 이 자격증명이 어느 도메인에 묶여 있는지 |
| flags (UP) | User Presence 비트 | 사용자가 인증기 앞에 존재하는지 확인 |
| flags (UV) | User Verification 비트 | 생체인증이나 PIN으로 신원 검증 여부 |
| signCount | 카운터 값 | 이 자격증명이 몇 번 사용됐는지 (클론 감지용) |
| attestedCredentialData | credentialId + 공개키 | 등록 시에만 포함 |
- UP(User Presence)는 물리적 터치나 버튼 클릭처럼 사용자가 인증기 앞에 있다는 최소 확인이다.
- UV(User Verification)는 생체인증이나 PIN처럼 그 사람이 맞는지 검증하는 더 강한 확인이다.
엔터프라이즈 환경에서는 UV 플래그가 설정된 응답만 받아들이도록 서버 정책을 구성한다. UP만으로는 누가 터치했는지 알 수 없기 때문이다.
인증(Authentication)

등록과의 차이는 명확하다. 등록에서는 키 쌍을 생성하고 공개키를 서버에 전달했다. 인증에서는 새로 생성하는 것이 없다. 서버가 보낸 challenge를 등록 시 만든 개인키로 서명해서 돌려줄 뿐이다. 비밀을 주고받는 과정이 전혀 없이 서버는 이미 가진 공개키로 서명을 검증한다.
challenge가 매 인증마다 새로운 난수인 것도 중요하다. 동일한 서명값을 다시 제출하는 replay attack이 원천 차단된다.
Sign Counter — 클론 감지 메커니즘
signCount는 표면적으로는 단순한 카운터처럼 보이지만, 그 역할은 인증기 복제 감지다.
정상 흐름에서 signCount는 인증할 때마다 증가한다. 서버는 마지막으로 확인한 signCount를 저장한다. 다음 인증에서 받은 signCount가 저장된 값 이하라면, 누군가 이 자격증명을 복사해 이전 상태에서 사용하고 있을 가능성이 있다. 인증기가 복제됐거나 특정 시점의 백업 상태로 복원됐을 가능성을 나타내는 신호다.
단, Passkey처럼 여러 기기에 동기화된 자격증명에서는 signCount를 0으로 고정하는 경우가 있다. 기기마다 독립적으로 카운터가 늘어나면 어느 쪽이 "최신"인지 판단할 수 없기 때문이다. Apple과 Google의 Passkey 구현이 signCount를 0으로 고정하는 것도 이 트레이드오프를 의식한 설계 결정이다. signCount 기반 클론 감지는 복사가 불가능한 물리적 하드웨어 키(YubiKey)에서 가장 의미 있다.
Origin Binding — 피싱이 원천 차단되는 이유
FIDO2가 AiTM 피싱을 막는 핵심 메커니즘은 origin binding이다.
등록 시 서버(Relying Party)는 자신이 사용할 rpId를 직접 지정해서 요청을 내린다. 브라우저는 이 rpId가 현재 페이지의 origin에서 유효한 서픽스인지 검증한 뒤, 검증된 rpId를 인증기에 전달한다. 인증기는 이 rpId로 키 쌍을 생성하고, 생성된 키 쌍은 이 rpId에만 응답하도록 내부적으로 묶인다.
인증 시 브라우저는 현재 페이지의 origin을 기반으로 rpId의 유효성을 재확인하고, 검증된 rpId를 인증기에 전달한다. 인증기는 전달받은 rpId의 해시가 등록 시 저장한 rpIdHash와 일치하는지 검증한다. 일치하지 않으면 서명 생성 자체를 거부한다.
AiTM 피싱 시나리오에서 공격자 사이트는 https://evil-google.com이다. 브라우저는 현재 origin(evil-google.com)의 서픽스여야 하는 rpId만 인증기에 전달할 수 있다. 인증기는 이 rpId의 해시가 등록된 google.com rpId의 해시와 다르다는 것을 확인하고 서명을 거부한다. 공격자는 사용자의 인증 시도를 중계할 수 없다. 피싱 사이트에 넘겨줄 수 있는 서명값 자체가 만들어지지 않는다.
설계의 미묘한 지점 하나: rpId는 origin의 등록 가능한 도메인(registrable domain)의 서픽스여야 한다. example.com에 등록된 자격증명은 app.example.com에서도 사용할 수 있다. 반대로 rpId를 app.example.com으로 설정하면 auth.example.com에서는 사용 불가다. 서브도메인별로 자격증명을 격리할지, 도메인 전체에서 공유할지는 서비스 설계에서 결정해야 한다.
인증기의 종류와 트레이드오프
Platform Authenticator
기기 내부에 내장된 인증기다. Apple 기기의 Secure Enclave, Windows Hello의 TPM이 여기에 해당한다. 앞의 포스트에서 다뤘듯, TPM과 Secure Enclave는 물리적으로 격리된 보안 영역에 키를 저장하며 운영체제도 그 키에 직접 접근할 수 없다.
FIDO2 관점에서 Platform Authenticator의 동작은 이렇다. 개인키는 Secure Enclave 또는 TPM 내부에서 생성되고 그 안에만 존재한다. 서명 연산을 요청하면, 생체인증이나 PIN이 올바를 때만 내부에서 서명 연산이 수행되고 서명값만 외부로 나온다. 키 자체는 어떤 경우에도 외부로 나가지 않는다.
편의성은 높다. 별도 기기가 필요 없고, 이미 쓰는 지문이나 얼굴 인식으로 인증된다. 단점은 기기 종속성이다. Passkey가 없다면.기기를 바꾸거나 잃으면 해당 자격증명을 다시 등록해야 한다.
Roaming Authenticator
YubiKey처럼 별도 하드웨어 보안 키다. USB, NFC, 블루투스로 연결된다. 내부에 자체 보안 칩이 있어 개인키를 저장하고 서명 연산을 수행한다.
기기에 묶이지 않아 어떤 기기에서도 일관된 피싱 저항 인증을 제공하며, 노트북을 교체해도 YubiKey 하나로 그대로 쓸 수 있다. 단점은 YubiKey 자체의 관리다. YubiKey를 분실하면 복구 절차가 필요하고, 매번 지참해야 한다. 엔터프라이즈 환경에서 인프라 관리자, 특권 계정처럼 고위험 역할에 Roaming Authenticator를 강제하는 방식으로 활용된다.
Attestation — 인증기의 신원 증명
등록 시 서버는 단순히 공개키를 받는 것이 아니라, 이 키가 어떤 종류의 인증기에서 생성됐는지도 확인할 수 있다. 이것이 Attestation이다.
인증기는 등록 시 자신이 어떤 기종인지, 해당 기종 제조사의 개인키로 서명한 인증서(certificate)를 함께 전달한다. 서버는 FIDO Alliance의 Metadata Service(MDS)에서 해당 인증기 기종의 공개키를 조회해 이 서명을 검증할 수 있다. "이 자격증명은 YubiKey 5C NFC에서 생성됐다"는 것을 암호학적으로 확인하는 방식이다.
엔터프라이즈 정책에서 이를 활용하면 "승인된 기기의 Secure Enclave 또는 특정 YubiKey 모델에서 생성된 자격증명만 허용"하는 제약을 걸 수 있다. 개인 소유 스마트폰에서 생성된 Passkey는 Attestation 검증 단계에서 거부하는 식이다.
Passkey — 동기화, 사용성, 그리고 Enterprise
Passkey는 FIDO2 자격증명을 기기 간에 동기화 가능하게 만든 구현이다. FIDO2의 기기 종속성 문제를 해결한다.
개인키가 클라우드에 올라가도 안전한 이유
개인키가 클라우드에 올라간다는 말에 보안 우려가 생길 수 있다. 실제로는 이렇게 동작한다.
자격증명(개인키 포함)은 기기의 Secure Enclave(또는 동급의 하드웨어 보안 모듈)가 관리하는 암호화 키로 E2E 암호화된 채 클라우드에 동기화된다. 이 암호화 키는 Secure Enclave 밖으로 나가지 않고, 생체인증이나 PIN이 올바를 때만 내부에서 복호화 연산이 수행된다. 키체인 제공자는 Apple iCloud Keychain, Google Password Manager, 1Password 등 여러 가지가 있지만, 보안 모델은 동일하다.
iCloud Keychain을 예로 들면, Apple 서버에는 Secure Enclave가 생성한 키로 Apple도 복호화할 수 없게 암호화된 바이트만 전달된다. 다른 기기에서 Passkey를 사용할 때는, 클라우드에서 암호화된 자격증명을 다운로드한 뒤 해당 기기의 생체인증이나 PIN으로 복호화해서 사용한다.

이 구조에서 클라우드는 암호화된 바이트만 저장하는 보관소 역할을 한다. 복호화는 항상 기기 위에서, 기기의 생체인증이나 PIN을 통과한 뒤에만 이루어진다.
Discoverable Credential — 사용자 이름 없이 로그인이 가능한 이유
FIDO2 자격증명은 저장 위치에 따라 두 종류로 나뉜다. 일반 자격증명은 credentialId를 서버에만 보관한다. Discoverable Credential(과거 명칭: Resident Key)은 credentialId와 사용자 정보를 인증기 내부에도 함께 보관한다.
이 차이가 인증 흐름에 영향을 준다. 일반 자격증명은 서버 지정 방식으로 동작한다. 인증 시 서버가 allowCredentials 목록에 credentialId를 담아 "이 키로 서명해라"고 지정해야 한다.
즉, 사용자가 누구인지 먼저 알아야(아이디 입력) 서버가 해당 credentialId를 찾아 요청할 수 있다.
Discoverable Credential은 인증기가 rpId를 기준으로 내부 자격증명 목록을 조회하고 적합한 것을 골라 서명까지 처리한다. 서버는 allowCredentials를 비워서 요청을 보내면 된다. 인증 흐름의 주도권이 서버에서 인증기로 넘어가는 것이다. 사용자 입장에서는 아이디 입력 없이 생체인증 한 번으로 로그인이 완성된다.
Passkey와 Discoverable Credential의 조합이 Passwordless 경험을 실제로 완성하는 방식이다.
Cross-device Flow
Passkey가 없는 기기, 또는 다른 플랫폼의 기기에서 인증할 때는 Cross-device flow를 사용한다.
로그인하려는 기기(예: 업무용 Windows 노트북)에서 QR 코드가 표시되면, Passkey가 있는 모바일 기기(iPhone)로 스캔한다. 이후 블루투스 근접성을 확인해 원격 공격이 아님을 검증한 뒤, iPhone의 Passkey로 서명을 생성해 노트북에 전달하고, 노트북이 최종적으로 서버에 전송한다.
여기서 블루투스를 쓰는 이유가 흥미롭다. 블루투스 근접성 검증은 "QR 코드를 스캔한 폰이 실제로 같은 공간에 있다"는 물리적 존재를 확인하기 위해서다. 원격 피싱 사이트에서 QR 코드를 표시해 인증을 유도하더라도, 피싱 서버는 블루투스 거리(수 미터) 이내에 있을 수 없으므로 이 근접성 채널이 성립하지 않는다.
Enterprise에서 Passkey를 다룰 때
Passkey는 편의성을 높이는 수단이지만, 기업 환경에서는 추가 고려가 필요하다.
기업이 관리하는 기기에서 생성된 Passkey는 퇴사 시 기기 반납과 함께 자연스럽게 접근이 차단된다. 하지만 직원이 회사 계정을 개인 기기에 등록한 Passkey는 퇴사 후에도 살아있을 수 있다. 이를 막으려면 Relying Party 서버에서 credentialId 단위로 자격증명을 무효화할 수 있어야 한다. FIDO2 스펙은 등록된 자격증명 목록에서 해당 credentialId를 제거하면 그 자격증명으로는 인증이 불가능해질 수 있게 해당 역할을 서버 측에 맡긴다. 앞선 포스팅에서 다룬 IGA의 Leaver 처리와 연동해야 하는 지점이다.
FIDO2가 막는 것과 막지 못하는 것
FIDO2/Passkey의 보호 범위를 정확히 이해해야 실무에서 올바른 기대를 가질 수 있다.
막는 것:
피싱 사이트를 통한 자격증명 탈취는 origin binding으로 원천 차단된다. 공격자 도메인에서는 서명 생성 자체가 거부된다. 서버 DB가 침해되더라도 공개키밖에 없으므로, 공개키만으로는 인증을 위조할 수 없다. 비밀번호와 달리 사이트별로 독립된 키 쌍이 생성되므로, 한 사이트가 침해돼도 다른 사이트로 피해가 전파되지 않는다.
막지 못하는 것:
기기 자체가 악성코드에 감염된 경우다. 로컬에서 인증 흐름을 가로채는 악성코드라면, 생체인증을 통과한 후의 서명값을 탈취할 수 있다. FIDO2는 피싱 저항을 보장하지, 엔드포인트 보안을 보장하지는 않는다. 이 시나리오에서 방어선은 앞 포스팅에서 다룬 Device Posture와 Remote Attestation이다.
계정 복구 경로는 FIDO2가 닿지 않는 영역이다. 이메일이나 SMS를 통한 계정 복구를 그대로 두면 그쪽이 피싱 타깃이 된다. 1차 인증을 아무리 강화해도 복구 경로가 약하면 우회 경로가 된다. 복구 경로도 피싱 저항 방식으로 전환해야 FIDO2 도입이 의미 있다.
인증 이후의 세션 하이재킹도 범위 밖이다. 인증에 성공한 뒤 발급된 세션 토큰이 탈취되면 재인증 없이 접근이 가능하다.
이것은 인증(Authentication)이 아니라 지속 검증(Continuous Verification)의 영역으로, 이후 포스팅에서 다룬다.
도입 전 체크포인트
어디서부터 시작할 것인가
전사 전환보다 고위험 계정을 먼저 타깃으로 한다. 인프라 관리자, 특권 접근 계정, C-level 임원은 피싱 타깃이 될 확률이 가장 높고 침해 시 피해도 가장 크다. JIT Access와 조합하면 더 강력하다. 특권 접근을 요청할 때 FIDO2 인증을 요구하고, 세션이 끝나면 권한이 자동으로 회수되는 구조다.
계정 복구 경로를 함께 강화한다
가장 많이 간과하는 부분이다. 주 인증을 강화해도 이메일이나 SMS 복구가 살아있으면 그쪽이 피싱 타깃이 된다. 복구 옵션도 피싱 저항 방식으로 전환하거나, 관리자 수동 승인 등 엄격한 신원 확인 프로세스로 대체한다.
자격증명 등록/해제 관리 프로세스
credentialId 단위의 자격증명 등록과 해제 프로세스를 IGA의 Leaver 처리와 연동한다. 퇴사 처리 시 IdP에서 계정을 비활성화하는 것만으로는 부족하다. 이미 등록된 FIDO2 자격증명은 서버 측 credentialId 무효화를 통해 별도로 회수해야 한다.
Attestation 정책 결정
기업 관리 기기의 Secure Enclave나 승인된 YubiKey 모델에서 생성된 자격증명만 허용할 것인지 결정한다. 엄격한 Attestation 검증은 보안을 높이지만, 운영 복잡도도 올라간다. FIDO Alliance MDS에서 인증기 메타데이터를 주기적으로 업데이트하는 운영 프로세스가 필요하다.
참고 자료
- W3C — Web Authentication: An API for accessing Public Key Credentials Level 3 (WebAuthn 스펙)
https://www.w3.org/TR/webauthn-3/ - FIDO Alliance — Client to Authenticator Protocol (CTAP) 2.1
https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html - FIDO Alliance — FIDO Metadata Service (MDS)
https://fidoalliance.org/metadata/ - FIDO Alliance — FIDO2: Moving the World Beyond Passwords
https://fidoalliance.org/fido2/ - Hunt, Troy — *"LinkedIn: The Largest Data Breach In History?"*, Have I Been Pwned, 2016
https://www.troyhunt.com/observations-and-thoughts-on-the-linkedin-data-breach/ - Krebs, Brian — *"Adobe Breach Impacted At Least 38 Million Users"*, Krebs on Security, 2013
https://krebsonsecurity.com/2013/10/adobe-breach-impacted-at-least-38-million-users/