문제는 "다 보여주거나, 아무것도 못 보여주거나"
신분증을 보여줄 때 우리는 이름·주소·주민등록번호·사진을 한꺼번에 보여줍니다. 술을 사는 데 필요한 것은 "성인인가" 하나인데도 그렇습니다. 디지털에서도 마찬가지였습니다. 서명된 문서는 일부만 떼어내면 서명이 깨지니까요.
SD-JWT(Selective Disclosure JWT)는 이 제약을 푼 구조입니다.
어떻게 동작하나
핵심 아이디어는 이렇습니다. 발급자는 각 항목의 값을 직접 서명하지 않고, 값의 해시를 서명합니다.
1. 발급자가 각 항목마다 무작위 값(salt)을 붙여 `[salt, "birthdate", "1994-03-21"]` 같은 구조를 만듭니다.
2. 이 구조의 해시를 계산해 자격증명 본문에 넣고, 본문 전체에 서명합니다.
3. 원래 값이 담긴 구조는 별도 조각(disclosure) 으로 지갑에 보관됩니다.
제시할 때 지갑은 서명된 본문과 함께 공개할 항목의 조각만 보냅니다. 검증자는 받은 조각을 해시해서 본문 안의 해시와 대조합니다. 일치하면 그 값은 발급자가 서명한 값이 맞습니다. 조각을 보내지 않은 항목은 본문에 해시만 남아 있고, 해시에서 원래 값을 알아낼 수 없습니다. salt가 붙어 있어 사전 대입도 막힙니다.
서명은 그대로 유효합니다. 일부만 보내도 깨지지 않습니다.
"성인 여부"는 한 걸음 더
생년월일 자체를 숨기려면 발급자가 `age_over_18: true` 같은 파생 항목을 미리 발급해 두어야 합니다. 이 항목은 별도 조각이므로, 생년월일 조각을 보내지 않고 이것만 보낼 수 있습니다. 검증자는 나이를 계산할 필요도, 알 필요도 없습니다.
여기서 중요한 설계 판단이 하나 있습니다. 무엇을 파생 항목으로 만들어 둘지는 발급 시점에 정해집니다. 발급자가 `age_over_18`만 만들어 두었다면, 나중에 "19세 이상"이 필요해져도 새로 발급받아야 합니다. 스키마 설계가 그래서 중요합니다.
제시자가 진짜 소유자인지는 별개 문제
선택적 공개는 "값이 진짜인가"를 해결하지만 "이 사람이 그 자격증명의 주인인가"는 해결하지 않습니다. 조각을 통째로 복사해 다른 사람이 쓸 수 있으니까요.
그래서 Key Binding JWT가 붙습니다. 발급 시점에 자격증명 본문에 보유자의 공개키를 넣어 두고, 제시할 때 지갑이 그 키로 nonce와 검증자 식별자를 서명합니다. 검증자는 이 서명까지 확인합니다. 복사된 조각만으로는 통과하지 못합니다.
검증자가 실제로 확인하는 것은 결국 네 가지입니다.
1. 발급자 서명이 유효한가
2. 받은 조각의 해시가 본문과 일치하는가
3. Key Binding JWT가 본문의 공개키로 서명됐고, nonce와 대상이 맞는가
4. 이 자격증명이 폐지되지 않았는가 (상태 목록 조회)
넷 중 하나라도 어긋나면 거부합니다. 컨포먼스 스위트가 집요하게 시험하는 것도 정확히 이 네 가지의 실패 경로입니다.
실무에서 걸리는 지점
- 로그를 남길 때 조각을 그대로 저장하면 선택적 공개의 의미가 사라집니다. 검증 영수증에는 "무엇을 확인했다"만 남기고 값 자체는 남기지 않는 설계가 필요합니다.
- 해시만 남은 항목도 존재 사실은 드러납니다. "이 자격증명에 주소 항목이 있다"는 것까지는 숨겨지지 않습니다.
- 조각 수가 많아지면 제시 크기가 커집니다. QR로 전달할 때 실질적인 제약이 됩니다.