- 기술
Wallet이 디지털 증명서를 받고 제시하는 메시지 규칙은 OpenID4VCI와 OpenID4VP가 정한다. 하지만 웹사이트가 사용자의 Wallet을 실제로 여는 방법은 따로 필요하다. 이번 글은 QR 코드와 앱 링크로 그 연결을 만드는 방식을 다룬다.
노트북 브라우저로 JM렌터카에서 차를 예약한다고 해 보자. 마지막 단계에 이르자 화면에 QR 코드와 “휴대폰 카메라로 스캔하세요”라는 문구가 뜬다. 휴대폰 카메라를 QR에 비추자 카메라 화면 아래에 열기 버튼이 나타나고, 버튼을 누르니 Wallet(디지털 지갑 앱)이 열리며 “JM렌터카가 이름, 면허 유효 여부, 성인 여부를 요청합니다”라는 화면이 나온다. 확인을 누르고 노트북을 보니, 예약 화면이 어느새 ‘확인 완료’로 바뀌어 있다.
노트북과 휴대폰은 케이블로 이어진 적도, 블루투스로 짝을 맺은 적도 없다. 두 기기는 어떻게 이어졌을까? 그리고 같은 예약을 휴대폰에서 했다면 무엇이 달랐을까?
3편에서 본 OpenID for Verifiable Presentations(OpenID4VP)는 JM렌터카 같은 Verifier(검증자)가 Wallet에 보내는 요청과 돌려받는 응답을 정한 규격이다. 그런데 도입 장면에서 JM렌터카 사이트는 노트북에 있고 Wallet은 휴대폰에 있다. 이렇게 웹사이트와 Wallet이 서로 다른 기기에 있는 경우를 기기 간 흐름(cross-device flow)이라고 한다.
기기 간 흐름에서 두 기기를 가장 흔히 잇는 수단은 QR 코드다. 도입 장면을 네 단계로 나눠 보면 다음과 같다.
세 번째 단계의 방향은 3편에서 본 그대로다. Wallet은 휴대폰 앱이어서 JM렌터카가 응답을 받으러 찾아갈 서버가 없다. 그래서 반대로 Wallet이 JM렌터카 서버로 응답을 보낸다. OpenID4VP는 이 단계까지를 정한다.
네 번째 단계는 규격이 정하지 않는다. 휴대폰에서 보낸 응답이 서버에 도착해도 노트북 화면은 저절로 바뀌지 않는다. 노트북 페이지가 서버에 결과가 왔는지 거듭 물어볼 수도 있고, 서버가 알려 주게 할 수도 있다. 어떤 방법을 쓸지는 JM렌터카가 정하고 직접 만든다.
이번에는 같은 예약을 휴대폰 브라우저에서 한다고 해 보자. 웹사이트와 Wallet이 한 기기에 있는 이 경우를 같은 기기 흐름(same-device flow)이라고 한다.
같은 기기 흐름에서는 QR 대신 링크를 쓴다. 예약 마지막 단계의 버튼을 누르면 그 링크로 Wallet이 열린다. 3편에서 경찰청 안내 페이지의 ‘Wallet에 추가’를 누르자 Wallet이 열린 것도 같은 방식이다. 그때는 발급 규격(OpenID4VCI)이 정한 링크로 Wallet이 열렸다.
사용자가 Wallet에서 확인을 누르면, Wallet은 응답을 보낸 뒤 사용자를 다시 JM렌터카 화면으로 돌려보내야 한다. OpenID4VP가 정해 둔 방법 하나는 이렇다. Wallet이 응답을 JM렌터카 서버로 보내면, 서버는 돌아갈 주소를 Wallet에 알려 준다. 그 주소에는 이번에만 쓰는 값이 붙어 있다. Wallet이 그 주소로 브라우저를 열면, JM렌터카는 이 값이 원래 예약 화면의 세션과 짝이 맞을 때만 결과를 넘긴다. 다른 기기에서 누군가 이 결과를 가져가지 못하게 하려는 장치다.
이 장치에는 한계가 있다. 결과를 받으려면 예약을 시작한 바로 그 브라우저로 돌아와야 한다. 그런데 Wallet이 다른 브라우저를 열어 버리면 원래 세션이 없어 결과를 이어 받을 수 없다. 휴대폰에 브라우저가 여럿이면 흔히 생기는 일로, 메신저 안의 브라우저에서 예약을 시작한 경우가 그렇다. 노트북과 휴대폰이 나뉜 기기 간 흐름에서는 이 장치를 아예 쓸 수 없다고 규격도 밝힌다. 원래 화면으로 무사히 돌아오게 하는 일은 결국 사이트의 몫으로 남는다.
지금까지는 휴대폰에 Wallet이 하나뿐인 것처럼 이야기했다. 하지만 사용자의 휴대폰에는 Wallet A와 Wallet B가 함께 깔려 있을 수 있다. 경찰청이 발급한 디지털 운전면허는 그중 Wallet B에만 들어 있다고 하자. 이때 JM렌터카의 링크는 어느 Wallet을 열까?
OpenID4VP는 Wallet을 여는 방법을 몇 가지 적어 두었다. 대표적인 것이 커스텀 스킴과 앱 링크다.
커스텀 스킴(custom URL scheme)은 앱이 OS에 등록해 두는 openid4vp:// 같은 주소다. 도입 장면에서 카메라로 찍은 QR 속 주소도 이런 커스텀 스킴 주소일 수 있다.
문제는 여러 Wallet이 같은 스킴을 등록할 수 있다는 데서 생긴다. iOS에서는 여러 앱이 같은 스킴을 등록하면 어느 앱이 열릴지 정해져 있지 않다고 Apple 공식 문서가 밝힌다. Android에서는 사용자가 기본 앱을 정해 두지 않았다면 어느 앱으로 열지 묻는 창이 뜬다. 어느 쪽이든 JM렌터카는 어떤 Wallet이 열렸는지 알 수 없다. 디지털 운전면허가 없는 Wallet A가 열리면 사용자는 그 자리에서 막힌다.
앱 링크(app link)는 이런 혼선이 적다. iOS의 Universal Link와 Android의 App Link는 특정 도메인의 주소를 그 도메인이 인정한 앱에 연결한다. 도메인을 가진 쪽이 자기 서버에 올려 둔 파일로 앱과의 연결을 증명하기 때문에, 엉뚱한 앱이 끼어들 수 없다. 대신 이 링크로는 그 도메인이 인정한 앱만 열린다. 앱이 깔려 있지 않으면 그 주소의 웹 페이지가 열린다. Wallet A 회사의 링크로는 Wallet A만 열린다. 사용자가 어떤 Wallet을 쓰는지 모르는 JM렌터카로서는 어느 회사의 링크를 걸어야 할지 알 수 없다.
Wallet이 제대로 열려도 문제는 남는다. 사용자가 Wallet에서 취소를 누르면 Wallet은 거절했다는 응답을 보낼 수 있다. 하지만 앱을 그냥 닫아 버리면 아무것도 오지 않는다. JM렌터카는 기다리다가 시간이 지나면 실패로 판단할 수밖에 없다. 링크로 앱을 직접 부르는 방식에서는 이런 경우를 사이트가 하나하나 챙겨야 한다.
기기 간 흐름에는 더 근본적인 문제도 있다. QR을 찍을 때 노트북과 휴대폰 사이에는 서로를 확인하는 장치가 없다.
이런 수법을 생각해 볼 수 있다. 공격자가 진짜 JM렌터카 사이트에서 자기 예약을 열고, 그 화면의 QR을 가짜 사이트에 옮겨 띄운다. 그리고 다른 사람에게 “본인 확인이 필요하다”며 그 QR을 찍게 만든다. 피해자의 Wallet에는 진짜 JM렌터카의 요청이 뜨므로 이상한 점을 알아채기 어렵다. 피해자가 확인을 누르면 자기 디지털 운전면허가 공격자의 예약에 쓰인다. 요청이 진짜 JM렌터카에서 왔는지, 응답이 JM렌터카로 가는지를 Wallet이 확인해도 이 수법은 걸러지지 않는다. 요청을 보낸 쪽도 응답을 받는 쪽도 모두 진짜이기 때문이다.
IETF가 기기 간 흐름의 보안에 관해 낸 권고(RFC 10027)도 이 점을 짚는다. 두 기기를 잇는 통로 자체는 서로를 확인하지 않으므로, 따로 장치를 더하지 않으면 사용자의 판단에 기댈 수밖에 없다는 것이다. 그래서 이 권고는 QR의 유효 시간을 짧게 하거나 한 번만 쓰게 하는 등의 대책을 정리하고, 가능하면 두 기기가 실제로 가까이 있는지 확인하라고 권한다. 이 권고는 적용 대상 프로토콜의 하나로 OpenID4VP를 직접 꼽는다.
같은 문제를 먼저 푼 기술이 있다. Passkey다. 노트북 브라우저에서 휴대폰에 저장된 Passkey로 로그인할 때도 QR을 찍는다. 다만 이 QR은 사이트가 아니라 브라우저가 띄운다. 휴대폰은 QR을 읽은 뒤 블루투스 신호를 보내 노트북 곁에 있음을 보인다. 실제 데이터는 인터넷 통로로 오가고, 이 통로는 그 신호로 나눈 키로 암호화한다. 이 근접 확인(proximity check)과 연결은 사이트가 아니라 브라우저와 OS가 맡는다.
비유는 여기까지다. Passkey는 로그인에 쓰는 인증 수단이고 Wallet은 디지털 증명서를 보관하는 앱이라, 둘의 보안 모델과 프로토콜은 다르다. 이 비유로 말하려는 점은 하나다. 두 기기를 잇는 일을 사이트마다 따로 만들지 않고 브라우저와 OS가 맡으면, 가까이 있는지 확인하는 장치까지 함께 얻는다는 점이다.
그렇다고 QR과 링크가 낡은 방식인 것은 아니다. 브라우저가 Wallet 호출을 도와주지 않는 환경에서는 지금 그대로 쓸 수 있는 방법이다. 5편에서 볼 브라우저의 방식이 널리 쓰여도, 이를 지원하지 않는 환경을 위해 QR과 링크는 대체 경로로 남는다.
다만 사이트, Wallet, 플랫폼의 조합에 따라 연결 방식과 사용자 경험이 달라진다. JM렌터카라면 노트북 화면에 띄울 QR과 휴대폰에서 쓸 링크를 준비하고, iOS와 Android에서 지원할 Wallet마다 어떻게 열리고 돌아오는지 확인해야 한다. 어떤 Wallet을 지원할지는 JM렌터카가 정할 일이고, 지원할 Wallet이 늘수록 확인하고 맞춰야 할 연동 작업도 늘어난다.
지금까지 본 문제를 모아 보자. 사이트는 여러 Wallet 중 맞는 것을 골라 열 수도, 어느 것이 열렸는지 알 수도 없다. 두 기기가 정말 가까이 있는지 확인할 장치가 없다. Wallet으로 넘어간 사용자가 원래 화면으로 돌아오는 길도, 사용자가 앱을 그냥 닫아 버린 경우도 사이트가 직접 챙겨야 한다. OpenID4VP는 Wallet을 여는 방법과 돌아오는 방법 하나를 적어 두었지만, 이 문제들을 다 풀지는 못한다.
이 문제들은 사이트가 앱을 직접 부르기 때문에 생긴다. 규격도 부록에서 브라우저와 OS의 기능으로 Wallet을 부르면 이런 문제가 상당 부분 줄어든다고 적는다. 그렇다면 사이트가 특정 앱을 부르는 대신, 브라우저에 “디지털 증명서를 보여 달라”고 부탁할 수는 없을까? 로그인에 Passkey를 쓸 때 사이트가 브라우저에 부탁하며 쓰는 navigator.credentials.get()이 그 실마리다.
Wallet과 주고받을 메시지를 표준으로 정하는 일과, 웹사이트와 Wallet을 잇는 경로를 정하는 일은 별개의 문제다.
이 연결을 웹사이트마다 따로 만드는 대신 브라우저와 OS가 맡아 준다면, 무엇이 달라질까?