- 기술
디지털 증명서에는 발급하는 Issuer, 보관하고 제시하는 Wallet, 확인하는 Verifier가 있다. 역할은 정해졌지만, 서로 모르는 회사들이 같은 Wallet과 증명서를 주고받으려면 공통된 절차가 필요하다. 이번 글은 그 절차인 OpenID4VCI와 OpenID4VP를 다룬다.
휴대폰으로 경찰청 안내 페이지에 들어가 본인 확인을 마치고 ‘Wallet에 추가’를 누른다고 해 보자. 미리 깔아 둔 Wallet(디지털 지갑 앱)이 열리고 “경찰청이 디지털 운전면허를 발급하려고 합니다”라는 화면이 뜬다. 확인을 누르면 Wallet 첫 화면에 운전면허 카드가 하나 생긴다.
그런데 이 Wallet은 경찰청이 만든 앱이 아니다. 몇 주 뒤 이 면허를 받아 볼 JM렌터카도 경찰청이나 Wallet을 만든 회사와 따로 약속한 적이 없다. 서로 모르는 세 곳이 어떻게 같은 말을 쓸 수 있을까?
2편에서 본 것처럼 디지털 운전면허가 오가는 때는 두 번이다. 도입 장면처럼 면허를 받는 발급(issuance) 시점과, 몇 주 뒤 JM렌터카 예약 마지막 단계에서 면허를 보여 주는 제시(presentation) 시점이다.
두 시점은 주고받는 상대가 다르다. 발급 때는 경찰청과 Wallet이, 제시 때는 JM렌터카와 Wallet이 주고받는다. 여기서 경찰청은 Issuer(발급자), JM렌터카는 Verifier(검증자)다.
게다가 Wallet은 하나가 아니다. 누군가는 Wallet A를, 누군가는 Wallet B를 쓴다. 경찰청과 JM렌터카가 Wallet마다 따로 연동할 수는 없다. 그래서 발급과 제시 모두 누구나 따르는 공통 규칙이 필요하다. 발급의 규칙이 OpenID for Verifiable Credential Issuance(OpenID4VCI)이고, 제시의 규칙이 OpenID for Verifiable Presentations(OpenID4VP)다.
OpenID4VCI는 Issuer가 Wallet에 디지털 증명서를 발급하는 절차를 정한 규격이다. 도입 장면을 이 절차에 맞춰 보면 세 단계로 나뉜다.
이 세 단계를 처리하려면 경찰청 쪽에 두 가지가 있어야 한다. Access Token을 내주는 역할과, 증명서를 내주는 발급 창구다. Access Token을 내주는 역할은 별도 서버에 맡길 수도 있다.
세 번째 단계에서 한 가지가 더 일어날 수 있다. 경찰청이 증명서를 Wallet의 키와 연결하기로 정했다면, Wallet은 증명서를 요청할 때 그 키로 서명한 값을 함께 보낸다. 그 키를 실제로 가진 쪽만 만들 수 있는 값이다. 경찰청은 그 키를 가리키는 정보를 증명서 안에 넣어 서명해서 내준다. 2편에서 말한, 증명서를 사용자 휴대폰의 키와 연결해 두는 일이 여기서 일어난다. 키가 휴대폰 밖으로 나가지 않게 보관되어 있다면, 증명서 파일만 복사해서는 다른 휴대폰에서 쓸 수 없다. 제시할 때마다 그 키로 새로 서명해야 하기 때문이다. 키와 연결할지는 Issuer가 증명서 종류마다 정한다.
두 번째 단계는 1편에서 본 장면과 닮았다. 1편에서 JM렌터카는 카카오 로그인으로 가입을 받았다. 사용자가 카카오에서 동의하면 카카오가 일회용 코드를 돌려주고, JM렌터카 서버가 그 코드를 카카오에 보내 Access Token과 로그인 결과인 ID Token으로 바꿔 오는 흐름이었다. 이 흐름의 바탕이 OAuth 2.0이다. OAuth 2.0은 권한 위임 규격이다. 비밀번호를 넘기지 않고, 사용자가 동의한 범위만큼만 권한을 맡긴다.
OpenID4VCI와 OpenID4VP도 OAuth 2.0을 바탕으로 한다. 다만 두 규격이 OAuth 2.0을 쓰는 방식은 다르다. 발급은 OAuth 2.0의 권한 위임을 대부분 그대로 쓰고, 제시는 OAuth 2.0의 요청·응답 틀만 빌려 쓴다.
발급에서 Wallet은 1편의 JM렌터카와 같은 일을 한다. 1편에서 JM렌터카가 Access Token을 받아 카카오의 API를 불렀듯, Wallet은 Access Token을 받아 경찰청의 발급 창구를 부른다.
제시에서는 사정이 다르다. 이번에는 Wallet이 1편의 카카오와 비슷한 일을 한다. JM렌터카가 1편처럼 요청을 보내면, Wallet은 요청을 받아 사용자의 동의를 얻고 답한다. 다만 코드를 토큰으로 바꾸는 단계도, Access Token도 없다.
돌아오는 것도 다르다. 1편의 ID Token은 답을 보낸 카카오가 그 자리에서 만들어 서명한 로그인 결과였다. 답을 보낸 쪽이 곧 내용을 보증하는 쪽이었다. 제시에서 답을 보내는 것은 Wallet이지만, 내용을 보증하는 것은 몇 주 전에 디지털 운전면허에 서명한 경찰청이다. JM렌터카가 믿는 것도 Wallet이 아니라 경찰청의 서명이다. 그래서 제시하는 순간에는 경찰청이 끼지 않아도 된다. 이 글에서는 Wallet이 보내는 이 답을 제시 응답이라고 부른다.
OpenID4VP는 OpenID Connect(OIDC)가 아니라 OAuth 2.0 위에 정의된 규격이다. OIDC는 1편에서 본, 로그인 결과를 전달하는 규격이다. 제시 응답은 로그인 결과가 아니다. JM렌터카는 로그인을 1편처럼 카카오 로그인으로 받고, 운전 자격은 제시 응답으로 따로 확인한다.
OpenID4VP는 Verifier가 Wallet에 ‘무엇을 보여 달라’고 요청하고 Wallet이 응답하는 절차를 정한 규격이다.
JM렌터카의 요청에는 조건이 들어간다. 운전면허 증명서에서 이름, 면허 유효 여부, 성인 여부(만 19세 이상)를 보여 달라는 조건이다. 어떤 항목을 요청할지는 JM렌터카가 정하고, 그 조건을 적는 방법은 OpenID4VP가 정한다. 조건은 DCQL로 적는다. DCQL은 Verifier가 요청 조건을 적는 JSON 형식의 조건문이다.
요청을 받은 Wallet은 조건에 맞는 증명서를 찾아 사용자에게 보여 준다. “JM렌터카가 이름, 면허 유효 여부, 성인 여부를 요청합니다” 같은 화면에서 확인을 누르면, 요청한 세 항목만 공개된 제시 응답이 JM렌터카로 간다. 다만 검증(verification)에 필요한 서명과 유효기간 같은 값은 함께 담긴다.
Wallet은 요청을 보낸 곳이 진짜 JM렌터카인지 확인할 수 있다. 1편처럼 서비스를 Wallet마다 미리 등록하지 않아도 되도록, 규격은 Verifier를 확인하는 방법을 몇 가지 정해 두었다. 요청에 서명하는 방법이면 Wallet이 요청자를 가려낼 수 있지만, 서명하지 않는 방법도 있어서 모든 요청을 같은 수준으로 가려내지는 못한다.
그렇다면 가로챈 제시를 다른 곳에서 다시 쓰는 일은 어떻게 막을까? JM렌터카는 요청을 보낼 때마다 이번에만 쓰는 임의의 값을 새로 만들어 넣는다. 증명서가 Wallet의 키와 연결되어 있다면, Wallet은 제시할 때 이 값과 ‘받는 쪽은 JM렌터카’라는 표시를 함께 넣어 그 키로 서명한다. JM렌터카는 서명 안에 자기가 보낸 값과 자기를 가리키는 받는 쪽 표시가 들어 있는지 반드시 확인해야 한다. 1편에서 ID Token을 받을 서비스가 자기 자신인지 확인하던 것과 같은 발상이다. 누군가 이 제시를 가로채 다른 사이트에 내밀면 받는 쪽 표시가 맞지 않고, 다른 요청에 내밀면 값이 맞지 않는다. 키와 연결되지 않은 증명서에는 이런 장치가 없다. 다시 쓰지 못하게 막는 것과 내용을 가리는 것은 다른 문제다. 내용을 가리는 응답 암호화는 따로 있다.
응답을 받은 뒤의 일은 2편에서 본 그대로다. JM렌터카는 서명을 확인하고 필요하면 증명서 상태도 확인한 뒤, 경찰청이 발급한 면허인지를 따진다. 상태를 확인할 때도 제시마다 경찰청에 묻지 않고, 경찰청이 게시해 둔 목록을 미리 받아 보는 방식이 있다. 경찰청 면허만 받는다는 신뢰 정책은 표준이 아니라 JM렌터카가 정한 것이다.
2편에서 디지털 증명서의 형식이 여러 가지라고 했다. 그렇다면 형식과 프로토콜은 어떤 관계일까?
프로토콜과 형식의 관계는 우편 절차와 봉투에 빗댈 수 있다. 우편 절차가 프로토콜이고, 봉투가 형식이다. 정확히 말하면 OpenID4VCI와 OpenID4VP 같은 프로토콜은 누가 누구에게 무엇을 어떤 순서로 보내는지를 정하고, 형식은 증명서 데이터를 어떻게 담고 서명하는지를 정한다.
대표적인 형식은 세 가지다.
OpenID4VCI와 OpenID4VP는 특정 형식에 묶이지 않는다. 같은 요청·응답의 틀에 세 형식을 모두 담고, 형식마다 다른 부분은 부록에서 따로 정한다.
제시 절차도 OpenID4VP 하나뿐은 아니다. mdoc에는 별도의 제시 방식도 있다. mdoc을 온라인에서 제시하는 방법을 정한 ISO 문서, ISO/IEC 18013-7의 부록 C다. 이 방식은 5편에서 브라우저와 함께 다시 나온다.
같은 운전면허를 경찰청이 mdoc이 아니라 SD-JWT VC로 발급했다고 해 보자. JM렌터카가 요청하는 이름, 면허 유효 여부, 성인 여부라는 내용은 같다. JM렌터카가 요청하고 Wallet이 응답하는 흐름도 같다. 달라지는 것은 응답 안에 든 증명서의 모양과, JM렌터카가 그 증명서를 검증하는 방법이다. 그래서 JM렌터카는 받을 형식마다 검증 구현을 따로 갖춰야 한다.
여기까지 보면 각자 정할 것이 드러난다. JM렌터카는 무엇을 요청할지, 어떤 Issuer를 믿을지, 증명서 상태를 확인할지, 어떤 형식을 받을지, 키와 연결된 증명서만 받을지, 어떤 Wallet과 맞출지를 정한다. 경찰청은 어떤 형식으로 발급할지, 키와 연결할지, 본인 확인을 언제 할지를 정한다. 어느 것도 표준이 대신 정해 주지 않는다.
이제 경찰청, JM렌터카, Wallet은 같은 규칙을 따른다. 다만 규칙 안에도 선택지가 많아서, 실제로 서로 통하려면 대상 Wallet과 생태계가 정한 조합에 맞춰야 한다. 어떤 Wallet과 맞출지가 정할 일에 들어가는 이유다.
그런데 도입 장면에서 그냥 넘어간 대목이 있다. ‘Wallet에 추가’를 누르자 Wallet이 열렸다고만 했다. 프로토콜은 무엇을 주고받을지를 정하고 Wallet을 여는 링크 형식도 몇 가지 정해 두지만, 사용자의 휴대폰에 깔린 Wallet A와 Wallet B 중 어느 것이 열릴지까지는 정하지 않는다. JM렌터카 사이트가 노트북 브라우저에 열려 있고 Wallet은 휴대폰에 있다면 문제는 더 커진다.
발급과 제시는 서로 다른 프로토콜이 정하며, 프로토콜은 사용자의 휴대폰에서 어느 Wallet이 열릴지까지 정해 주지 않는다.
Wallet과 주고받을 메시지는 정해졌다. 그렇다면 JM렌터카 사이트가 열린 브라우저는 어떤 경로로 사용자의 Wallet을 찾아 요청을 전달할까?