이것이 실제로 하는 일
크리덴셜은 누군가가 서명한 한 문장입니다. “이 사람은 만 18세 이상이다.” “이 사람은 자동차를 운전할 수 있다.” 그 문장에 값어치를 주는 것이 서명입니다. 서명은 어떤 기관이 그 내용을 보증한다는 뜻이고, 그 뒤로 아무도 고치지 않았다는 뜻입니다.
종이에서도 PDF에서도 서명 하나가 문서 전체를 덮습니다. 한 줄을 증명하려면 전부를 건네야 합니다. 나이만 보이려고 운전면허증을 내밀면 상대는 주소와 면허 번호와 정확한 생년월일까지 알게 됩니다. 상대는 그런 것을 요구한 적이 없습니다. 그래도 갖게 됩니다.
선택적 공개는 그 거래를 없앱니다. 발급자가 각 항목에 따로 서명하고, 크리덴셜은 항목마다 지문만 담습니다. 무엇을 보일지는 본인이 고르고, 나머지는 내용을 전혀 드러내지 않는 지문으로 남습니다. 서명은 쪼갤 수 없는 한 덩어리에 걸린 적이 없으므로 그대로 검증을 통과합니다.
이 모든 것이 검증자가 읽는 코드 안에 들어갑니다. 서명도, 지문도, 본인이 골라 보여 준 항목도 함께입니다. 검증하는 동안 아무것도 받아오지 않으므로 출입문에서도, 버스 안에서도, 지방 진료소에서도, 순항 중인 비행기에서도 네트워크 없이 확인됩니다.
마지막 한 조각이 남았고, 흔히 잊히는 것이 바로 이것입니다. 읽을 수 있는 것은 사진으로도 찍을 수 있습니다. 그래서 제시하는 사람은 기기를 떠나지 않는 개인 키로 그 자리의 챌린지에 서명해야 합니다. 이 단계가 없으면 남의 크리덴셜 화면 캡처도 통과합니다. 이 라이브러리는 그것이 빠진 제시를 반드시 거부합니다.
신호가 없는 그 순간을 위해 만들었습니다
저는 4천만 명 넘게 쓰는 브라질의 디지털 운전면허증 eCNH를 만들었습니다. 가장 많이 배운 건 앱이 아니었습니다. 길가였습니다. 신호가 한 칸, 혹은 아예 없는 국도에서 면허증을 읽고 1초 안에 예 또는 아니오를 내놓아야 하는 현장이었습니다.
그 답에 필요한 모든 것이 코드 안에 들어갑니다. 서명이 발급자를 증명하고, 클레임은 바로 거기 있으며, 바깥에서 필요한 건 발급자의 공개키 하나뿐입니다. 그건 좀처럼 바뀌지 않아서 앱에 함께 실어 보내고 주 1회 갱신하면 그만입니다.
이런 용도의 라이브러리는 있습니다. 다만 기업용 SDK입니다. 무겁고, 한 나라의 프로파일에 묶여 있고, 읽는 사람이 이미 신원 업계에서 일한다는 전제로 쓰였습니다. 이건 제품 엔지니어가 화요일에 추가할 수 있는 버전입니다.
세 당사자, 그중 서버는 하나도 없습니다
각 클레임에 한 번 서명
발급자는 나중에 숨길 수 있는 클레임을 정하고, 그 각각에 다이제스트를 서명합니다.
요구된 것만 보내기
지갑은 남겨 둘 클레임을 빼놓습니다. 남은 내용에 대해 발급자의 서명은 여전히 유효합니다.
누구에게도 묻지 않고 답하기
서명, 만료, 폐기, 그리고 모든 공개를 기기에 고정된 키와 대조합니다.
생년월일을 넘기지 않고 만 18세 이상임을 증명하기
연령 확인 법규가 도구보다 빨리 도착하고 있습니다. 흔한 구현은 손님이 신분증 사진을 제3자에게 올리게 합니다. 이는 프라이버시 재앙이자, 터질 날만 기다리는 유출입니다.
선택적 공개는 이것을 제대로 합니다. 검증자는 원해도 생년월일을 알 수 없습니다. 그 값이 지갑을 떠난 적이 없기 때문입니다. 이 성질은 암호학적인 것이지 개인정보 처리방침 속 약속이 아닙니다.
// 자격증명에는 이름, 주소, 생년월일, 문서 번호가 들어 있습니다.
// 술집이 받는 건 불리언 하나입니다.
const presentation = await present(credential, { disclose: ['over_18'] })
const result = await verify(presentation, { trust })
result.claims // { over_18: true }
result.claims.birth_date // undefined, 전송된 적이 없습니다
result.withheld // 4, 다만 어떤 넷인지는 알 수 없습니다
실제 숫자, 불편한 것까지 포함해서
현실적인 운전면허증. 클레임 여덟 개, 5년 만료, 상태 목록 포인터. 추정이 아니라 측정한 값입니다.
| 자격증명 | 문자 수 | QR 버전 |
|---|---|---|
| 전부 공개 | ~740 | 18, 잘 읽힘 |
| 여덟 클레임 모두 공개 가능 | ~1590 | 27, 너무 빽빽함 |
presenting only over_18 | ~1115 | 22, 여전히 빽빽함 |
fits()가 현재 위치를 알려 주게 하세요.
제 말을 믿지 마세요
플레이그라운드는 라이브러리 전체를 브라우저에서 돌리고, 실제 공격 여덟 가지를 던집니다. 각각이 예상하는 거절 코드를 함께 보여 주므로, README를 믿는 대신 라이브러리를 그 자신의 주장과 맞대어 확인할 수 있습니다.
왜 이것이고, 언제 아닌가
고를 이유 다섯 가지, 그리고 솔직한 이야기.
- 직접 읽을 수 있는 약 1,300줄엔지니어 한 명이 오후 한나절에 전부 읽고 무슨 일을 하는지 알 수 있는 크기입니다. 읽을 수 없는 보안은 믿음으로 받아들이는 보안입니다.
- 의존성 없음런타임에 npm에서 가져오는 것이 없습니다. 감사할 공급망은 이것 하나뿐이고, 다음 주에 발밑에서 바뀌는 것도 없습니다.
- 완전히 확인하지 못하는 것은 거부합니다부분 통과도, 넘겨도 되는 경고도 없습니다. 모든 실패는 기록하고 처리할 수 있는 이름 붙은 사유로 돌아옵니다. 맨 false로 돌려주지 않습니다.
- 검증이 일어나는 곳에서 돌아갑니다Node, Deno, Bun, 브라우저, React Native, 엣지 워커. 어디서나 같은 코드가 플랫폼에 이미 들어 있는 표준 암호 위에서 돕니다.
- 세 가지 방식으로 검증단위 테스트, RFC 9901과 함께 공개된 공식 테스트 벡터, 그리고 무작위로 망가진 입력을 던져 빠져나가는 경우를 찾는 속성 기반 테스트입니다.
그리고 이것이 맞지 않는 경우.
- ISO 18013-5 모바일 운전면허증이 필요한 경우그쪽은 CBOR와 COSE라는 다른 인코딩이고 표준 자체가 다릅니다. 이 라이브러리는 유럽 지갑과 OpenID 명세가 쓰는 형식인 SD-JWT를 다룹니다. 가까운 문제지만 다른 일입니다.
- 지갑 전체가 필요한 경우이것은 검증하고 제시합니다. 크리덴셜을 보관하지도, 기기를 관리하지도, 온보딩을 처리하지도 않습니다. 제품 안에 들어가도록 만든 한 부품입니다.
- 규제 환경 도입 전에 제3자 감사가 필요한 경우아직 없습니다. 테스트와 RFC 테스트 벡터가 공개되어 있고 코드도 전부 공개되어 있지만, 규제 환경에 쓰려면 자체 검토 예산을 잡아 두시기 바랍니다.
설치
npm i qredential
Node 20 이상, 최신 브라우저 전부, React Native. 런타임 의존성 없이 약 1,300줄. 믿기 전에 읽는 일이 현실적이도록 일부러 그렇게 두었습니다.
발명이 아니라 표준
- SD-JWT, RFC 9901선택적 공개와 키 바인딩. 중첩과 재귀까지 지원하며, 유럽 신원 지갑이 쓰는 그 방식입니다
- SD-JWT VC자격증명의 형태
- Token Status List캐시된 사본으로도 동작하는 폐기 확인
- base45, RFC 9285QR 봉투. 리더 호환성을 보고 골랐습니다
ISO 18013-5 mDL은 아닙니다. 그쪽은 JWT가 아니라 CBOR과 COSE입니다. 예정에는 있고, 지원하지 않는 것을 쓰는 자격증명은 반쯤 이해되는 대신 거절됩니다. 적합성 표준의 절반을 했다고 주장하는 것은 아무것도 주장하지 않는 것보다 나쁩니다.