이 글에서 다루는 내용

이 글에서는 분산 신원 증명(DID) 기반으로 지갑 앱(Wallet) 과 검증 앱(Verifier) 이 VC(Verifiable Credential) 를 주고받을 때, 각 주체가 어떤 검증을 책임져야 하는지, 왜 본인 매칭 검증은 반드시 Verifier 쪽에서 수행해야 하는지를 다룬다. 실무에서 "A 계정으로 발급받은 VC 가 B 계정에서도 통과되는데 이게 맞는 걸까?" 와 같은 의문을 해소하고, Wallet 측 구현자와 Verifier 측 구현자가 각자 빠뜨리기 쉬운 검증 항목을 체크리스트로 정리한다.

DID 기반 VC 연동의 기본 구조

DID 생태계에서 VC 를 주고받는 흐름은 세 주체로 구성된다.

  • Issuer (발급자): 사용자의 자격이나 속성을 확인하고 VC 를 발급. 발급된 VC 에는 Issuer 의 개인키 서명이 담긴다.
  • Holder / Wallet (지갑 앱): 사용자가 발급받은 VC 를 기기 로컬에 안전하게 저장하고, 검증자의 요청에 따라 VC 를 제시(Present) 한다.
  • Verifier (검증 앱/서비스): 특정 자격이 필요한 서비스 제공자. Wallet 에 검증 요청 스펙(VerifyProfile) 을 전달해 원하는 VC 를 받고, 그 VC 를 검증한다.

같은 앱이 Issuer + Wallet 을 겸할 수 있지만, Verifier 는 일반적으로 독립된 별도 주체로 설계된다.

전체 데이터 흐름

[Verifier 앱]                [Wallet 앱]                   [사용자]
     │                           │                            │
     │── ① VerifyProfile 전달 ──▶│                            │
     │    (요청 스펙, nonce,       │                            │
     │     Verifier 공개키)        │                            │
     │                           │                            │
     │                           │── 로그인/인증 ──────────▶  │
     │                           │◀── 사용자 세션 확정 ────    │
     │                           │                            │
     │                           │── 로컬 VC 조회 + 유효성 검증 │
     │                           │                            │
     │                           │── VC 본문을 Verifier         │
     │                           │    공개키로 암호화 → vcCopy  │
     │                           │                            │
     │◀── ② vcCopy 전달 ─────────│                            │
     │                           │                            │
     │── ③ 자기 비밀키로 복호화  │                            │
     │── ④ VC 서명/발급자/본인 매칭 검증                       │
     │── ⑤ 결과에 따라 서비스 진행 또는 거부                   │
     │                           │                            │

핵심 포인트는 ② 단계에서 전달되는 vcCopy 는 Verifier 공개키로 암호화되어 있다는 것이다. Wallet 이나 네트워크 중간에서는 내용이 보이지 않고, 오직 Verifier 만 자기 비밀키로 열 수 있다.

VerifyProfile 이 담는 것과 담지 않는 것

VerifyProfile 은 Verifier 가 Wallet 에 전달하는 "어떤 VC 를 원한다" 는 요청 페이로드다. Base64URL 로 인코딩된 JSON 형태가 일반적이다.

예시 구조

{
  "id": "did:<method>:<verifier-did-value>",
  "profile": {
    "name": "<서비스명>",
    "type": "VERIFY",
    "callBackUrl": "<결과 수신 URL>",
    "nonce": "<랜덤값>",
    "publicKey": "<암호화용 공개키>",
    "encryptType": 2,
    "filter": {
      "allowIssuerList": ["did:<method>:<issuer-did>"],
      "requiredAssertionList": [
        { "id": "<자격 타입>", "name": "<자격명>", "type": "<assertion type>" }
      ],
      "requiredPrivacyList": []
    }
  },
  "proof": {
    "signatureValue": "<Verifier 서명>",
    "type": "Secp256k1VerificationKey2018"
  }
}

담는 정보

  • 무엇을 원하는지: requiredAssertionList (자격 타입, 자격명)
  • 누가 발급한 것만 허용: allowIssuerList (허용 Issuer DID)
  • 암호화에 쓸 공개키: publicKey, encryptType
  • 재전송 공격 방지: nonce
  • 위변조 방지: proof.signatureValue

의도적으로 담지 않는 정보

  • 요청 대상 사용자의 개인정보 (이름, 생년월일, CI/DI 등)
  • Verifier 앱에 현재 로그인된 사용자 식별자

담지 않는 이유는 명확하다. Wallet 이 Verifier 측 사용자 정보를 알 필요가 없도록 설계해야 프라이버시가 보호된다. 만약 VerifyProfile 에 사용자 식별자가 들어간다면, 악의적 Wallet 이 사용자 정보를 수집할 수 있는 경로가 생긴다.

Wallet 이 해야 하는 검증

Wallet 은 크게 세 영역의 검증을 수행한다.

수신한 VerifyProfile 검증

항목 설명
Scheme / Host 매칭 자기 앱이 처리하는 정당한 deeplink 인지 확인
VerifyProfile 서명 검증 proof.signatureValue 유효성, 위변조 여부
Issuer 조건 부합 allowIssuerList 에 자신이 발급한 VC 의 issuer DID 포함 여부
요청 Assertion 타입 지원 requiredAssertionList 에 해당하는 VC 가 로컬에 있는지

로컬 VC 유효성 검증

항목 설명
VC 존재 여부 로컬 저장소에 해당 타입 VC 가 있는지
최신 발급분 일치 서버의 최신 발급 ID 와 로컬 VC 의 발급 ID 매칭
유효기간 체크 VC 의 만료 여부
본인 세션 일치 현재 로그인 사용자의 VC 인지 (세션 컨텍스트)

vcCopy 생성 및 전달

항목 설명
VC 본문 암호화 Verifier 의 publicKey 로 비대칭 암호화
메타데이터 래핑 발급자 DID, nonce, encryptType 등 포함
안전한 전달 URL scheme 을 통해 Verifier 앱에 vcCopy 전달

Wallet 이 수행하지 않는 검증

  • Verifier 앱에 로그인된 사용자가 누구인지 알아내려 하지 않는다.
  • Verifier 사용자와 Wallet 사용자가 동일인인지 매칭하지 않는다.
  • 제시하는 VC 가 "정당한 요청 대상자의 것" 인지 판단하지 않는다.

이 세 가지를 Wallet 이 하려 하면 Verifier 의 사용자 컨텍스트를 알아야 하는데, 그러려면 프라이버시를 해치게 된다. 사용자가 어떤 계정으로 Wallet 에 로그인했는가에 따라 제시되는 VC 가 결정될 뿐, 그 이상의 매칭은 Wallet 의 역할이 아니다.

Verifier 가 해야 하는 검증

Verifier 의 검증은 네 단계로 나뉜다.

vcCopy 복호화 및 구조 검증

항목 설명
vcCopy 복호화 자신의 비밀키로 data 필드 복호화
encryptType 매칭 요청 시 지정한 암호화 방식과 일치
nonce 검증 VerifyProfile 의 nonce 와 매칭, 재전송 방지
필수 필드 존재 복호화된 VC 구조의 완전성

VC 무결성 및 발급자 검증

항목 설명
VC 서명 검증 Issuer 공개키로 proof.signatureValue 검증
Issuer DID 일치 허용 Issuer 목록과 일치
발급일/유효기간 발급 시점과 만료 여부
취소 여부 필요 시 VC Revocation Registry 조회

자격 내용 검증

항목 설명
Assertion 타입 일치 요청한 타입의 VC 가 맞는지
자격 상태 "가입 자격 있음" 등 요구 상태 충족 여부

본인 매칭 검증 (핵심)

항목 설명
VC 주체 ↔ Verifier 로그인 사용자 매칭 복호화된 VC 내 사용자 식별자(CI/DI/개인정보) 가 현재 Verifier 앱에 로그인된 사용자 와 일치하는지
불일치 시 거부 처리 매칭 실패 시 명확한 오류로 거부, 감사 로그 기록

본인 매칭은 Wallet 이 대신할 수 없다. 반드시 Verifier 가 수행해야 한다. 이 검증이 누락되면 다음과 같은 오용이 가능해진다.

  • A 계정으로 Wallet 에 로그인해서 A 의 VC 를 제시 → Verifier 앱에는 B 로 로그인 → B 가 A 의 자격으로 혜택을 받음
  • 제3자가 타인 Wallet 계정으로 발급받은 VC 를 자신의 Verifier 세션에서 재사용

신뢰 경계와 책임 분담 도식

                          신뢰 경계
                             ┃
  ┌──── Wallet 측 책임 ────┐ ┃ ┌──── Verifier 측 책임 ────┐
  │                        │ ┃ │                          │
  │ • 로그인 사용자의 VC 제시│ ┃ │ • vcCopy 복호화           │
  │ • VC 암호화              │ ┃ │ • VC 서명/발급자 검증     │
  │ • VerifyProfile 서명 확인│ ┃ │ • 본인 매칭 검증           │
  │                        │ ┃ │ • 서비스 로직 적용        │
  │                        │ ┃ │                          │
  └────────────────────────┘ ┃ └──────────────────────────┘
                             ┃
                      암호화된 vcCopy
                       nonce, timestamp

Wallet 은 Verifier 측 정보를 모르고, Verifier 는 Wallet 측 저장 상태를 모른다. 두 주체를 잇는 연결 고리는 암호화된 vcCopy 와 서명뿐이다. 자기 사용자 컨텍스트를 아는 Verifier 만 본인 매칭을 수행할 수 있다.

자주 발생하는 오해

"Wallet 에서도 본인 확인을 해야 하는 것 아닌가?"

Wallet 은 현재 로그인한 사용자 기반으로 VC 를 제시할 뿐, "요청자가 누구를 위한 검증을 원하는지" 를 알 수 없다. 본인 매칭은 구조적으로 Verifier 만 가능하다.

"VerifyProfile 에 사용자 정보를 담으면 되지 않는가?"

프라이버시 원칙에 반한다. 또한 Wallet 에 접근 가능한 악의적 앱이 Verifier 사용자 정보를 수집할 경로가 된다. DID 표준은 이를 명시적으로 배제한다.

"Wallet 이 여러 계정의 VC 를 동시에 갖고 있으면 문제 아닌가?"

그 자체는 문제가 아니다. 사용자가 VC 를 제시할 때 어느 계정으로 로그인했는지가 결정되며, 어떤 VC 가 제시되든 본인 매칭은 Verifier 에서 걸러진다.

"A 의 VC 로 B 가 통과된다 = Wallet 버그인가?"

아니다. Verifier 가 본인 매칭을 구현했는지 먼저 확인해야 한다. Verifier 에서 구현하지 않았다면 그것이 취약점이고, Wallet 의 설계 문제는 아니다.

보조 방어선 (Defense in Depth)

필수는 아니지만 Wallet 측에서 이중 안전장치로 추가할 수 있는 조치들이다.

조치 효과 비용
Wallet 로그인 시 본인 인증 강화 로그인 자체의 본인성 보장 낮음
Wallet 서버 제출 API 에서 세션 사용자 ↔ VC 주체 이중 체크 Verifier 누락 시 백업 차단 중간
VC 제시 전 사용자 확인 UI 사용자 실수 방지 낮음
감사 로그 (민감정보 제외) 사후 추적 낮음

근본 책임은 Verifier 에 있고, Wallet 측 방어선은 보조적 이중 안전장치로 운용한다.

구현자 체크리스트

Wallet 구현 시

  • VerifyProfile 서명 검증 (SDK 기본 기능 활용)
  • 로컬 VC 유효기간 / 발급 일치성 검증
  • 사용자 로그인 기반의 VC 선택
  • Verifier 공개키로 VC 암호화
  • 필요 시 UI 확인 단계 추가
  • 감사 로그 (민감정보 제외)

Verifier 구현 시

  • vcCopy 복호화 (자기 비밀키)
  • nonce / 재전송 방지 검증
  • VC 서명 / 발급자 DID 검증
  • VC 유효기간 / 취소 여부 검증
  • 본인 매칭 — VC 내 사용자 식별자와 현재 로그인 사용자 일치 체크
  • 불일치 시 거부 / 경고 / 감사 로그
  • 서비스 로직 적용 전 모든 검증 통과 확인

맺음말

DID 기반 시스템의 보안성과 프라이버시는 역할 분담을 얼마나 정확히 이해하고 구현하느냐에 달려 있다. 한쪽이 상대 역할까지 떠안으려 하면 표준 위반 또는 과도한 정보 수집으로 귀결되고, 한쪽이 자기 역할을 누락하면 전체 시스템이 우회 가능한 취약점을 갖게 된다. Wallet 은 "누구의 VC 를 제시할지" 만 결정하고, 본인이 맞는지에 대한 최종 책임은 자기 사용자 컨텍스트를 아는 Verifier 가 진다는 것을 잊지 말자.