수업 영상 보기: 암호화된 영수증으로 AI 에이전트 보안 강화하기
(수업 영상 및 썸네일은 병합 후 Microsoft 콘텐츠 팀에서 추가할 예정이며, 14/15강 패턴에 맞추어집니다.)
이 수업에서 다룰 내용:
이 수업을 완료하면 다음을 알 수 있습니다:
Contoso Travel용 AI 에이전트를 배포했다고 상상해 보세요. 이 에이전트는 고객 요청을 읽고, 항공편 API를 호출하여 옵션을 조회하고, 고객을 대신해 좌석을 예약합니다. 지난 분기에 에이전트는 50,000건의 예약을 처리했습니다.
오늘, 감사인이 찾아옵니다. 그들은 간단한 질문을 합니다: “당신의 에이전트가 한 일을 보여 주세요.”
당신은 로그 파일을 건네줍니다. 감사인은 로그를 보고 더 어려운 질문을 합니다: “이 로그가 편집되지 않았다는 걸 어떻게 알죠?”
이것이 감사 추적 문제입니다. 오늘날 대부분의 에이전트 배포는 다음에 의존합니다:
이들은 모두 감사인이 누군가(당신, 클라우드 제공자, 데이터베이스 공급자)를 신뢰하지 않고는 질문에 답할 수 없습니다. 내부 용도로는 흔히 신뢰가 가능하지만, 규제 대상 작업(금융, 의료, EU AI 법령 적용 대상 등)에는 적합하지 않습니다.
암호화된 영수증은 각 에이전트 동작을 독립적으로 검증 가능하도록 하여 이 문제를 해결합니다. 감사인은 당신을 신뢰할 필요 없이 공개 키와 영수증만 있으면 됩니다.
영수증은 에이전트가 수행한 작업을 기록한 JSON 객체로, 디지털 서명으로 서명되어 있습니다.
flowchart LR
A[에이전트가 도구를 호출함] --> B[영수증 페이로드 생성]
B --> C[JSON RFC 8785 정규화]
C --> D[SHA-256 해시]
D --> E[Ed25519 서명]
E --> F[서명된 영수증]
F --> G[감사자가 오프라인 검증]
G --> H{서명 유효한가?}
H -- yes --> I[변조 방지 증거]
H -- no --> J[영수증 거부됨]
간단한 영수증은 다음과 같습니다:
{
"type": "agent.tool_call.v1",
"agent_id": "contoso-travel-bot",
"tool_name": "lookup_flights",
"tool_args_hash": "sha256:a3f9c1...",
"result_hash": "sha256:7b2e1d...",
"policy_id": "contoso-travel-policy-v3",
"timestamp": "2026-04-25T14:30:00Z",
"sequence": 47,
"previous_receipt_hash": "sha256:9d4e6a...",
"signature": {
"alg": "EdDSA",
"sig": "c5af83...",
"public_key": "8f3b2c..."
}
}
세 가지 속성이 핵심 역할을 합니다:
서명: 영수증은 에이전트 게이트웨이가 Ed25519 비밀 키로 서명합니다. 대응하는 공개 키를 가진 누구나 오프라인에서 서명을 검증할 수 있습니다. 어떤 필드라도 변조하면 서명이 무효가 됩니다.
정규 인코딩: 서명 전에 영수증은 JSON Canonicalization Scheme (JCS, RFC 8785)으로 직렬화됩니다. 이는 두 구현체가 동일한 논리적 영수증을 생성하면 바이트가 완전히 동일하도록 보장합니다. 정규화 없이는 서로 다른 JSON 직렬화기가 같은 내용에 대해 서로 다른 서명을 만듭니다.
해시 체이닝: previous_receipt_hash 필드는 각 영수증을 이전 영수증에 연결합니다. 하나의 영수증을 제거하거나 순서를 바꾸면 이후 모든 영수증이 손상됩니다. 서명이 우회되어도 체인 수준에서 변조가 보입니다.
이 세 속성은 다음 세 가지 보장을 제공합니다:
영수증 생성에 특수 라이브러리는 필요 없습니다. 암호학적 원시 기능은 널리 제공되며, 로직도 수십 줄의 Python 코드입니다.
code_samples/18-signed-receipts.ipynb 노트북의 실습에서는 전체 과정을 단계별로 안내합니다. 요약 버전:
import json
import hashlib
import base64
from nacl import signing
from jcs import canonicalize # RFC 8785 표준 JSON
def b64url_nopad(data: bytes) -> str:
return base64.urlsafe_b64encode(data).decode("ascii").rstrip("=")
def sha256_canonical(obj) -> str:
"""SHA-256 of a Python object's JCS-canonical JSON form."""
return f"sha256:{hashlib.sha256(canonicalize(obj)).hexdigest()}"
# 서명 키 생성 또는 로드 (운영 환경에서는 키 볼트에 저장)
signing_key = signing.SigningKey.generate()
verify_key = signing_key.verify_key
# 영수증 페이로드 구성 (아직 서명 없음)
tool_args = {"origin": "SYD", "destination": "LAX"}
tool_result = [{"flight": "QF11", "price": 1850, "stops": 0}]
payload = {
"type": "agent.tool_call.v1",
"agent_id": "contoso-travel-bot",
"tool_name": "lookup_flights",
"tool_args_hash": sha256_canonical(tool_args),
"result_hash": sha256_canonical(tool_result),
"policy_id": "contoso-travel-policy-v3",
"timestamp": "2026-04-25T14:30:00Z",
"sequence": 0,
"previous_receipt_hash": None,
}
# 정규화, 해시, 서명.
canonical_bytes = canonicalize(payload)
message_hash = hashlib.sha256(canonical_bytes).digest()
signature_bytes = signing_key.sign(message_hash).signature
# 구조화된 서명 객체 첨부.
receipt = {
**payload,
"signature": {
"alg": "EdDSA",
"sig": b64url_nopad(signature_bytes),
"public_key": b64url_nopad(bytes(verify_key)),
},
}
이것이 서명 파이프라인의 전부입니다. 노트북 실습이 각 단계를 상세히 다룹니다.
검증은 반대 과정입니다:
import base64
import hashlib
from nacl import signing
from nacl.exceptions import BadSignatureError
from jcs import canonicalize
def b64url_decode(s: str) -> bytes:
padding = "=" * ((4 - len(s) % 4) % 4)
return base64.urlsafe_b64decode(s + padding)
def verify_receipt(receipt: dict) -> bool:
# 서명은 구조화된 객체입니다: {"alg", "sig", "public_key"}.
sig_obj = receipt.get("signature")
if not sig_obj or sig_obj.get("alg") != "EdDSA":
return False
# 실제로 서명된 페이로드를 재구성합니다 (서명을 제외한 모든 것).
payload = {k: v for k, v in receipt.items() if k != "signature"}
canonical_bytes = canonicalize(payload)
message_hash = hashlib.sha256(canonical_bytes).digest()
try:
verify_key = signing.VerifyKey(b64url_decode(sig_obj["public_key"]))
verify_key.verify(message_hash, b64url_decode(sig_obj["sig"]))
return True
except BadSignatureError:
return False
이 함수는 영수증을 받아 서명이 유효하면 True, 아니면 False를 반환합니다. 네트워크 호출, 서비스 의존, 제3자 신뢰가 전혀 필요 없습니다.
변조 감지 시연은 노트북에서 다음을 다룹니다:
tool_args_hash 필드의 1바이트를 수정.이것은 영수증이 변조 감지 기능을 갖췄음을 보여주는 실용적 시연입니다: 아주 작은 수정도 서명을 망가뜨립니다.
단일 서명 영수증은 한 동작을 보호합니다. 영수증 체인은 시퀀스를 보호합니다.
flowchart LR
R0[영수증 0<br/>제네시스] --> R1[영수증 1]
R1 --> R2[영수증 2]
R2 --> R3[영수증 3]
R1 -. previous_receipt_hash .-> R0
R2 -. previous_receipt_hash .-> R1
R3 -. previous_receipt_hash .-> R2
각 영수증은 이전 영수증의 해시를 기록합니다. 체인 중간의 영수증 2를 조용히 제거하려면 공격자는:
previous_receipt_hash 필드를 수정(영수증 3의 서명 무효), 또는하드웨어 키 보관소에 비밀 키가 있고 각 영수증에 공개 키를 발행하면 어느 쪽 공격도 탐지 없이 불가능합니다.
노트북은 다음을 안내합니다:
previous_receipt_hash가 실제 이전 영수증 해시와 일치하는지 검증.이것이 외부 감사인이 당신을 신뢰하지 않고도 검증 가능한 감사 추적을 만드는 방법입니다.
이 섹션은 이 수업의 가장 중요한 부분입니다. 영수증은 강력하지만 그 한계가 분명합니다.
영수증이 증명하는 세 가지:
영수증이 증명하지 않는 것:
policy_id에 참조된 정책이 실제로 평가되었거나, 평가 시 이 동작을 허용했을지 여부. 영수증은 주장된 내용을 기록할 뿐, 시행된 내용은 아님.이 경계가 중요한 두 가지 이유:
흔한 실수는 “영수증이 있으니 통제되고 있다”고 착각하는 것. 그렇지 않습니다. 영수증은 기반일 뿐, 통제 시스템은 그 위에 구축하는 것입니다.
이 수업의 Python 코드는 의도적으로 최소한으로 작성되어 정확히 무슨 일이 일어나는지 읽고 이해하기 쉽습니다. 운영 환경에서는 두 가지 옵션이 있습니다:
암호학 원시 기능을 직접 사용: 위의 50줄 코드는 많은 사용 사례에 충분합니다. PyNaCl (Ed25519)과 jcs 패키지(정규 JSON)는 잘 유지·감사된 라이브러리입니다.
운영용 영수증 라이브러리 사용: 몇몇 오픈소스 프로젝트는 추가 기능(키 교체, 배치 검증, JWK 세트 배포, 정책 엔진 통합)을 갖춘 동일 패턴 구현:
draft-farley-acta-signed-receipts)에 따름.protect-mcp(npm) 및 @veritasacta/verify(npm) 패키지는 영수증 서명과 오프라인 검증을 위한 Node 기반 구현 제공, 임의 MCP 서버를 변조 감지 감사 추적으로 포장 가능.pip install nobulex)는 LangChain 및 CrewAI 통합, 교차 검증 테스트 벡터와 OWASP PR #2210을 통한 규정 준수 매핑 포함 Ed25519 + JCS 서명 패턴 구현.자체 구현과 라이브러리 사용 결정은 직접 JWT 라이브러리 작성과 검증된 라이브러리 사용 결정과 유사: 두 가지 모두 합리적, 라이브러리는 시간 절약 및 감사지면 축소, 직접 구현은 모든 원시 기능 이해 강제. 이 수업은 직접 구현 방식을 가르쳐 두 가지 선택 모두에 기초를 마련합니다.
실습 전에 이해도를 테스트하세요.
1. 영수증은 에이전트 개인 Ed25519 키로 서명됩니다. 감사인은 공개 키만 가집니다. 감사인은 오프라인에서 영수증을 검증할 수 있나요?
2. 공격자가 영수증의 policy_id 필드를 더 관대한 정책으로 수정했습니다. 서명은 원본 페이로드에 대한 것이었습니다. 검증 시 어떻게 되나요?
3. 왜 영수증에 원본 인자와 결과 대신 tool_args_hash 및 result_hash가 포함되나요?
4. previous_receipt_hash 필드는 각 영수증을 바로 이전 영수증에 연결합니다. 공격자가 체인 중간에서 영수증 하나를 몰래 삭제하면 무엇이 무효가 되나요?
5. 영수증이 정상적으로 검증됩니다. 이것이 에이전트 동작이 올바르고 정책에 부합했다는 것을 증명할까요?
code_samples/18-signed-receipts.ipynb를 열고 네 섹션을 모두 완료하세요:
확장 과제 2: 두 개의 영수증을 SHA-256으로 해시(정식 바이트를 결정적인 순서로 연결)한 다음, 결과 요약을 서명 전에 세 번째 영수증의 새 필드에 삽입하세요. 세 개의 영수증 모두 정상적으로 순환하는지 검증하세요. 이렇게 하면 세 번째 영수증을 가진 누구나 첫 번째 두 영수증이 서명 시점에 존재했음을 내용 공개 없이 증명할 수 있는 한 단계 포함 증명을 구축한 셈입니다. 이것이 대규모 선택적 공개 영수증에서 사용하는 패턴입니다(머클 커밋먼트, RFC 6962).
암호화 영수증은 AI 에이전트에 다음과 같은 감사 추적을 제공합니다:
입력 검증, 정책 집행, 신원 인프라를 대체하지는 않습니다. 이들은 해당 레이어를 위한 기초입니다. 규제된 작업, 다중 조직 워크플로우, 미래의 감사자가 신뢰할 수 없을 상황에 에이전트를 배포할 때, 영수증이 감사 추적을 정직하게 만드는 방법입니다.
가장 중요한 요점: 영수증은 누가 언제 무엇을 말했는지를 증명합니다. 말한 내용이 진실이거나 옳다는 것을 증명하지는 않습니다. 이 차이를 명확히 유지하세요. 이는 정직한 출처 시스템과 오도하는 시스템의 차이입니다.
이 수업을 마치고 실제 환경에 영수증 서명 에이전트를 배포할 준비가 되면:
https://your-org.example.com/.well-known/agent-keys.json.Microsoft Foundry Discord에 참여해 다른 학습자와 만나고, 오피스 아워에 참석하고, AI 에이전트 질문에 답변을 받아보세요.
이 수업은 단일 영수증 서명과 해시 체인 시퀀스를 다룹니다. 동일한 기본 원칙으로 더 고급 패턴을 구성할 수 있으며, 거버넌스 체계가 성숙해질수록 접하게 될 것입니다:
authorization_*)과 실행 후(result_*) 절반으로 나누어 독립 서명을 수행합니다. 승인 결정과 관찰된 결과가 다른 주체 또는 시간에 발생할 때 유용합니다. 이 패턴은 본 수업에서 배운 영수증 포맷 위에 추가로 적용됩니다.result_hash에 넣은 바이트를 봉인합니다. 실제 페이로드는 도구 호출 결과 하나보다 더 풍부할 수 있습니다: 사전 결정 추론(모델 예측, 고려 옵션, 증거 및 그 완전성, 위험 태세, 책임 사슬, 게이트 결과) 등도 페이로드에 포함되어 단일 영수증으로 봉인할 수 있습니다. 이렇게 하면 영수증 포맷은 최소화하면서 도메인별 페이로드 스키마를 진화시킬 수 있습니다.signature.alg 필드에 필요 시 ML-DSA-65(NIST 포스트 양자 서명 표준)를 넣을 수 있습니다. 전환 기간 동안 영수증이 이중 서명되는 것을 계획하세요.(커리큘럼 관리자가 결정 예정)
면책 조항: 이 문서는 AI 번역 서비스 Co-op Translator를 사용하여 번역되었습니다. 정확성을 기하기 위해 노력하고 있으나, 자동 번역은 오류나 부정확한 부분이 있을 수 있음을 유의하시기 바랍니다. 원본 문서의 원어본이 권위 있는 자료로 간주되어야 합니다. 중요한 정보의 경우, 전문가의 인간 번역을 권장합니다. 이 번역 사용으로 인해 발생하는 오해나 잘못된 해석에 대해 당사는 책임을 지지 않습니다.