같은 잔액, 서로 다른 두 판단
은행 응답 하나를 끝까지 따라갑니다. 자료가 검사를 통과한 뒤에도 서비스가 가입을 거절하는 이유를 살펴봅니다.
5,000만 원의 의미부터 정합니다
premium.example은 사용 가능한 잔액이 5,000만 원 이상인 사람에게 프리미엄 가입을 허용한다고 가정합니다.
하지만 응답에서 큰 숫자 하나를 찾는 것만으로는 충분하지 않습니다. 어떤 계좌의 어떤 잔액인지, 통화는 무엇인지, 신용한도를 합산한 값인지부터 정해야 합니다.
이 예시는 acct_demo_7F21의 ITAV(지정 시점의 가용 잔액)를 고릅니다. CreditDebitIndicator가 Credit이고 통화가 KRW이며 금액이 50000000.00 이상이어야 합니다. CreditLine은 없거나 모든 Included 값이 false여야 합니다.
이 조건이 참이라는 주장을 claim이라고 부릅니다. premiumEligible: true는 여기서 정한 잔액 조건을 만족했다는 뜻입니다. 가입 승인은 아직 남아 있습니다.
금액뿐 아니라 통화, 잔액 종류, 신용한도 포함 여부까지 같은 조건으로 검사합니다.
잔액 조건 원문
- Type은 ITAV
- Credit 상태
- KRW 50,000,000.00 이상
- CreditLine은 합산하지 않음
주장과 서비스 정책을 나누는 이유
제출 자료가 말하는 내용과 서비스가 실제로 가입을 허용하는 조건은 같은 판단이 아닙니다.
claim(검증할 주장)은 정해진 응답과 판정 조건에서 premiumEligible이 true였다는 진술입니다. 이 자료가 검증 완료(verified) 상태가 되어도 premium.example이 가입을 허용했다는 뜻은 아닙니다.
| 구분 | 누가 정하는가 | 무엇을 답하는가 |
|---|---|---|
| 검증할 주장 | Prover와 Verifier가 합의한 형식 | 제출 자료가 지정한 조건을 만족하는가? |
| 서비스 정책 | premium.example | 지금 이 가입 요청을 받아들일 것인가? |
서비스는 허용하는 자료의 나이, 가입 기준 금액, 사용 대상, 받아들일 증명 방식을 바꿀 수 있습니다. 최종 가입 결정을 재현하려면 검증 결과와 당시의 정책 버전을 함께 봐야 합니다.
잔액 조건에 포함되는 값과 제외되는 값
ITAV 잔액과 CreditLine을 섞으면 같은 숫자라도 다른 뜻이 됩니다.
Data.Balance는 배열이므로 먼저 어느 항목을 판정할지 골라야 합니다. 이 예시는 AccountId가 acct_demo_7F21이고 Type이 ITAV인 항목을 사용합니다. AccountId는 API 안의 계좌 식별자일 뿐, 그 자체로 제출자의 계좌 소유권까지 증명하지 않습니다.
Type == ITAV
AND CreditDebitIndicator == Credit
AND Amount.Currency == KRW
AND decimal(Amount.Amount) >= 50000000.00
AND (CreditLine is absent OR every CreditLine.Included == false)CreditLine.Included가 false라는 값은 표시된 잔액에 그 신용한도를 합산하지 않았다는 뜻입니다. 신용한도가 없거나 부채가 없다는 뜻은 아닙니다. 72,840,000원 역시 이 응답 시점의 가용 잔액이지 총자산이나 순자산이 아닙니다.
이번 가입 요청에 연결합니다
Verifier(검증자)는 이번 요청을 식별하는 challenge(검증 요청값)를 발급합니다.
제출 자료에는 challenge와 audience(사용 대상)인 premium.example을 함께 묶습니다. 다른 요청에서 받은 자료나 다른 서비스용 자료를 그대로 가져오지 못하도록, 검증할 때 이 값들도 확인합니다.
challenge의 만료와 잔액 자료의 유효 시간은 별개입니다. 요청값이 아직 유효하더라도 잔액이 관측된 시점이 너무 오래됐다면 현재 가입 정책을 통과하지 못할 수 있습니다.
무슨 조건인지에 더해, 누가 어떤 요청에 사용할 자료인지도 정합니다.
검증 요청 예제
{"sessionId":"trace_zktls_001","challenge":"challenge_demo_001","audience":"premium.example","issuedAt":"2026-08-25T10:14:20+09:00","expiresAt":"2026-08-25T10:29:20+09:00"}검증 요청 만료와 서비스 최신성
challenge의 만료 시각과 서비스가 요구하는 응답 최신성은 따로 검사합니다.
challenge(검증 요청값)는 제출 자료를 이번 가입 요청과 premium.example에 묶습니다. expiresAt은 그 요청값을 언제까지 받을지 정합니다. 잔액이 언제 관측됐는지는 API 응답의 DateTime이 따로 알려 줍니다.
| 시간값 | 이 예시의 값 | 검사 |
|---|---|---|
| challenge.issuedAt | 2026-08-25T10:14:20+09:00 | 이번 검증 요청이 시작된 시각 |
| challenge.expiresAt | 2026-08-25T10:29:20+09:00 | 요청값을 받아들일 수 있는 마지막 시각 |
| Balance.DateTime | 2026-08-25T10:14:32+09:00 | 잔액이 관측된 시각 |
challenge가 아직 유효해도 잔액 자료의 나이가 서비스 허용 범위를 넘을 수 있습니다. 반대로 잔액이 충분히 최근이어도 만료된 challenge를 다시 쓰면 이번 가입 요청에 맞는 제출 자료로 받아들이지 않습니다.
응답의 숫자를 같은 규칙으로 읽습니다
Prover(제출자)가 api.bank.example에서 받은 잔액은 72,840,000원입니다.
로그인 자격 정보는 원본 서버에만 보냅니다. 응답의 Data.Balance 배열에서 대상 계좌와 ITAV 항목을 고르고 Amount.Amount의 문자열 72840000.00을 Amount.Currency의 KRW와 함께 읽습니다.
같은 숫자라도 읽은 위치와 해석 방법이 다르면 판정이 달라질 수 있으므로, parser(해석기)의 규칙도 검사합니다. 화면에서 숫자를 검색하는 방식으로는 부족합니다. 응답 원문을 읽은 JSON 필드 경로와 선택한 항목, 정확한 십진수로 비교했는지까지 함께 확인해야 같은 값으로 판정할 수 있습니다.
CreditLine.Included가 false이면 그 한도를 잔액에 합산하지 않은 것입니다. 신용한도나 부채의 유무와는 별개입니다. 이 잔액은 해당 시점의 값이며 총자산이나 순자산도 아닙니다.
72840000.00이라는 문자열뿐 아니라 그 값의 위치와 해석 규칙을 확인합니다.
API 응답 원문
{
"Data": {
"Balance": [
{
"AccountId": "acct_demo_7F21",
"Amount": {
"Amount": "72840000.00",
"Currency": "KRW",
"SubType": "BCUR"
},
"CreditDebitIndicator": "Credit",
"Type": "ITAV",
"DateTime": "2026-08-25T10:14:32+09:00",
"CreditLine": [
{
"Included": false,
"Amount": {
"Amount": "10000000.00",
"Currency": "KRW",
"SubType": "BCUR"
},
"Type": "Pre-Agreed"
}
]
}
]
},
"Links": {
"Self": "https://api.bank.example/open-banking/v4.0/aisp/accounts/acct_demo_7F21/balances"
},
"Meta": {
"TotalPages": 1
}
}UK Open Banking v4.0을 참고한 테스트 응답
필드 구조만 공개 표준을 참고했으며 주소와 계좌, 통화, 잔액은 모두 교육용입니다.
합성 테스트 응답의 뼈대는 UK Open Banking Read/Write API v4.0의 OBReadBalance1을 참고했습니다. Data.Balance가 배열이고 각 항목에 Amount, CreditDebitIndicator, Type, DateTime이 놓이는 구조를 따릅니다.
| 필드 | 이 예시의 값 | 읽는 법 |
|---|---|---|
| Type | ITAV | 업무일 중 지정 시점의 가용 잔액 |
| CreditDebitIndicator | Credit | 잔액의 부호 방향 |
| Amount | 72840000.00 KRW | 통화와 금액 문자열을 함께 해석 |
| CreditLine[0].Included | false | 표시된 잔액에 해당 한도를 합산하지 않음 |
응답 바이트를 주장에 묶는 JSON 해석 규칙
필드 경로나 숫자 해석이 달라지면 같은 응답도 다른 주장이 될 수 있습니다.
parser(해석기)는 화면에서 72840000.00이라는 문자열만 찾지 않습니다. 응답 원문을 JSON으로 읽고 Data.Balance 배열에서 대상 항목을 고른 뒤 Amount.Amount와 Amount.Currency를 정해 둔 규칙으로 해석합니다.
금액은 JSON 문자열이므로 십진수 규칙을 명시해야 합니다. 부동소수점으로 바꾸면서 반올림하거나 천 단위를 잘못 처리하면 경계값 판정이 달라질 수 있습니다. 이 예시는 50000000.00과 정확한 십진수로 비교합니다.
- 01 response bytes
- 02 JSON 규칙
- 03 Balance 항목 선택
- 04 decimal 비교
- 05 premiumEligible
잔액은 남겨 두고 조건의 결과를 보냅니다
Verifier에게는 실제 잔액 대신 premiumEligible: true와 검증에 필요한 자료를 제출합니다.
여기에는 서로 다른 두 작업이 필요합니다. 먼저 데이터가 지정한 서버의 통신에서 왔음을 확인할 근거가 있어야 합니다. 이어서 그 데이터의 금액이 5,000만 원 이상임을, 금액을 공개하지 않고 보여야 합니다.
selective disclosure(선택 공개)는 응답의 일부 바이트만 보여 주는 기능입니다. 금액을 가리기만 해서는 숨긴 금액이 기준 이상이라는 결론을 얻을 수 없습니다. 이 글의 비공개 비교에는 별도의 영지식 증명(ZKP)이 필요합니다.
그 비교 결과도 앞에서 정한 응답, 해석 규칙, 조건, challenge와 연결돼야 합니다. true라는 문자열 하나를 보내는 것만으로는 검증할 수 없습니다.
공개하지 않는 것과, 숨긴 값의 조건을 증명하는 것은 서로 다른 기능입니다.
선택 공개와 영지식 증명은 무엇이 다른가
바이트 일부를 공개하는 방식과 숨긴 값의 조건을 증명하는 방식은 구분해야 합니다.
selective disclosure(선택 공개)는 TLS 통신 기록 가운데 고른 바이트를 보여 주고 나머지를 가립니다. 금액 필드를 공개하면 Verifier가 직접 72,840,000원을 읽을 수 있지만, 실제 잔액을 숨긴 채 5,000만 원 이상이라는 결과만 내는 기능은 아닙니다.
| 방식 | Verifier가 보는 것 | 이 예시에서의 결과 |
|---|---|---|
| 선택 공개 | 선택한 응답 바이트 | Amount를 열면 실제 잔액도 보임 |
| 영지식 증명 | 정해진 명제와 증명 | 잔액은 숨기고 5,000만 원 이상만 확인 |
검사를 통과해도 아직 가입 전입니다
Verifier는 제출 자료를 검사하고 verified 결과를 냅니다.
기대한 서버와 세션에서 나온 자료인지, 이번 challenge와 사용 대상에 맞는지 확인합니다. 응답과 JSON 해석 규칙이 연결됐는지, 정해진 조건과 공개 결과가 일치하는지도 검사합니다.
모두 통과하면 자료 검증이 끝납니다. 제출자의 신원이나 계좌 소유권은 별도로 확인해야 합니다. AccountId는 API가 계좌를 식별하는 값이므로, 그 계좌가 제출자의 것임을 확인하는 절차가 따로 필요합니다.
verified가 답하는 질문은 “이 자료가 정해진 검사를 통과했는가?”입니다.
Verifier가 확인하는 항목
출처, 세션 결합, challenge, 공개 범위, 판정 조건을 각각 검사합니다.
검증자는 proofValid 같은 플래그 하나만 읽지 않습니다. 제출 자료가 기대한 원본 서버, 이번 challenge, 정해 둔 JSON 해석 규칙과 판정 조건에 묶였는지 각각 검사해야 합니다.
| 검사 | 실패하면 알 수 없는 것 |
|---|---|
| 원본 서버와 세션 | 어느 서버 통신에서 나온 자료인가 |
| challenge와 사용 대상 | 이번 premium.example 요청에 낸 자료인가 |
| 응답·parser·필드 경로 | 어떤 원문 값을 판정했는가 |
| 판정 조건과 공개 결과 | premiumEligible: true가 무엇을 뜻하는가 |
모든 검사를 통과하면 결과는 verified입니다. 이는 제출 자료가 정해 둔 구조와 암호학적 검사를 통과했다는 뜻일 뿐, 제출자의 신원이나 계좌 소유권, 가입 허용을 보장하지 않습니다.
가입 허용은 서비스가 결정합니다
premium.example은 검증 결과에 현재의 가입 정책을 적용합니다.
자료가 충분히 최근인지, 증명한 금액 조건이 현재 기준과 같은지, 서비스가 받아들이는 검증 방식인지 확인합니다. 이 예시에서는 필요한 정책 검사를 모두 통과한 경우에만 approved, 즉 가입 허용으로 넘어갑니다.
검증 결과와 가입 결정을 따로 남기면 나중에도 이유를 설명할 수 있습니다. 어떤 자료를 검사했고 어떤 정책을 언제 적용했는지 함께 남깁니다.
verified는 자료 검사 결과이고 approved는 현재 정책에 따른 가입 결정입니다.
검증과 가입 판단 비교
| 단계 | 묻는 질문 | 결과 |
|---|---|---|
| Verify | 제출 자료가 유효한가? | verified |
| Rely | 현재 서비스 정책으로 허용할 것인가? | approved 또는 denied |
검증 이후에도 남는 서비스 판단
가입 가능 여부에는 서비스의 현재 기준 금액, 대상, 시간 정책이 반영됩니다.
premium.example은 verified 결과를 입력으로 받되, 그 결과만으로 가입을 확정하지 않습니다. 현재 기준 금액, 자료의 유효 시간, 허용한 검증 방식, 가입 대상과 같은 서비스 정책을 이어서 적용합니다.
| 검증 결과 | 현재 정책 검사 | 최종 상태 |
|---|---|---|
| verified | 모두 통과 | approved |
| verified | 자료가 너무 오래됨 | denied |
| verified | 기준 금액이 변경됨 | reproofRequired |
나중에 결정을 설명하려면 어떤 제출 자료를 검사했는지뿐 아니라 당시의 정책 버전과 판정 시각도 남겨야 합니다. verified와 approved를 한 상태값으로 합치면 정책 변경으로 생긴 거절을 증명 실패처럼 잘못 읽게 됩니다.
자료는 그대로인데 결정이 달라집니다
자료 검증과 가입 판단이 달라지는 이유를 살펴보기 위해, 같은 verified 자료를 두 가지 상황에 적용해 봅니다.
첫째, 잔액 자료가 만들어진 지 480초가 지났는데 서비스는 300초 이내의 자료만 받습니다. 자료 검사는 통과해도 최신성 정책에서 denied, 즉 가입 거절이 됩니다. 이 사례에서는 challenge 자체는 아직 유효하다고 가정합니다.
둘째, 가입 기준이 5,000만 원에서 7,000만 원으로 바뀝니다. 기존 자료로 확인한 조건은 5,000만 원 이상입니다. 새 조건으로 다시 제출해야 하는 reproofRequired가 됩니다.
실제 예시 잔액인 72,840,000원은 새 기준보다도 높습니다. 하지만 Verifier에게 금액을 숨겼으므로 기존 결과만으로는 이를 알 수 없습니다. 잔액 부족을 발견한 것도, 기존 proof가 위조됐다는 뜻도 아닙니다.
시간이나 정책이 바뀌어도, 유효한 자료가 위조된 자료로 바뀌는 것은 아닙니다.
두 실패 사례 비교
| 변경 | 검증 | 최종 판정 |
|---|---|---|
| 자료 나이 480초, 허용 범위 300초 | 통과 | denied |
| 가입 기준 5,000만 원 → 7,000만 원 | 기존 조건은 통과 | reproofRequired |
거절, 탐지, 사각지대로 실패를 나누기
어느 단계가 실패를 막는지, 이상을 알아채기만 하는지, 아예 보지 못하는지를 구분합니다.
거절(reject)은 해당 단계가 입력을 받아들이지 않고 흐름을 멈춘 경우입니다. 탐지(detect)는 이상을 알아냈지만 별도 정책이 후속 행동을 정하는 경우입니다. 사각지대(blind spot)는 현재 제출 자료와 검사만으로 판단할 수 없는 범위입니다.
| 사례 | 분류 | 판정 지점 |
|---|---|---|
| challenge 값 또는 결합이 맞지 않음 | reject | 제출 자료 검증 |
| 자료 나이 480초, 허용 범위 300초 | detect → denied | 서비스 최신성 정책 |
| 가입 기준 5,000만 원 → 7,000만 원 | detect → reproofRequired | 현재 가입 정책 |
| AccountId만으로 실제 계좌 소유자 확인 | blind spot | 별도의 사용자–계좌 결합 필요 |