호패DOCS
데모 열기
문서 목차프라이버시와 ZK
Concepts

프라이버시와 ZK

검증 결과를 공개 원장에 올리면서 개인정보를 지키는 방법입니다. 온체인에는 커밋먼트 루트 32바이트만 올라가고, 원본 값은 사용자 브라우저에 남습니다. 그 구조를 제3자가 우리 코드 없이 직접 확인하는 방법까지 정리했습니다.

무엇이 어디에 있나

위치저장되는 것저장되지 않는 것
온체인종류·등급·증적 해시·claimsRoot·auditRef (모두 32바이트 이하 판정값)이름, 생년월일, 신분증, 연락처 — 어떤 형태로도 올리지 않습니다
사용자 브라우저크리덴셜 — 입력한 값과 필드별 salt발급 서버로 전송되지 않습니다
발급 서버심사 결과, 증적 레코드, 커밋먼트 루트신원 원본 값 (브라우저에서 이미 커밋먼트로 바뀐 뒤 전달됩니다)
검증 벤더신원 검증 원본 (연동 이후)호패는 참조 ID와 판정만 받습니다

그래서 어테스테이션이 공개하는 사실은 “이 지갑은 어느 등급으로 검증됐고 언제 만료된다”까지입니다. 그 이상은 커밋먼트 뒤에 있고, 열지 말지는 사용자가 정합니다.

다음 단계가 술어 증명입니다. 값을 열지 않고 조건만 증명합니다. 생년월일을 보내지 않고 성년임을 증명하는 age_over 회로가 그 첫 사례이며, 온체인 검증까지 동작합니다.

검증 가능한 주장 4가지

주장내용
결속온체인 claimsRoot는 검증 필드들의 Poseidon2 커밋먼트 루트다. pre-image에서 재계산하면 비트 단위로 일치한다.
은닉온체인에는 루트 32바이트뿐. 필드별 128-bit 랜덤 salt 때문에 루트에서 값을 역산할 수 없다.
폐기 전이어테스테이션이 폐기·만료되면 claimsRootOf()가 0을 반환한다.
술어 증명age_over 회로가 커밋된 생년월일을 공개하지 않고 성년임을 증명하고, 온체인 검증자(UltraHonk + HopaeZkGate)가 이를 검증했다 (아래 시연 tx).
현재 한계: 기준일(cutoff)은 검증자가 제공하고, 회로는 외부 감사 전이며, 브라우저 안에서 증명을 생성하는 UI는 로드맵입니다. 지금 동작하는 것은 회로 → 로컬 증명 → 온체인 검증의 전 구간입니다.

방법 1 — 이 사이트에서 (크리덴셜 검증 탭)

  1. 발급 체험에서 입력한 신원 정보로 브라우저가 크리덴셜(값 + salt)을 만들고, 이 브라우저에만 저장합니다.
  2. 크리덴셜 검증 탭에서 “내 크리덴셜 검증”을 누르면 브라우저가 루트를 재계산하고 레지스트리의 claimsRootOf()를 읽어 대조합니다.
  3. 필드 하나를 골라 선택 공개를 검증합니다. 나머지 필드는 비공개로 남습니다.
  4. 위변조 테스트는 값을 1 바꿔 재검증합니다. 실패해야 정상입니다.
  5. 콘솔 탭에서 폐기하면 같은 검증이 즉시 실패로 바뀝니다.

방법 2 — 독립 스크립트

저장소에서 — 공개된 시연 크리덴셜로 재현sh
pnpm install   # 최초 1회
node sdk/scripts/verify-credential.mjs \
  docs/deployment/evidence/v2/issue-A.credential.json

이 스크립트는 SDK를 import하지 않습니다. 커밋먼트 수학을 자체 재구현하고, 온체인 조회도 raw eth_call로 합니다. 시연 지갑 A의 크리덴셜은 시뮬레이션 데이터라 재현 검증용으로 공개되어 있습니다. 실사용자 크리덴셜은 본인 브라우저에만 존재합니다.

방법 3 — cast만으로

스키마 UID 재계산 · 온체인 루트 조회sh
RPC=https://sepolia-rpc.giwa.io
REG=0x3C05Bc238b74d99ad9d8c0345d0f64BD249234d2
A=0x2C98FAf3900E04Eb4eB37d5141619F426A120aB9

# 스키마 UID가 정말 v2 스키마 문자열에서 나온 값인지
cast keccak "$(cast concat-hex "$(cast from-utf8 \
  'uint16 kind,uint8 level,bytes32 evidenceHash,bytes32 claimsRoot,bytes32 auditRef')" \
  0x0000000000000000000000000000000000000000 0x01)"
cast call $REG 'schemaUID()(bytes32)' -r $RPC   # 위 값과 일치해야 함

# 유효성 결합 뷰 — 폐기·만료 시 0
cast call $REG 'claimsRootOf(address,uint16)(bytes32)' $A 1 -r $RPC

전체 절차(어테스테이션 5필드 디코드·증적 해시 대조 포함)는 저장소 docs/zk-verify.md에 있습니다.

온체인 시연 트랜잭션

장면tx
성년 ZK 증명 온체인 검증 — 생년월일은 어디에도 없음0x666f2135ac3b… ↗
폐기된 지갑의 유효한 증명 → 게이트 revert (의도된 실패)0xd0cdc3750c2f… ↗

시연 tx 전체와 증명 아티팩트는 저장소 docs/deployment.md docs/deployment/evidence/v2/에 있습니다.

커밋먼트 스펙 (제3자 재구현용)

hopae-credential/2text
리프:    leaf_i = Poseidon2(keyId, value, salt)        // BN254 스칼라 필드
트리:    깊이 4 고정(슬롯 16), 빈 슬롯 리프 = 0, 부모 = Poseidon2(L, R)
salt:    필드별 독립 128-bit CSPRNG — 발급 시 생성, 사용자에게만 전달
keyId:   1=DOB(YYYYMMDD) · 2=NATIONALITY · 3=RESIDENCY('KR'→0x4B52)
         · 4=SCREENED_AT(unix초) · 5=RISK_BAND(1~5)
  • ·해시는 Poseidon2 (BN254)입니다. Noir 회로 쪽 구현과 동일 다이제스트임을 고정 벡터로 검증합니다. 온체인은 해시를 재계산하지 않고 저장된 bytes32만 비교합니다.
  • ·회로: age_over (Noir) — 커밋 개봉 + 머클 포함 + 생년월일 비교 + 지갑 바인딩. 재현 절차는 저장소 circuits/README.md에 있습니다.