Systems Notebook

zkTLS / API 응답과 서비스 판단

zkTLS로 API 응답의 잔액 조건을 증명하는 과정

잔액을 공개하지 않고 5,000만 원 이상임을 확인받는다면, 서비스 가입까지 허용될까요?

흐름 살펴보기
API 응답에서 가입 결정까지
premium.example 가입 조건 premiumEligible: true

잔액 대신 제출할 조건 결과
아직 검증하지 않은 교육용 예시입니다.

  1. 조건 정의
  2. 요청 연결
  3. 응답 해석
  4. 증명 생성
  5. 자료 검증
  6. 가입 결정

bank.example과 premium.example, 계좌와 잔액은 모두 교육용 가상 데이터입니다.

UK Open Banking v4.0의 잔액 구조를 참고합니다. 실제 은행 조회나 proof 생성은 실행하지 않습니다.

TLS 기반 출처 확인에 별도의 비공개 비교를 결합한 개념 모델입니다. 특정 zkTLS 구현의 기본 기능을 뜻하지 않습니다.

같은 잔액, 서로 다른 두 판단

은행 응답 하나를 끝까지 따라갑니다. 자료가 검사를 통과한 뒤에도 서비스가 가입을 거절하는 이유를 살펴봅니다.

5,000만 원의 의미부터 정합니다

premium.example은 사용 가능한 잔액이 5,000만 원 이상인 사람에게 프리미엄 가입을 허용한다고 가정합니다.

하지만 응답에서 큰 숫자 하나를 찾는 것만으로는 충분하지 않습니다. 어떤 계좌의 어떤 잔액인지, 통화는 무엇인지, 신용한도를 합산한 값인지부터 정해야 합니다.

이 예시는 acct_demo_7F21의 ITAV(지정 시점의 가용 잔액)를 고릅니다. CreditDebitIndicator가 Credit이고 통화가 KRW이며 금액이 50000000.00 이상이어야 합니다. CreditLine은 없거나 모든 Included 값이 false여야 합니다.

이 조건이 참이라는 주장을 claim이라고 부릅니다. premiumEligible: true는 여기서 정한 잔액 조건을 만족했다는 뜻입니다. 가입 승인은 아직 남아 있습니다.

금액뿐 아니라 통화, 잔액 종류, 신용한도 포함 여부까지 같은 조건으로 검사합니다.

KRW 가용 잔액 5,000만 원 이상을 검사합니다. Credit 상태이며 신용한도는 잔액에 합산하지 않습니다.
잔액 조건 원문
  • 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 안의 계좌 식별자일 뿐, 그 자체로 제출자의 계좌 소유권까지 증명하지 않습니다.

text 교육용 예시값
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의 만료와 잔액 자료의 유효 시간은 별개입니다. 요청값이 아직 유효하더라도 잔액이 관측된 시점이 너무 오래됐다면 현재 가입 정책을 통과하지 못할 수 있습니다.

무슨 조건인지에 더해, 누가 어떤 요청에 사용할 자료인지도 정합니다.

Verifier가 Prover에게 challenge를 발급합니다. 제출 자료를 이번 premium.example 요청에 연결합니다.
검증 요청 예제
json 교육용 예시값
{"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.issuedAt2026-08-25T10:14:20+09:00이번 검증 요청이 시작된 시각
challenge.expiresAt2026-08-25T10:29:20+09:00요청값을 받아들일 수 있는 마지막 시각
Balance.DateTime2026-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이라는 문자열뿐 아니라 그 값의 위치와 해석 규칙을 확인합니다.

Prover가 은행 응답에서 72,840,000원이라는 잔액을 읽습니다. 응답 원문과 JSON 해석 규칙을 함께 묶습니다.
API 응답 원문
json 교육용 예시값
{
  "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이 놓이는 구조를 따릅니다.

이 글이 판정에 사용하는 필드
필드이 예시의 값읽는 법
TypeITAV업무일 중 지정 시점의 가용 잔액
CreditDebitIndicatorCredit잔액의 부호 방향
Amount72840000.00 KRW통화와 금액 문자열을 함께 해석
CreditLine[0].Includedfalse표시된 잔액에 해당 한도를 합산하지 않음
응답 바이트를 주장에 묶는 JSON 해석 규칙

필드 경로나 숫자 해석이 달라지면 같은 응답도 다른 주장이 될 수 있습니다.

parser(해석기)는 화면에서 72840000.00이라는 문자열만 찾지 않습니다. 응답 원문을 JSON으로 읽고 Data.Balance 배열에서 대상 항목을 고른 뒤 Amount.Amount와 Amount.Currency를 정해 둔 규칙으로 해석합니다.

금액은 JSON 문자열이므로 십진수 규칙을 명시해야 합니다. 부동소수점으로 바꾸면서 반올림하거나 천 단위를 잘못 처리하면 경계값 판정이 달라질 수 있습니다. 이 예시는 50000000.00과 정확한 십진수로 비교합니다.

검증 절차도 응답 바이트가 정해 둔 구조와 십진수 규칙으로 해석됐음을 확인해야 같은 판정을 냅니다.

잔액은 남겨 두고 조건의 결과를 보냅니다

Verifier에게는 실제 잔액 대신 premiumEligible: true와 검증에 필요한 자료를 제출합니다.

여기에는 서로 다른 두 작업이 필요합니다. 먼저 데이터가 지정한 서버의 통신에서 왔음을 확인할 근거가 있어야 합니다. 이어서 그 데이터의 금액이 5,000만 원 이상임을, 금액을 공개하지 않고 보여야 합니다.

selective disclosure(선택 공개)는 응답의 일부 바이트만 보여 주는 기능입니다. 금액을 가리기만 해서는 숨긴 금액이 기준 이상이라는 결론을 얻을 수 없습니다. 이 글의 비공개 비교에는 별도의 영지식 증명(ZKP)이 필요합니다.

그 비교 결과도 앞에서 정한 응답, 해석 규칙, 조건, challenge와 연결돼야 합니다. true라는 문자열 하나를 보내는 것만으로는 검증할 수 없습니다.

공개하지 않는 것과, 숨긴 값의 조건을 증명하는 것은 서로 다른 기능입니다.

Prover는 실제 잔액을 숨기고 premiumEligible: true를 제출합니다. TLS 기반 출처 확인과 별도의 ZKP 비교를 결합한 개념 모델입니다.
선택 공개와 영지식 증명은 무엇이 다른가

바이트 일부를 공개하는 방식과 숨긴 값의 조건을 증명하는 방식은 구분해야 합니다.

selective disclosure(선택 공개)는 TLS 통신 기록 가운데 고른 바이트를 보여 주고 나머지를 가립니다. 금액 필드를 공개하면 Verifier가 직접 72,840,000원을 읽을 수 있지만, 실제 잔액을 숨긴 채 5,000만 원 이상이라는 결과만 내는 기능은 아닙니다.

공개 범위에 따라 필요한 기법이 달라집니다.
방식Verifier가 보는 것이 예시에서의 결과
선택 공개선택한 응답 바이트Amount를 열면 실제 잔액도 보임
영지식 증명정해진 명제와 증명잔액은 숨기고 5,000만 원 이상만 확인

검사를 통과해도 아직 가입 전입니다

Verifier는 제출 자료를 검사하고 verified 결과를 냅니다.

기대한 서버와 세션에서 나온 자료인지, 이번 challenge와 사용 대상에 맞는지 확인합니다. 응답과 JSON 해석 규칙이 연결됐는지, 정해진 조건과 공개 결과가 일치하는지도 검사합니다.

모두 통과하면 자료 검증이 끝납니다. 제출자의 신원이나 계좌 소유권은 별도로 확인해야 합니다. AccountId는 API가 계좌를 식별하는 값이므로, 그 계좌가 제출자의 것임을 확인하는 절차가 따로 필요합니다.

verified가 답하는 질문은 “이 자료가 정해진 검사를 통과했는가?”입니다.

Verifier가 서버, 요청, 응답 해석, 잔액 조건을 확인합니다. verified가 되어도 서비스 가입은 아직 결정하지 않았습니다.
Verifier가 확인하는 항목

출처, 세션 결합, challenge, 공개 범위, 판정 조건을 각각 검사합니다.

검증자는 proofValid 같은 플래그 하나만 읽지 않습니다. 제출 자료가 기대한 원본 서버, 이번 challenge, 정해 둔 JSON 해석 규칙과 판정 조건에 묶였는지 각각 검사해야 합니다.

verified에 이르기 전의 주요 검사
검사실패하면 알 수 없는 것
원본 서버와 세션어느 서버 통신에서 나온 자료인가
challenge와 사용 대상이번 premium.example 요청에 낸 자료인가
응답·parser·필드 경로어떤 원문 값을 판정했는가
판정 조건과 공개 결과premiumEligible: true가 무엇을 뜻하는가

모든 검사를 통과하면 결과는 verified입니다. 이는 제출 자료가 정해 둔 구조와 암호학적 검사를 통과했다는 뜻일 뿐, 제출자의 신원이나 계좌 소유권, 가입 허용을 보장하지 않습니다.

가입 허용은 서비스가 결정합니다

premium.example은 검증 결과에 현재의 가입 정책을 적용합니다.

자료가 충분히 최근인지, 증명한 금액 조건이 현재 기준과 같은지, 서비스가 받아들이는 검증 방식인지 확인합니다. 이 예시에서는 필요한 정책 검사를 모두 통과한 경우에만 approved, 즉 가입 허용으로 넘어갑니다.

검증 결과와 가입 결정을 따로 남기면 나중에도 이유를 설명할 수 있습니다. 어떤 자료를 검사했고 어떤 정책을 언제 적용했는지 함께 남깁니다.

verified는 자료 검사 결과이고 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가 위조됐다는 뜻도 아닙니다.

시간이나 정책이 바뀌어도, 유효한 자료가 위조된 자료로 바뀌는 것은 아닙니다.

검증은 두 사례 모두 verified입니다. 자료 나이 480초가 허용 범위 300초를 넘으면 denied, 기준이 5,000만 원에서 7,000만 원으로 바뀌면 reproofRequired입니다. 위조나 잔액 부족을 뜻하지 않습니다.
두 실패 사례 비교
같은 verified 결과가 서로 다른 최종 상태로 이어지는 두 사례
변경검증최종 판정
자료 나이 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별도의 사용자–계좌 결합 필요

검증이 끝나면 서비스가 판단합니다

이 예시에서는 은행 응답에서 필요한 조건을 정해 이번 요청에 연결하고 잔액을 숨긴 제출 자료를 만들었습니다. Verifier는 그 자료에 담긴 주장을 확인했습니다.

서비스는 그 결과를 지금 사용할 수 있는지 판단합니다. 이 두 단계를 구분해야 시간 만료나 정책 변경을 암호학적 검증 실패와 혼동하지 않습니다.

기술 출처

기존 Act별 출처와 확인 날짜를 유지하고, 이번 개정에서 확인한 자료를 덧붙였습니다. 구현 세부사항은 각 문서의 버전과 함께 읽어주세요.

Act 1 · 검사 조건을 먼저 정한다

Act 3 · 은행 응답을 받는다

Act 4 · 응답과 판정 규칙을 묶는다

  • RFC 8259 - JSON ↗

    JSON 구조와 숫자 표현. 금융 API의 문자열 금액은 별도의 십진수 해석 규칙으로 비교합니다. 2026-09-09 확인.

Act 5 · 잔액 대신 조건을 증명한다