배운 것

WebAuthn, Passkey로 로그인하기

세발낙지 2024. 1. 4. 20:45

이 글은 혼자서 RFC 문서를 읽어가며 passkey 로그인을 구현해본 경험을 글로 옮긴 것이다.

FIDO

  • Fast IDentity Online alliance의 약어로 아이디 패스워드에 의존적인 기존 로그인 방식을 대체하기 위한 인증 표준을 만드는 것을 목적으로 하는 협회이다.
  • WebAuthn은 FIDO에서 발표한 FIDO2의 일부이며, FIDO는 그 외에도 Universal 2nd Factor(U2F), Universal Authentication Framework, 그리고 FIDO2(WebAuthn, CTAP 등)를 발표했다.

WebAuthn

  • 비밀번호 없이 로그인을 하기 위한 웹 표준이다.
    • 즉, 지식 기반 (비밀번호에 대한 지식) 인증 시스템이 아니라 소유 기반 (스마트폰, 인증 디바이스 등)
  • 공개키 암호 방식을 기반으로 하는 사용자 인증 시스템이다.
  • 인증 과정에서 Relying Party ID (URL 기반)을 기반으로 private key에 대한 scope가 있어 피싱 사이트에서는 해당 private key를 인식하거나, 사용할 수 없다. 아래는 현재 호스트가 아닌 웹페이지로 API를 호출했을 때 발생하는 오류이다.
  • USB, Bluetooth Low Energy, NFC 등의 방식으로 사용자 인증 디바이스와 통신하여 인증이 이루어지기 때문에 사용자가 이를 현재 소유하고 있음을 보장할 수 있다.
  • Registration과 Authentication 두 단계로 구성된 전체 로그인 플로우 내에서 사용자의 민감한 정보는 교환되지 않는다.

Flow

Registration Flow, 출처: https://www.wallarm.com/what/webauthn-web-authentication
Authentication Flow, 출처: https://www.wallarm.com/what/webauthn-web-authentication

 

위 그림은 WebAuthn을 구성하는 Registration과 Authentication 두 과정의 flow를 나타내고 있다. 내가 개발자로 나의 웹페이지를 만들었다고 했을 때, User는 내 사이트에 로그인을 하려고 하는 사용자, User Agent는 웹 브라우져, Relying Party는 나의 웹 페이지, Authenticator는 사용자의 스마트폰이나 Yubi key 등의 인증 단말이다.

 

전체 flow를 보면, 담겨있는 payload만 살짝 상이할 뿐, 거의 같은 흐름으로 진행되는 것을 알 수 있는데, Registration에서는 Authenticator에서 새로운 Credential이 생성되어 User Agent를 거쳐 Relying Party로 전달되고, Authentication 단계에서는 Authenticator 내부에 저장된 Private key로 서명된 signature가 같은 과정으로 Relying Party로 저장되는 것이 다른 점이다.

이 중에서 내가 공부한 부분은 User Agent가 제공하는 WebAuthn API에 대한 것이며, User Agent와 Authenticator가 통신하는 과정에 대한 것은 FIDO2에 함께 포함된 Client To Authenticator Protocol (CTAP)에 기술되어 있다.

Registration

Registration의 publicKey의 예시

Web Browser에서 navigator.credentials.create({ publicKey })의 형태로 API를 호출한다. publicKey는 relying party(나의 웹페이지)에 대한 정보, 유저에 대한 정보, 해당 요청에 대응하는 일련의 바이트로 구성된 challenge, 사용 가능한 알고리즘 등의 정보를 함께 담아 API를 호출하면 된다.

Authentication

Authentication의 publicKey 예시

 

Web Browser에서 navigator.credentials.get({ publicKey })의 형태로 API를 호출한다. publicKey는 해당 요청에 대응하는 일련의 바이트로 구성된 challenge, 나의 웹페이지에 대한 정보(host URI)인 rpID, 사용한 인증기에서 사용자에 대한 인가를 요청할 지에 대한 여부를 담은 userVerification, 서버가 해당 유저에 대해 가지고 있는 credentials를 담은 allowCredentials 등을 담아 API를 호출하면 된다. 단, allowCredentials는 꼭 인자로 담지 않아도 인증이 가능하며 오히려 이것을 담으면 사용자에 대한 정보가 노출되는 것이라는 권고가 RFC 문서에 있었다. 

 

Arguments

Raw ID

  • Registration과 Authentication 두 단계 모두에서 반환된다.
  • Registration의 경우, 새로 생성된 credential의 식별자인 ID이다.
  • Authentication의 경우, 서명에 사용된 private key에 대응하는 credential의 식별자인 ID이다.

Client Data JSON

  • Registration와 Authentication 두 단계 모두에서 반환된다.
  • Challenge, Type, origin, crossOrigin의 필드를 가지고 있다.
  • JSON 형태가 Base64URL 인코딩 된 형태로 반환된다.
  • Authenticator로 정보가 넘어가지는 않으나, 해시된 값이 전달되어 private key로 서명을 진행할 때 이용된다.

Challenge

  • replay 공격을 막기위한 장치로 relying party가 임의로 생성한 32바이트 데이터이다.
  • User Agent의 응답에 담긴 challenge가 서버에서 생성한 것과 일치하는 지 서버는 이를 검증해야 한다.

Attetation Object

Attestation Object

  • Registration과 Authentication 두 과정 모두에서 반환되지만, 형태가 조금 다를 수 있다.
  • CBOR 인코딩 되어 있다.
    • CBOR 인코딩은 human readable하지는 않지만, 더 적은 CPU와 메모리 자원을 활용하여 인코딩, 디코딩이 가능한 JSON과 상호 변환이 가능한 인코딩 방식이다
  • Format, Attestation Statement, Authenticator Data로 구성되어 있다.
  • Format은 Attestation Object의 format에 대한 정보를 담고 있으며, 각각의 형태에 따라 다른 방식으로 Authenticator를 검증한다.
  • Attestation Statement는 Authenticator가 생성한 데이터로 이를 활용하여 Relying Party는 해당 Authenticator가 신뢰할 수 있는 지 검증할 수 있다.
  • Authenticator Data에는 RP ID의 해시값, 여러 flag, Authenticate 단계가 진행될 때마다 증가하는 counter, Registration인 경우에 포함되는 생성된 Credential Data, 그리고 추가적인 Extension의 결과값이 포함된다.
    • Flag의 ED는 Extention Data가 존재하는 지에 대한 여부이다.
    • Flag의 UV는 User Verified 여부이다. 예를 들어 스마트폰을 활용했다고 했을 때, 유저가 지문이나 안면 인식을 추가로 진행한 경우 1로 표기된다.
    • Flag의 AT는 Attestation Credential Data가 존재하는 지에 대한 여부이다. Registration의 경우 1으로, Authentication이 0으로 표기된다.
    • Flag의 UP는 User Presense에 대한 여부이다. 이는 verification과는 다르게 단순 상호작용만으로 검증된다.
  •  Attested Credential Data (새로 생성된 Credential Data, 즉 public key)는 COSE 형식으로 되어 있다, 즉 CBOR 인코딩되어 있다.

디코딩한 Attested Credential Data. 타원곡선 암호 알고리즘 key로 x좌표와 y좌표, 그리고 곡선에 대한 정보가 담겨있다.

Signautre

  • Authentication의 경우에만 반환되는 데이터이다.
  • UserAgent에서 Authenticator로 전달한 Client Data JSON의 해시값을 Authenticator Data에 연결하여, 다시 sha256으로 해싱한 값을 authenticator가 가지고 있는 private key로 서명한 형태이다.
  • Relying Party (내 웹페이지)는 이것을 Registartion 단계에서 생성된 Credential Data 안에 포함된 public key를 활용하여 복호화할 수 있다.
  • 마찬가지로 Authenticator Data에 Client Data JSON의 해시값을 연결하여 sha256으로 재해시한 값을 복호화한 값과 비교하여 해당 서명이 동일한 authenticator로부터 생성되었음을 확인할 수 있다.

Limitations

사용자의 입장에서는 외부 인증 기기가 꼭 필요하다는 것과 외부 인증 기기를 잃어버릴 경우, 복구가 힘들다는 단점이 있다. 만약 비밀번호나 이메일을 활용해 복구를 진행하는 경우, 다시 지식 기반의 인증으로 회귀하게 되어버리니, 모순적이다. 물론 Google, Apple, Microsoft 등의 계정에 생성된 private key를 공유하는 방식으로 다른 기기에서 같은 passkey를 사용할 수는 있으며, Apple의 경우 기기에만 passkey를 저장할 수 없고 icloud keychain에만 저장할 수 있는 등 편의성이 제공되고 있지만 최종적으로 passkey를 저장하는 계정에 종속된다는 단점이 있다.

 

개발자의 입장에서는 우선 개발 편의성이 아주 떨어진다는 단점이 있다. Signature를 검증하는 과정이나 CBOR 디코딩, COSE 형식의 key를 해석, Base64URL 인코딩 등등 내장 함수만으로 처리하기에 큰 불편함이 있었고, 전체적으로 알아야 하는 것들이 너무 많았다. 또한, 기존에 서비스되고 있는 사이트에서 이것으로 패스워드를 제거하기에는 어려움이 있고, 유저의 입장에서와 마찬가지로 유저가 기기를 잃어버렸을 경우를 대비할 수 없는 아이러니에 빠지게 된다.

 

+ Apple 기기의 경우 IOS 16부터 passkey를 icloud keychain에만 저장할 수 있어 authenticator를 인증하는 것은 의미가 없다고 판단하는 것인지 registration 단계에서 attestation option을 direct로 주었을 때에도 none format으로 반환한다. 이는 맥북에서 사파리에 웹페이지에 접속하여 아이폰으로 passkey를 생성하여 확인해보았다. chrome으로 접근한 경우, attestation object의 format이 'packed' 였지만, safari의 경우에는 'none'이었고 'none' format에서는 attestation statement는 빈 object이다.

 

References

'배운 것' 카테고리의 다른 글

가운데를 말해요  (0) 2023.03.29
기술 부채란 무엇일까?  (0) 2022.11.29
DAY01 - 학습정리  (0) 2022.07.18
미확인 도착지  (0) 2022.06.08
자바스크립트의 this란?  (0) 2022.06.06