WebAuthn과 Passkey

WebAuthn과 Passkey

여러분들은 패스워드 매니저를 사용하시나요? 저는 개인적으로 Apple의 암호 기능을 주로 사용합니다. 또한 icloud+도 사용하고 있어 이메일 가리기 + 암호 조합으로 가급적 서비스별로 계정과 패스워드가 겹치지 않는 상태로 유지하고 있습니다. 구글의 경우 Google password manager를 통해 비슷하게 사용하시는 분들도 있을거라고 생각합니다. Apple이나 Google의 이러한 기능들과 FIDO 관련 인증 방식들은 Passwordless의 대중화를 앞당겼고 이제는 Password를 입력하는 것이 점점 어색해지는 시기가 온 것 같습니다.

작년에 Apple은 macOS 벤투라 업데이트에서 WebAuthn을 기반으로 한 Passkey 기능을 공개했습니다. 구글이나 MS 또한 Passkey를 제공하면서 강하게 추친하는 상태라 본격적으로 Passwordless가 확대되지 않을까 싶습니다. 오늘은 WebAuthn과 Passkey에 대한 이야기를 풀어봅니다.

Passwordless

Password는 보안에서 정말 중요한 개념입니다. 인류 역사에서도 아주 오래전부터 등장했고 현대까지 꾸준히 등장하고 있고 현재의 인터넷 환경에서도 결국 Password는 큰 부분을 차지하고 있는 것이 현실입니다.

다만 Password는 일반적으로 사용자의 의지에 따른 고정값으로 유출되기 쉬운 반면, 유출 시 리스크는 굉장히 높습니다. 이를 보완하기 위해 Password 없이 인증하는 방식에 대한 기술이 꾸준히 연구 및 사용되고 있습니다. 대표적으로 OTP나 이메일 링크 기반 로그인 등이 있겠죠. 이렇게 Password 없이 인증 구현하려는 노력들과 결과를 Passwordless라고 부르면 될 것 같습니다.

WebAuthn

WebAuthn은 W3C(World Wide Web Consortium) 에서 2019년 발표한 웹 표준으로 공개키 암호화를 이용해서 웹 기반 어플리케이션 및 서비스에서 사용자를 인증할 수 있는 표준화된 인터페이스입니다. FIDO 얼라이언스의 FIDO2 프로젝트의 핵심이자 생각보다 많은 곳에서 사용중이며, 뒤에서 이야기할 Passkey의 사용이 늘어남에 따라 인증 체계의 핵심적인 요소로 자리잡고 있는 것 같습니다.

https://www.w3.org/TR/webauthn/

기술 자체는 기존 디지털 서명과 동일하며 목적과 부수적인 요소에서의 차이만 있습니다. 결국 키 쌍을 생성하고 개인키로 서명, 공개키로 검증하는 형태의 구조를 가지고 있습니다.

  • 키 쌍 생성 알고리즘
  • 이용자의 개인 키를 통한 서명 알고리즘
  • 이용자의 공개 키를 통한 서명 검증 알고리즘

Entities

  • User: 사용자
  • Browser(User-Agent): 브라우저 등 유저의 서비스와의 연결점이 있는 Application
  • Relying Party: 웹 서비스 또는 어플리케이션
    • rpID: Relying Party를 식별하기 위한 ID
  • Authenticator: 인증을 수행할 수 있는 수단
    • e.g
    • Security Key(USB, NFC, Etc)
    • Mobile App
    • 생체인증 등

Registration

WebAuthn을 이용하기 위해 사용자를 등록하는 과정입니다.

  1. 사용자가 Relying Party에 계정을 생성할 때, 웹 페이지는 사용자에게 고유한 사용자 이름을 요청
  2. 사용자 이름을 생성한 후, Relying Party는 사용자에게 고유한 challenge와 rpId(Relying Party ID)를 생성
  3. Relying Party는 사용자에게 등록을 위해 지원되는 Authenticator의 종류를 알려줌
  4. 사용자가 Authenticator를 선택하면, Authenticator는 사용자의 공개 키를 생성하고 사용자를 인증
  5. 인증 성공 시, 인증기는 공개 키와 인증기로부터 받은 서명 데이터를 Relying Party에 반환
  6. Relying Party는 이 정보를 저장하고 사용자를 나타내는 식별자와 연결하여 사용자의 등록을 완료

Authentication

이미 등록된 사용자가 WebAuthn을 통해 로그인을 수행하는 과정입니다.

  1. 사용자가 다시 로그인하거나 서비스에 액세스하려고 할 때, Relying Party는 사용자의 인증 요청을 생성
  2. Relying Party는 사용자에게 고유한 challenge와 rpId를 생성하고, 사용자에게 등록된 Authenticator 종류를 알려줌
  3. 사용자가 Authenticator를 선택하면, Authenticator는 Relying Party가 제공한 정보와 사용자의 개인 키를 사용하여 서명 데이터를 생성하고 Relying Party에 반환
  4. Relying Party는 공개 키를 사용하여 서명 데이터를 확인하고, challenge 및 rpId와 일치하는지 확인
  5. 모든 확인이 완료되면 Relying Party는 사용자를 로그인하고 서비스에 액세스

사실 상 Registration과 Authentication은 목적과 약간의 동작의 차이만 있을 뿐 핵심적인 부분은 거의 비슷합니다. 정리하자면 전자서명을 이용해서 Authenticator의 신원을 증명하여 등록하고 로그인 시 검증하여 본인임 증명하는 기술입니다.

Passkey

Passkey는 WebAuthn을 기반으로 만들어진 기술입니다. 아니 정확하게는 서비스라고 보는게 좋을 것 같네요. WebAuthn의 스펙을 사용하여 비밀번호 없는 로그인을 각 기기에서 지원하는 서비스입니다.

WebAuthn에서의 Authenticator가 아이폰, 안드로이드 등의 디바이스가 되며 안면인식, 지문인식 등을 통해 사용자의 신원을 증명하고 디바이스 내부에 저장된 개인키를 통해 서명하는 형태로 사용됩니다. 이를 통해 비밀번호 없는 로그인을 제공합니다.

WebAuthn과 차이

사실상 Passkey가 WebAuthn 스펙을 기준으로 구현되었기 때문에 기술적으로는 동일합니다. 오히려 마케팅 용어에 가까울 수도 있습니다. 다만 원본 기술이 같을뿐이지 일부 차이점은 가지고 있고 사실상 WebAuthn과 Passkey는 상호보완적인 관계입니다.

Target range

WebAuthn과 Passkey는 기술적으론 같지만 이를 통한 결과에선 살짝 차이가 있습니다. WebAuthn은 웹 상에서의 공개키를 통한 인증을 구현하기 위한 스펙이라 웹에 대한 인증이 핵심이고, Passkey는 좀 넓게 웹 이외에 대한 인증도 포함하게 됩니다. 이는 Passkey를 서비스하는 업체(Apple, Google 등)가 플랫폼이자 디바이스에 관여할 수 있는 업체이기 떄문입니다. 그래서 아래 느낌으로 바라볼 수 있을 것 같습니다.

  • WebAuthn: Web Authentication
  • Passkey: Digital Credential

Synchronize private key

다만 조금 더 세심하게 WebAuthn과의 차이를 본다면 Apple, Google이 가진 다른 플랫폼 기술을 이용하여 소유 디바이스간 개인키 공유 등을 통해 사용성을 조금 더 올렸다는 점입니다. 예를들어 Apple의 경우 키 쌍 생성 후 개인키를 키체인에 보관하여 본인 소유의 다른 디바이스에서도 사용할 수 있도록 제공하고, 유실에 대한 리스크도 줄인 형태로 제공합니다. 그래서 아이폰에서 Passkey 등록 시 맥북에서도 지문이나 Watch 인증으로 로그인할 수 있게 됩니다.

Apple과 Google이 개인키 공유를 위해 사용하는 플랫폼은 아래와 같습니다.

  • Apple: KeyChain
  • Google: Password Manager

여기서 재미있는 점은 Passkey를 동기화하는 서비스들이 나타나고 있다는 점입니다. 이를 통해 사용자 입장에선 사파리에서 저장한 패스워드를 구글 크롬에서 사용할 수 있는 시나리오가 가능하게 된 것이죠.

For Developer

앞으로 사용자의 경험에서 Passwordless가 자연스러워진다면 서비스 개발 시 Passkey 또는 WebAuthn에 대한 지원은 필수적이게 될 것 같습니다. Apple, Google 등에서도 이미 API로 쉽게 구현할 수 있도록 문서를 제공하고 있으며 MDN 문서를 참고하거나 각 언어의 라이브러리에서도 많이 지원되고 있어 이를 잘 활용한다면 어렵지 않게 구현할 수 있을거라고 생각합니다.

// Register a new account on a servicein page link

let challenge: Data // Obtain this from the server.
let userID: Data // Obtain this from the server.
let platformProvider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: "example.com")
let platformKeyRequest = platformProvider.createCredentialRegistrationRequest(challenge: challenge, name: "Anne Johnson", userID: userID)
let authController = ASAuthorizationController(authorizationRequests: [platformKeyRequest])
authController.delegate = self
authController.presentationContextProvider = self
authController.performRequests()

다만 구현 시 스펙을 잘 따르는 것은 정말 중요합니다. 일부 서비스에선 편의나 실수로 인해 스펙을 준수하지 않은 상태로 구현되는 경우가 종종 있는데, 이는 반대로 보안적인 리스크를 만들어낼 수 있는 부분이 됩니다. OAuth만 하더라도 제가 테스트하면서 발견했던 이슈는 대부분 스펙을 준수하지 않았거나 잘못 구현된 내용이 대다수였습니다. 아무래도 인증을 담당하는 중요한 기능인만큼 구현하게 되신다면 꼼꼼히 체크하는 것을 추천드립니다.

For Pentester

키 쌍을 통한 인증이라 기술 자체는 현재 기준으로 탄탄하다고 생각합니다. 다만 위에서 이야기했듯이 해당 기능을 구현하는 서비스의 상태에 따라서 충분히 인증 우회로 이어질 수 있기 때문에 잘 살펴보는 것은 정말 중요할 것 같네요 :D

References