- 기술
지금까지 웹사이트는 QR 코드나 앱 링크로 Wallet을 직접 불러야 했다. 그래서 Wallet 선택, 기기 간 근접 확인, 복귀 처리를 사이트마다 따로 풀어야 했다. 이번 글은 브라우저와 OS가 이 연결을 중재하는 Digital Credentials API를 다룬다.
노트북 브라우저로 JM렌터카에서 차를 예약한다고 해 보자. 마지막 단계에서 ‘운전면허 확인’ 버튼을 누르자, 사이트가 그린 화면이 아니라 브라우저 자체의 작은 창이 뜬다. 창에는 JM렌터카가 요청하는 항목과, 휴대폰으로 QR 코드를 찍으라는 안내가 있다. 휴대폰으로 찍으면 Wallet(디지털 지갑 앱)이 열리고 “JM렌터카가 이름, 면허 유효 여부, 성인 여부를 요청해요. 보여줄까요?”라는 화면이 나온다. 확인을 누르면 노트북의 예약이 끝난다. 사이트는 어느 Wallet을 쓰는지 묻지도, QR 코드를 직접 만들지도 않았다.
1편 첫머리의 장면을 조금 더 자세히 들여다본 것이다. 브라우저는 어떻게 이 창을 띄웠을까? 사이트는 브라우저에 무엇을 넘겼고, 무엇은 여전히 사이트가 해야 할까?
4편에서 본 방식에서는 사이트가 QR 코드나 앱 링크(iOS의 Universal Link, Android의 App Link)로 Wallet을 직접 불렀다. 그래서 문제가 사이트에 남았다. 여러 Wallet 중 어느 것이 열릴지 알 수 없었다. 노트북과 휴대폰이 정말 가까이 있는지 확인할 장치가 없어서, 가짜 사이트가 진짜 요청의 QR을 띄워도 사용자가 스스로 가려내야 했다. 사용자가 원래 화면으로 돌아오는 길도 사이트가 챙겨야 했다.
Digital Credentials API는 W3C가 표준화하고 있는 브라우저 API로, 사이트가 Wallet을 직접 부르는 대신 브라우저에 디지털 증명서를 요청하게 한다. 여기서 요청하는 것은 제시(presentation), 곧 사용자가 Wallet에 든 증명서에서 필요한 항목을 사이트에 보여 주는 일이다. 구조가 이렇게 바뀐다.
사이트는 이제 특정 앱을 부르지 않고, 브라우저에 요청한 뒤 결과를 기다린다.
사이트는 navigator.credentials.get()으로 브라우저에 요청한다. 브라우저에 저장된 로그인 수단을 꺼내 달라고 할 때 쓰는 함수로, Passkey로 로그인할 때도 이 함수를 부른다. 디지털 증명서를 요청할 때는 여기에 digital이라는 항목을 담는다.
아래는 ‘운전면허 확인’ 버튼을 눌렀을 때 실행되는 코드다. 이 함수는 버튼 클릭 같은 사용자 동작 직후에만, 동작 한 번에 한 번만 부를 수 있다. 그래서 서버에서 요청을 받아 오느라 시간을 끌지 않도록, 요청 내용은 페이지를 열 때 미리 받아 둔다.
요청 하나는 두 가지로 이루어진다. protocol은 어떤 제시 규칙을 따르는 요청인지를 가리키는 이름이고, data는 그 규칙에 맞춰 서버가 만든 요청 내용이다. data에는 이번 요청에만 쓰는 임의의 값이 들어 있고, 서버는 이 값을 기억해 두었다가 응답을 검증할 때 맞춰 본다. 값은 한 번으로 끝나므로, 다시 시도할 때는 서버에서 새 요청을 받아야 한다.
브라우저가 돌려주는 결과도 어떤 규칙으로 답했는지와 그 답의 내용을 담은 객체다. 요청을 두 개 넣었다면 어느 쪽이 쓰일지는 브라우저와 OS가 고르고, 서버는 결과에 담긴 규칙 이름을 보고 처리를 나눈다. 사이트는 이 결과를 읽지 않고 그대로 서버로 보낸다. 코드 앞부분의 걸러 내기와 대체 경로는 뒤에서 다시 본다.
protocol에 아무 이름이나 쓸 수 있는 것은 아니다. 브라우저가 Wallet에 넘길 수 있는 프로토콜은 명세가 직접 정한다.
제시에 쓰는 것은 두 갈래다. 하나는 3편에서 본 OpenID for Verifiable Presentations(OpenID4VP)다. 서명 없는 요청, 서명된 요청, 여러 서명을 붙인 요청마다 openid4vp-v1-signed처럼 버전을 붙인 이름이 따로 있다. 다른 하나는 ISO/IEC 18013-7의 부록 C다(org-iso-mdoc). mdoc(ISO/IEC 18013-5가 정한 디지털 운전면허의 형식)을 온라인에서 제시하는 방법으로, 3편에서 예고한 mdoc 전용 제시 방식이 이것이다. 발급(issuance) 쪽으로는 3편에서 본 OpenID for Verifiable Credential Issuance(OpenID4VCI), 곧 Issuer가 Wallet에 증명서를 발급하는 절차가 명세의 목록에 올라 있다.
처음에는 누구나 프로토콜을 추가할 수 있는 개방형 목록을 두는 방안도 논의됐다. 하지만 W3C 작업반은 이 목록을 두지 않고, 명세가 직접 몇 가지를 정하기로 했다. 명세가 정하지 않은 프로토콜의 요청은 브라우저가 조용히 빼고, 남는 요청이 없으면 오류를 낸다. 앞의 코드가 요청을 먼저 걸러 내는 것도 대체 경로로 갈지 미리 정하려는 것이다. 다만 명세가 정하는 것은 어떤 프로토콜을 넘길지까지다. data 안의 요청 규칙과 증명서의 형식은 각 프로토콜 규격과 형식 규격에 맡긴다. 3편에서 본 ‘형식과 프로토콜은 별개’라는 원칙이 여기서도 그대로 통한다.
하지만 실제 지원 현황은 명세와 다르다. 명세는 브라우저가 이 제시 프로토콜을 모두 지원하라고 요구한다. 실제로는 Chrome이 OpenID4VP와 부록 C를 함께 넘기는 반면, Safari는 부록 C만 넘긴다. Wallet 쪽도 다르다. Apple Wallet은 부록 C로, Google Wallet은 OpenID4VP로 요청을 받는다. 두 Wallet 모두 운전면허를 mdoc 형식으로 담지만, 주고받는 프로토콜은 다르다. 그래서 두 Wallet 사용자를 모두 받으려면, JM렌터카는 같은 운전면허 확인을 두 가지 요청으로 준비해야 한다. 앞의 코드가 요청을 두 개 만든 이유다. 어느 쪽까지 받을지는 JM렌터카가 정한다.
요청을 받은 브라우저와 OS는 사이트 대신 여러 일을 맡는다. 사용자에게 무엇이 요청되는지 보여 주는 창을 띄우고, 요청에 맞는 증명서를 가진 Wallet을 찾고, 사용자가 고르고 동의하게 한다.
이 선택 화면과 Wallet을 찾는 방식은 명세가 정하지 않는다. 그래서 플랫폼마다 다르다. Android에서는 Wallet이 자기가 가진 증명서 정보를 OS에 등록해 두고, OS가 요청에 맞는 증명서를 골라 보여 준다. iPhone에서는 Apple Wallet과, 신분증 제공 앱으로 등록된 Wallet이 대상이 된다. 사이트는 어느 Wallet이 선택될지 정하지 않고, 알 수도 없다. 이 경로에서는 4편의 ‘Wallet A가 열릴까, Wallet B가 열릴까’ 문제를 브라우저와 OS가 대신 푸는 셈이다. 다만 어느 Wallet이 JM렌터카의 요청을 받아 주는지는 여전히 JM렌터카가 챙길 일이다.
휴대폰 브라우저에서 요청하는 같은 기기 흐름(same-device flow)이라면, OS가 같은 휴대폰의 Wallet을 곧바로 띄운다. 노트북에서 요청하는 기기 간 흐름(cross-device flow)이라면 휴대폰과 이어야 한다. Chrome은 노트북 화면에 QR 코드를 띄운다. 사용자가 휴대폰으로 이 QR을 찍으면, 블루투스로 두 기기가 가까이 있는지 확인한 뒤 암호화된 통로로 둘을 잇는다. 이때 쓰는 것이 FIDO 규격의 CTAP(Client to Authenticator Protocol)다. 4편에서 비유로 든 Passkey의 기기 간 로그인도 같은 규격으로 두 기기를 잇는다. 명세도 다른 기기의 Wallet과 이을 때 이 규격을 쓰라고 권한다. 연결 방식이 같다는 뜻이지, Passkey와 Wallet의 보안 모델이 같다는 뜻은 아니다. 4편의 QR과 다른 점은 이 QR을 사이트가 아니라 브라우저가 띄우고, 근접 확인(proximity check)도 함께 이루어진다는 점이다.
Safari는 방식이 조금 다르다. Mac이나 iPad에서 요청하면, 같은 Apple 계정으로 로그인한 가까운 iPhone에서 이어 가게 한다.
모든 일이 끝나면 결과는 요청을 보낸 원래 탭으로 돌아온다. 이 경로에서는 4편에서 사이트가 따로 챙겨야 했던 복귀 문제가 사라진다.
명세가 내세우는 목표에는 프라이버시가 있다. 사용자가 명시적으로 동의하기 전에는 사이트가 사용자도, 사용자가 가진 증명서도 전혀 알아낼 수 없게 한다.
그래서 몇 가지 제약이 있다. 사용자가 버튼을 누르는 것 같은 동작이 없으면 사이트는 요청할 수 없다. 앞의 코드에 나온 DigitalCredential.userAgentAllowsProtocol()도 브라우저가 그 프로토콜을 받는지만 알려 준다. 사용자가 Wallet을 깔았는지, 운전면허를 가졌는지에 따라 답이 달라지면 안 된다고 명세가 못 박는다. 답이 달라지면 사이트가 그 차이로 사용자를 몰래 가려낼 수 있기 때문이다. 같은 이유로, 요청이 실패해도 사이트는 사용자가 취소했는지 증명서가 없는지 구분할 수 없다.
사용자가 동의한 뒤에도 사이트가 받는 것은 요청한 항목과 그 항목을 검증하는 데 필요한 데이터뿐이다. JM렌터카라면 이름, 면허 유효 여부, 성인 여부에 서명, 유효기간 같은 검증용 값을 더해 받을 뿐, 생년월일이나 면허번호는 받지 않는다.
브라우저는 요청을 넘길 때 요청한 사이트 주소(origin)를 Wallet에 직접 알려 주고, Wallet은 이 사이트 주소를 넣어 응답을 만든다. 4편에서 본 QR 피싱, 곧 공격자가 진짜 JM렌터카 요청의 QR을 가짜 사이트에 옮겨 띄워 사용자가 찍게 만드는 수법을 이 장치가 막는다. 가짜 사이트가 JM렌터카의 요청을 그대로 가져와 브라우저에 넘겨도, 브라우저가 Wallet에 알려 주는 사이트 주소는 가짜 사이트의 주소다. 응답에도 가짜 사이트의 주소가 들어가므로, 공격자가 그 응답을 JM렌터카에 들고 가도 검증을 통과하지 못한다. 다만 가짜 사이트가 자기 이름으로 새 요청을 만들어 사용자의 정보를 받아 가는 것까지 막지는 못한다. 그때는 Wallet 사업자의 등록 절차와, 동의 화면에 뜨는 요청자 정보로 막아야 한다.
서명된 요청이라면 Wallet이 한 걸음 먼저 막는다. OpenID4VP의 서명된 요청에는 요청을 보내도 되는 사이트 주소가 적혀 있고, Wallet은 브라우저가 알려 준 주소가 그 안에 없으면 요청을 거부한다. 서명 없는 요청도 규격에는 있다. 이때는 브라우저가 알려 주는 사이트 주소가 곧 JM렌터카의 신원이 된다. 하지만 실제로는 서명된 요청을 요구하는 Wallet이 많다. Google Wallet은 기기 간 흐름에서 서명된 요청을 요구하고, Apple Wallet은 요청에 서명된 사이트 정보를 넣게 한다. 둘 다 사이트가 미리 등록 절차를 거쳐 발급받은 X.509 인증서로 서명한다. 코드에서 서명된 요청을 쓴 이유이고, 도입을 준비할 때 가장 먼저 챙겨야 할 일이기도 하다.
브라우저가 많은 일을 맡지만, 응답을 믿을 만한지 판단하는 일은 하지 않는다.
브라우저는 Wallet의 응답이 올바른 형태의 데이터인지만 보고 사이트에 넘긴다. 그 안의 디지털 운전면허가 진짜인지는 보지 않는다. 앞의 코드가 결과를 곧바로 서버로 보낸 이유다. 응답은 대개 JM렌터카가 요청에 넣어 둔 키로 암호화되어 오므로, 서버는 먼저 자기 비밀 키로 응답을 풀어야 한다. 그다음 2편과 3편에서 본 검증(verification)을 모두 해야 한다.
브라우저는 요청과 전달을 맡을 뿐, 무엇을 믿을지는 여전히 Verifier(검증자)인 JM렌터카가 정하고 검증한다.
이 API를 쓸 수 있는 곳은 아직 한정적이다. Chrome은 141 버전부터 Android와 데스크톱에서 기본으로 켜져 있고, 데스크톱에서는 Android 휴대폰이나 iOS 26 이상의 iPhone과 이어진다. Safari는 26 버전부터 iOS, iPadOS, macOS에서 쓸 수 있고, iOS 26부터는 iPhone의 Chrome 같은 다른 브라우저에서도 쓸 수 있다. Firefox에서는 기본으로 쓸 수 없다. Mozilla는 이 API가 사생활 문제를 일으키고 웹 사용자를 부당하게 배제할 위험이 크다며 공식 입장을 ‘반대’로 밝혔다(Mozilla standards-positions #1003). 앱 안에 웹 화면을 띄우는 웹뷰에서도 아직 쓸 수 없다. Android의 WebView와 iOS의 WKWebView 모두 그렇다.
그래서 앞의 코드는 먼저 브라우저가 받는 프로토콜을 확인하고, 받을 수 있는 요청이 하나도 없거나 요청이 실패하면 4편의 QR과 링크로 넘어간다. 제시가 끝났는데 서버 검증이 실패했다면 QR로 처음부터 다시 하게 하지 않고 오류를 알린다. 4편에서 말했듯 QR과 링크는 사라지지 않고, 브라우저 API를 쓸 수 없는 곳을 위한 대체 경로로 함께 남는다. JM렌터카는 Chrome, Safari, 그 밖의 브라우저, 웹뷰마다 어떤 경로를 쓸지 정해야 한다.
명세는 제시만이 아니라 발급도 다룬다. 사이트가 navigator.credentials.create()로 브라우저에 발급을 부탁하면, 3편에서 본 경찰청의 발급 절차를 브라우저가 중재한다. Chrome이 이 기능을 준비하고 있다. 발급 쪽 이야기는 따로 다룰 주제로 남겨 둔다.
정리하면 JM렌터카가 준비할 것은 네 가지다. Wallet마다 받는 프로토콜에 맞춘 두 가지 요청, Wallet 사업자별 등록과 서명, 응답을 풀고 검증하는 서버, 그리고 브라우저 API를 쓸 수 없는 곳을 위한 대체 경로다.
브라우저는 요청과 전달을 중재하지만, 어떤 증명서를 신뢰할지는 서비스의 검증 정책에 달려 있다.
이 시리즈는 카카오 로그인 버튼 뒤에서 오가는 일에서 출발해, 브라우저가 띄운 작은 창 하나에 이르렀다. 그사이 표준은 Wallet과 주고받을 메시지를 정했고, 이제 그 메시지가 오갈 경로까지 정하고 있다.