Watch the lesson video: Securing AI Agents with Cryptographic Receipts
(Lesson video and thumbnail to be added by the Microsoft content team post-merge, matching the lesson 14 / 15 pattern.)
이 강의에서는 다음 내용을 다룹니다:
이 강의를 완료하면 다음을 알게 됩니다:
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 필드의 한 바이트를 수정하기이것이 영수증이 변조 감지 기능을 가진 실질적 증거입니다: 아주 작은 변경이라도 서명을 깨뜨립니다.
단일 서명 영수증은 하나의 동작을 보호합니다. 영수증 체인은 시퀀스를 보호합니다.
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) 패키지는 tamper-evident 감사 추적으로 MCP 서버를 래핑하기 위한 노드 기반 영수증 서명 및 오프라인 검증 구현체입니다.직접 구현할지 라이브러리를 사용할지는 JWT 라이브러리 선택과 유사합니다: 둘 다 합리적; 라이브러리가 시간 절약 및 감사 표면 감소; 직접 구현은 모든 기본 요소를 이해하게 됨. 이 강의는 직접 구현 방식을 가르쳐 어느 쪽 선택에도 기초를 제공합니다.
실습 전에 이해도를 테스트하세요.
1. 영수증은 에이전트의 Ed25519 비공개 키로 서명됩니다. 감사자는 공개 키만 갖고 있습니다. 감사자가 오프라인으로 영수증을 검증할 수 있나요?
2. 공격자가 영수증의 policy_id 필드를 더 관대한 정책을 적용한 것처럼 수정했습니다. 서명은 원본 페이로드에 대해 됐습니다. 검증 과정에서 무슨 일이 발생하나요?
3. 왜 영수증은 원시 인수나 결과 대신 tool_args_hash와 result_hash를 포함하나요?
4. previous_receipt_hash 필드는 각 영수증을 이전 영수증과 연결합니다. 공격자가 체인 중간에서 한 영수증을 조용히 삭제하면 무엇이 무효가 되나요?
5. 영수증이 정상 검증됐습니다. 이로써 에이전트 동작의 정확성, 건전성 또는 정책 준수가 증명되나요?
code_samples/18-signed-receipts.ipynb를 열어 네 섹션을 모두 완료하세요:
도전 과제 1: 영수증 스키마에 임의의 추가 필드(예: 추적용 요청 ID)를 넣고 표준 서명 로직을 갱신 후, 영수증이 검증 과정에서 정상적으로 왕복하는지 확인하세요. 그 후 서명 후 필드를 수정해 검증 실패를 확인하세요. 이것은 표준 인코딩의 모든 바이트가 서명에 어떻게 기여하는지 이해하게 합니다. 도전 과제 2: 두 개의 영수증을 SHA-256으로 해시합니다(결정론적 순서로 정규화된 바이트를 연결). 그리고 그 결과 다이제스트를 세 번째 영수증의 새로운 필드로 삽입한 뒤 서명하세요. 세 개의 영수증 모두가 여전히 올바르게 왕복 검증되는지 확인합니다. 이렇게 하면 단일 단계 포함 증명을 구축한 것입니다: 세 번째 영수증을 가진 누구나 그 서명 시점에 첫 두 영수증이 존재했다는 것을, 내용 공개 없이 증명할 수 있습니다. 이것은 선택적 공개 영수증이 대규모로 사용하는 패턴입니다(머클 커밋먼트, RFC 6962).
암호화 영수증은 AI 에이전트에게 다음과 같은 감사 추적을 제공합니다:
영수증은 입력 검증, 정책 실행, 또는 신원 인프라를 대체하지 않습니다. 그것들은 그런 계층들을 위한 기반입니다. 규제된 작업, 다기관 워크플로우, 또는 미래의 감사자가 신뢰할 수 없는 상황에 에이전트를 배포할 때, 영수증은 감사 추적을 정직하게 만드는 방법입니다.
가장 중요한 핵심은: 영수증은 누가 언제 무엇을 말했는지를 증명합니다. 영수증은 말한 내용이 진실이거나 옳았음을 증명하지 않습니다. 이 차이를 확실히 인식하세요. 정직한 출처 시스템과 오해를 불러일으키는 시스템의 차이입니다.
이 교육과정을 졸업하고 실제 환경에 영수증 서명 에이전트를 배포할 준비가 되면:
https://your-org.example.com/.well-known/agent-keys.json.다른 학습자들과 만나고, 오피스 아워에 참석하며 AI 에이전트 관련 질문에 답변을 받으려면 Microsoft Foundry Discord에 참여하세요.
이 수업에서는 단일 영수증 서명과 해시로 체인된 시퀀스를 다루었습니다. 동일한 원시 알고리즘이 성숙한 거버넌스 태세에서 마주칠 수 있는 여러 고급 패턴으로 구성됩니다:
authorization_*)과 실행 후(result_*)의 두 절반으로 나누어 독립 서명하며, 권한 부여 결정과 관찰된 결과가 다른 행위자나 다른 시점에 생산될 때 유용합니다. 이는 본 수업에서 다룬 영수증 포맷 위에 추가적으로 구성됩니다.result_hash에 넣는 어떤 바이트든 봉인합니다. 실제 페이로드는 단일 도구 호출 결과보다 풍부하며: 결정 전 추론(모델 예측, 고려 옵션, 증거 및 완전성, 위험 태도, 책임 체인, 게이트 결과)이 단일 영수증으로 봉인될 수 있습니다. 이는 영수증 포맷은 최소한으로 유지하면서 페이로드 스키마는 영역별로 진화할 수 있게 합니다.signature.alg 필드에 필요 시 ML-DSA-65 (NIST 후양자 서명 표준)를 담을 수 있습니다. 영수증이 이중 서명되는 전환 기간을 계획하세요.(커리큘럼 유지관리자가 결정 예정)
면책 조항: 이 문서는 AI 번역 서비스 Co-op Translator를 사용하여 번역되었습니다. 정확성을 기하기 위해 노력하고 있으나, 자동 번역은 오류나 부정확한 부분이 있을 수 있음을 유의하시기 바랍니다. 원본 문서의 원어본이 권위 있는 자료로 간주되어야 합니다. 중요한 정보의 경우, 전문가의 인간 번역을 권장합니다. 이 번역 사용으로 인해 발생하는 오해나 잘못된 해석에 대해 당사는 책임을 지지 않습니다.