본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 09. 10:56

AI에게 블랙박스를 부여하세요: 약 150줄의 Python 코드로 구현하는 검증 가능한 결정 로그

요약

AI 시스템의 결정 과정을 투명하게 증명하기 위한 '결정 출처(decision-provenance)' 구현 방법을 다룹니다. VeritasChain Protocol(VCP)을 활용하여 변조 방지, 독립적 검증, 누락 탐지, 외부 앵커링 기능을 갖춘 블랙박스 로깅 시스템을 Python으로 구축하는 가이드를 제공합니다.

핵심 포인트

  • AI 결정의 투명성을 위한 결정 출처(decision-provenance) 개념 정의
  • 해시 체인과 공개 키 서명을 이용한 변조 방지 및 검증 구현
  • UUIDv7과 가명화 처리를 통한 데이터 무결성 및 보안 준수
  • VCP 이벤트 모델을 활용한 체계적인 결정 로그 구조 설계

2026년 5월 말, 공격자들은 Meta의 AI 지원 어시스턴트에게 자신이 소유하지 않은 계정에 복구 이메일을 추가해 달라고 요청함으로써 20,000개 이상의 Instagram 계정을 탈취했습니다. 익스플로잇 코드(exploit code)는 없었습니다. 어시스턴트는 '이 이메일 변경은 승인됨'이라는 보안 결정을 수천 번 내렸고, 그 과정에서 남겨진 흔적은 회사 외부의 누구도 조사할 수 있도록 설계되지 않았습니다.

이것은 결정 출처 (decision-provenance) 문제이며, 가장 많은 관심을 받고 있는 콘텐츠 출처 (content-provenance, C2PA, SynthID) 문제와는 다릅니다. 질문은 "이 이미지는 어디에서 왔는가?"가 아닙니다. "자동화된 시스템이 언제, 어떤 입력값에 대해, 어떤 로직을 바탕으로 무엇을 결정했는지 누군가 증명할 수 있는가? 그리고 그 기록이 몰래 수정되거나 누락되지 않았음을 증명할 수 있는가?"입니다.

대부분의 시스템은 로그(logs)로 이 질문에 답합니다. 하지만 로그는 블랙박스가 아닙니다. 이 포스트에서는 알고리즘 트레이딩의 결정 출처를 위한 오픈 스펙인 VeritasChain Protocol (VCP)의 이벤트 모델을 사용하여, 적은 양의 Python 코드로 블랙박스 역할을 할 시스템을 구축해 보겠습니다. 동일한 네 가지 속성이 모든 중대한 자동화된 결정에 적용됩니다.

기록이 항공기의 블랙박스처럼 작동하려면 일반적인 로깅(logging)에는 없는 네 가지 속성을 갖추어야 합니다:

  1. 변조 흔적 발견 (Tamper-evidence): 외부인이 확인할 수 있어야 함 (해시 체인 (hash chain))
  2. 독립적 검증 가능성 (Independent verifiability): 공유 비밀(shared secrets)이 아닌 공개 키 서명 (public-key signatures) 사용
  3. 누락 탐지 (Omission detection): 각 항목뿐만 아니라 집합의 _완전성 (completeness)_에 대해 커밋(commit)해야 함
  4. 외부 앵커링 (External anchoring): 운영자가 다시 쓸 수 없는 어딘가에 커밋을 게시해야 함

우리는 각각을 구현할 것입니다. 표준 라이브러리 외의 유일한 의존성은 cryptography입니다:

pip install cryptography

이벤트 모델 (The event model)

VCP 이벤트는 세 가지 계층의 구조를 가집니다: header (누가/무엇을/언제), payload (결정 관련 데이터), 그리고 security 블록 (암호학적 결합 요소)입니다. 미리 숙지해둘 몇 가지 규칙은 다음과 같습니다: ID는 시간순으로 정렬될 수 있도록 UUIDv7 (RFC 9562)을 사용합니다. 금융 값은 정밀도가 직렬화 (serialization) 과정에서 유지되도록 부동 소수점 (float)이 아닌 항상 **문자열 (string)**로 처리합니다. 계정 ID는 기록되기 전에 가명화 (pseudonymized) 처리되며, 이는 GDPR 준수에 있어 매우 중요합니다.

다음은 알고리즘이 행동을 결정하는 실제 SIG (signal, 신호) 이벤트의 예시입니다:

{
  "header": {
    "event_id": "01934e3a-7b2c-7f93-8f2a-1234567890ab",
...

이벤트 유형은 하나의 생명 주기를 형성합니다: SIG (signal, 신호) → ORD (order, 주문) → ACK (acknowledged, 승인) → EXE (executed, 실행) 순이며, 이 외에도 REJ (rejected, 거절), CXL (cancelled, 취소), RSK (risk control, 리스크 제어) 등이 있습니다. 각 유형은 고정된 정수 코드(SIG=1, ORD=2, ACK=3, EXE=4, REJ=6, …)를 가지므로 의미가 변질되지 않습니다.

기본 요소들을 설정해 보겠습니다. 먼저, 규격을 준수하는 UUIDv7과 가명화 도구(pseudonymizer)입니다:

import time
import secrets
import hashlib
...

속성 1: 해시 체인을 통한 변조 증거 (tamper-evidence)

핵심 아이디어는 해시 체인 (hash chain)입니다. 각 이벤트는 자신의 콘텐츠 해시(event_hash)와 이전 이벤트의 해시(prev_hash)를 함께 포함합니다. 각 연결 고리가 이전 고리를 확정(commit)하기 때문에, 과거의 어떤 이벤트를 수정하더라도 해당 이벤트의 해시가 변경되며, 이는 그 이후의 모든 이벤트를 깨뜨리게 됩니다. 그리고 이 파손은 체인을 보유한 누구라도 감지할 수 있습니다.

한 가지 미묘한 점은 _정확히 무엇을 해싱할 것인가_입니다. 두 당사자가 동일한 데이터에 대해 서로 다른 해시를 계산하지 않으려면, 정형화되고 결정론적인 직렬화 (deterministic serialization)가 필요합니다. VCP는 RFC 8785 스타일의 정형 JSON (canonical JSON, 키 정렬 및 공백 제거)을 사용합니다. 참조 구현체는 이벤트의 식별 정보와 타이밍, 전체 페이로드(payload), 그리고 이전 해시를 확정합니다:

def event_hash(header: dict, payload: dict, prev_hash: str) -> str:
    """정형 JSON에 대한 SHA-256 (RFC 8785 스타일: 키 정렬, 압축)."""
    canonical = {
...

이제 체인을 유지하는 로거(logger)를 구현하겠습니다. **속성 2(Property 2)**를 동시에 적용할 것인데, 단 한 줄이면 충분하기 때문입니다. 각 event_hash를 Ed25519 개인키(private key)로 서명하는 것입니다.

속성 2: 공개키 서명을 통한 독립적 검증 가능성 (independent verifiability with public-key signatures)

이것은 조용히 가장 중요한 속성이며, 대부분의 "변조 방지(tamper-proof)" 로그들이 놓치는 부분이기도 합니다. 일부 도구들은 키 기반 해시(HMAC)로 항목들을 체이닝합니다. 이는 공유 키를 보유한 사람 — 대개 운영자(operator) — 에게 무결성을 증명합니다. 하지만 비밀 키를 전달받지 않고서는 독립적인 감사자(auditor)가 아무것도 검증할 수 없게 만듭니다. 공개키 서명(Public-key signatures)은 이 문제를 해결합니다. 운영자는 **개인키(private key)**로 서명하고, **공개키(public key)**를 가진 사람은 누구나 검증할 수 있는 반면, 운영자 외에는 아무도 유효한 서명을 생성할 수 없습니다. VCP는 Ed25519를 사용하며 (현재 포스트 양자 ML-DSA / FIPS 204로 마이그레이션 중입니다).

class DecisionLog:
    def __init__(self, signing_key: Ed25519PrivateKey, venue_id="DEMO-VENUE"):
        self.key = signing_key
...

현실적인 라이프사이클을 방출해 보겠습니다. 하나의 trace_id를 공유하는 신호(signal), 그로 인해 생성된 주문(order), 그리고 체결(fill)입니다:

key = Ed25519PrivateKey.generate()
log = DecisionLog(key)

...
SIG 9f2c1a04e7b6d8c0 <- 0000000000000000
ORD 3b71e9aa55c2f014 <- 9f2c1a04e7b6d8c0
EXE c0d4f8819ab33e72 <- 3b71e9aa55c2f014

prev_hash는 이전 이벤트의 event_hash와 같습니다. 이제 검증자(verifier)를 살펴보겠습니다. 여기서 주목할 점은 검증자에게는 비밀스러운 것이 전혀 필요 없으며, 오직 **공개키(public key)**만 필요하다는 것입니다:

def verify_chain(events, public_key: Ed25519PublicKey):
    prev = GENESIS
    for i, e in enumerate(events):
...
(True, '3 events verified')

이제 우리가 악의적인 행위자(bad actor)가 되어 보겠습니다. 누군가 사후에 실행 가격(execution price)을 변경했다고 가정해 봅시다. 예를 들어, 체결 가격이 보고된 것보다 더 나빴던 경우입니다:

log.events[2]["payload"]["trade_data"]["execution_price"] = "2649.00"
print(verify_chain(log.events, pub))
(False, 'event 2: hash mismatch (record was altered)')

수정 사항이 정규 바이트(canonical bytes)를 변경했으므로, 재계산된 해시(hash)는 더 이상 저장된 해시와 일치하지 않습니다. 만약 공격자가 저장된 해시까지 일치하도록 다시 작성했다면, 대신 체인 링크(chain link)와 그 이후의 모든 prev_hash가 깨지게 되며 서명(signature) 검증에 실패하게 됩니다. 개인 키(private key) 없이는 세 가지 검증을 모두 통과할 수 있는 단일 필드 수정은 존재하지 않습니다.

속성 3: 누락 탐지 (omission detection)

다음은 더 까다로운 공격이며, 대부분의 감사 추적(audit trails)이 답을 내놓지 못하는 문제입니다. 운영자가 불리한 기록을 _수정_하는 것이 아니라, 단순히 기록을 아예 작성하지 않거나 규제 기관에 실제로 실행된 로그와는 다른 로그를 보여주는 것입니다. 이것이 바로 스플릿 뷰 (split-view) 문제입니다. 해시 체인(hash chain)만으로는 이를 막을 수 없는데, 선별된 하위 집합(subset)에 대해 구성된 체인은 내부적으로는 일관성을 유지하기 때문입니다.

해결책은 배치(batch)의 _완전성 (completeness)_을 확정하는 것입니다. 이벤트 해시들에 대해 머클 루트 (Merkle root)를 계산하고 해당 루트를 게시합니다. 나중에 누구라도 당신이 보여준 이벤트들로부터 루트를 재계산할 수 있으며, 만약 이 값이 당신이 확정했던 값과 일치하지 않는다면 어떤 이벤트가 누락되었거나 교체되었음을 의미합니다. 다음은 RFC 6962 스타일의 머클 트리 (Merkle tree) 구현입니다 (리프 노드(leaf)가 내부 노드(internal node)로 오인되지 않도록 도메인 분리 태그(domain-separation tags)를 사용함):

def _leaf(data: bytes) -> bytes:
    return hashlib.sha256(b"\x00" + data).digest()   # 리프 접두사 (leaf prefix)

...

배치를 확정(commit)한 다음, 특정 이벤트가 해당 배치에 속함을 증명합니다:

hashes = [e["security"]["event_hash"] for e in log.events]
root = merkle_root(hashes)
print("committed root:", root[:24], "...")
...
committed root: 7d1f0a9c4b2e8835ac6e3f10 …
ORD in committed set: True
root after dropping ORD matches original: False

루트가 게시되면 해당 집합은 동결됩니다. 순서를 누락시키거나, 교체하거나, 조작된 이벤트를 삽입하는 모든 행위는 서로 다른 루트를 생성하게 됩니다.

속성 4: 외부 앵커링 (external anchoring)

Merkle root (머클 루트)의 신뢰성은 그것을 어디에 게시하느냐에 달려 있습니다. 만약 본인의 서버에 보관한다면, 나중에 언제든 내용을 다시 쓸 수 있습니다. **앵커링 (anchoring)**의 핵심은 해당 약속(commitment)을 본인이 제어할 수 없고 몰래 변경할 수 없는 곳에 두는 것입니다. 예를 들어, 추가만 가능한 (append-only) 투명성 로그 (Certificate Transparency가 TLS 인증서에 사용하는 것과 동일한 메커니즘), SCITT 투명성 서비스, 또는 퍼블릭 블록체인 등이 있습니다. 이를 통해 사후에 위조할 수 없는 포함 증명서 (inclusion receipt)와 타임스탬프 (timestamp)를 돌려받게 됩니다.

코드는 단지 인터페이스일 뿐이며, 보안은 로그가 본인이 아닌 타인에 의해 운영된다는 점에서 발생합니다:

def anchor(merkle_root_hex: str) -> dict:
    """운영 환경: 독립적인 제3자가 운영하는
    추가 전용 투명성 로그(RFC 6962 / SCITT / 퍼블릭 체인)에 제출
...```

이것이 전체 스택입니다. **해시 체인 (hash chain)** (속성 1)은 각 기록의 변조 여부를 확인할 수 있게 하며, **Ed25519 서명 (Ed25519 signatures)** (속성 2)은 외부인이 검증할 수 있게 합니다. **머클 약속 (Merkle commitments)** (속성 3)은 누락을 감지할 수 있게 하며, **앵커링 (anchoring)** (속성 4)은 해당 약속을 운영자의 손이 닿지 않는 곳에 둡니다. 약 150줄의 코드이며, 모두 공개된 프리미티브 (primitives)를 사용합니다.

## 이것이 제공하지 않는 것

이 부분은 솔직하게 말씀드려야 합니다. 과도한 약속은 신뢰 인프라가 신뢰를 잃게 만드는 원인이기 때문입니다.

비행 기록 장치(flight recorder)가 비행기를 조종하는 것도 아니고, 추락을 막아주는 것도 아닙니다. 이 로그가 Meta의 인수 합병을 막지는 못했을 것입니다. 이것은 실시간 안전 시스템(real-time safety system)이 아니며, 잘못된 결정을 차단하지도 않고, 결정이 _옳았는지_를 판단하지도 않습니다. 이것은 특정 모델 버전 하에서, 특정 입력값에 대해, 특정 시점에 알고리즘이 명령을 거부했다는 사실을 증명할 수는 있지만, 그것이 공정했는지 혹은 적법했는지는 인간과 규제 기관이 판단해야 할 문제입니다. 또한, 이것이 어떤 시스템을 자동으로 "준수(compliant)" 상태로 만들어주는 것도 아닙니다. 블랙박스가 항공사를 항공법에 따라 준수하게 만들어주지 않는 것과 마찬가지로, 어떤 로깅 표준도 EU AI Act나 MiFID II를 준수하게 만들어주지는 않습니다. 이 시스템이 하는 일은 그러한 규제 체계들이 점점 더 요구하고 있는 **증거를 생성(generate the evidence)**하는 것입니다. 즉, 감사인이 요청하는 기록, 사고 검토(incident review) 시 재구성하는 흔적, 분쟁의 근거가 되는 증거를 만들어내는 것입니다.

이는 좁은 범위의 주장인 동시에 강력한 주장입니다. 실패를 막아주지는 못하지만, 당신의 말을 그대로 믿을 필요가 없는 사람에게 실제로 무슨 일이 일어났는지 증명할 수 있게 해줍니다.

데모에서 생략된 몇 가지 실무적인 참고 사항이 있습니다. 저희는 기록 시점에 계정 ID를 가명 처리(pseudonymized)했습니다. 진정한 프라이버시를 위해서는 **크립토 슈레딩(crypto-shredding)**도 필요합니다. 즉, 대상별로 민감한 필드를 암호화하고 해당 키를 삭제하여 복구가 불가능하게 만드는 방식입니다. 이를 통해 해시 체인(hash chain)을 깨뜨리지 않고도 GDPR의 삭제 요청(erasure request)을 이행할 수 있습니다(해시는 암호문(ciphertext)에 대해 수행되므로 암호문은 그대로 유지됩니다). 그리고 VCP에서 볼 수 있는 준수 "티어(tiers)"(Silver/Gold/Platinum)는 별개의 프로토콜이 아니라 실제로는 _검증 깊이(verification depth)_를 의미합니다. 즉, 시계 동기화(clock-sync)의 정밀도, 머클 증명(Merkle proofs) 및 외부 앵커링(external anchoring)의 필수 여부 등을 나타냅니다.

## 다음 단계

전체 이벤트 모델(event model), JSON 스키마(JSON Schemas), 그리고 적합성 테스트(conformance tests)는 [github.com/veritaschain/vcp-spec](https://github.com/veritaschain/vcp-spec)의 공개 사양(open spec)에 포함되어 있습니다. 이 접근 방식은 COSE 서명된 성명서(COSE-signed statements) 및 영수증(receipts)에 관한 IETF의 SCITT(Supply Chain Integrity, Transparency and Trust) 작업과 궤를 같이합니다. 다만, 정확히 말하자면 VCP 관련 IETF 제출물은 현재 단계에서 채택된 표준이 아닌 개별 인터넷 초안(Internet-Drafts)이며, 이 프로토콜은 아직 기관의 운영 환경(production)에 배포되지 않았습니다.

만약 여러분이 거래, 승인, 에이전트 동작과 같이 중대한 자동화된 결정(automated decisions)을 내리는 시스템을 운영하고 있다면, 시도해 볼 가치가 있는 간단한 연습이 있습니다. 바로 오늘 여러분의 코드가 내리는 결정 중 하나를 선택한 뒤, 외부인에게 그 결정이 무엇이었는지, 그리고 그 기록이 온전하다는 것을 _증명(prove)_할 수 있는지 자문해 보는 것입니다. 만약 답변이 "우리 로그를 믿으세요"라면, 여러분은 블랙박스(black box)가 아닌 단순한 로깅 시스템(logging system)을 가지고 있는 것입니다. 이 두 가지 사이의 간극을 메우는 것이 바로 핵심입니다.

_공개 사항: 저는 VeritasChain Standards Organization에서 VCP 및 더 넓은 범위의 검증 가능한 AI 출처(Verifiable AI Provenance, VAP) 프레임워크를 포함하여, 검증 가능한 AI 결정 출처(verifiable AI decision provenance)를 위한 오픈 표준 작업을 수행하고 있습니다. 여기에 제시된 코드는 교육용으로 축약된 버전입니다. VCP의 이벤트 모델(event model)을 충실히 따르되 명확성을 위해 다듬었습니다. 수정 사항이나 풀 리퀘스트(pull requests)는 언제나 환영합니다._

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0