수업 영상 보기: 암호화 영수증으로 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 정규화 방식(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를 반환합니다. 네트워크 호출, 서비스 의존성, 제삼자 신뢰가 필요 없습니다.
변조 감지 실습에서는 다음을 진행합니다:
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에 참조된 정책이 실제로 평가되었는지, 또는 이 행동이 허용되었는지. 영수증은 주장된 내용을 기록할 뿐 강제된 내용을 기록하지 않는다.이 경계가 중요한 이유 두 가지:
흔한 오류는 “영수증이 있으니 우리가 관리되고 있다”고 가정하는 것이다. 그렇지 않다. 영수증은 기초이며 관리 체계는 그 위에 구축하는 시스템이다.
위 3번 항목은 별도 섹션이 필요하다: 행동 영수증은 “이 키가 이 내용을 서명했다”고 말할 뿐 “인간이 승인했다”고는 절대 말하지 않는다. 고위험 행동(환불, 삭제, 송금)의 경우 거버넌스 프레임워크는 점점 더 그 누락된 진술을 요구하며, 이 수업에서 이미 만든 원시 함수로도 생성 가능하다.
후속 노트북 code_samples/human-authorization-receipts.ipynb은 수업 영수증과 같은 봉투 형태(정규 SHA-256에 Ed25519 서명된 유형화된 페이로드, 서명 객체는 서명 바이트 바깥)에 두 번째 영수증 종류 human.approval.v1을 추가합니다. 명명된 승인자가 전체 정규화된 행동과 그 다이제스트를 실행 전에 서명하며, 에이전트의 행동 영수증은 같은 행동 다이제스트와 함께 parent_approval_ref, 즉 승인 영수증의 receipt_hash를 가지고 있는데, 이는 위에서 만든 체인의 previous_receipt_hash와 동일한 관례입니다. 하나의 verify_chain 함수가 두 아티팩트를 별개의 고정 키 레지스트리(승인자 키 대 에이전트 키) 하에 검증하므로 코드는 공유하지만 권한자는 절대 공유하지 않습니다.
이 속성의 정리된 표현: 인간이 이 정확한 행동을 승인했고, 에이전트는 정확히 그 승인된 행동을 실행했다. 노트북의 거부 테스트가 이 속성을 명시가 아닌 실체로 만듭니다:
각 실패에는 별개의 거부 사유가 있어 감사인이 권한 만료인지 실행된 행동 변경인지 알 수 있습니다. 노트북에서 가르치는 규칙: 서명된 승인은 단독으로 권한이 아니다. 실행 시 두 영수증 모두 같은 정규화 행동에 묶여야 권한이 존재한다. 이 수업과 동일한 인터넷 초안(draft-farley-acta-signed-receipts)의 공동 서명 경로가 이 패턴의 표준 트랙 형태입니다.
이 수업의 Python 코드는 매우 간단하여 모든 줄을 읽고 정확히 어떤 일이 일어나는지 이해할 수 있습니다. 실제 환경에서는 두 가지 선택지가 있습니다:
암호화 원시 함수를 직접 사용하여 구축. 위의 50줄 코드는 많은 사용 사례에 충분합니다. PyNaCl(Ed25519)과 jcs 패키지(정규 JSON)는 잘 관리되고 감사받은 라이브러리입니다.
상용 영수증 라이브러리 사용. 몇몇 오픈소스 프로젝트는 동일한 패턴을 추가 기능(키 회전, 일괄 검증, JWK 세트 배포, 정책 엔진 통합)과 함께 구현합니다:
draft-farley-acta-signed-receipts, 2차 개정)를 따르며, 독립 구현체들이 바이트 단위로 동일한 정규화 출력을 교차 검증하는 공유 적합성 테스트 스위트(agent-governance-testvectors)와 함께 제공됩니다.protect-mcp(npm)와 @veritasacta/verify(npm) 패키지는 영수증 서명과 오프라인 검증의 Node 구현체를 제공합니다. 이는 MCP 서버에 변조 감지 감사 추적을 감싸기 위한 것으로, 중단된 행동이 WebAuthn을 사용하는 데스크톱 플로우 속도의 승인 영수증을 방출하는 공동 서명 플로우를 포함합니다. 이는 위 인간 승인 노트북과 동일한 승인 영수증 패턴입니다.pip install nobulex)는 LangChain과 CrewAI 통합을 포함한 동일한 Ed25519 + JCS 서명 패턴을 제공하며, 공개 검증 테스트 벡터와 OWASP PR #2210를 통한 준수 매핑도 포함합니다.자체 구축과 라이브러리 사용 간 결정은 검증된 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 해시를 함께 계산(정규화 바이트를 결정론적으로 연결)하여 세 번째 영수증에 새로운 필드로 삽입한 다음 서명하세요. 세 영수증 모두가 여전히 검증-복원 가능한지 확인하세요. 이렇게 하면 1단계 포함 증명을 만든 셈입니다: 세 번째 영수증 소지자는 첫 번째 두 영수증이 서명 시점에 존재했다는 것을 내용 공개 없이 증명할 수 있습니다. 이 패턴은 대규모 선택적 공개 영수증에 사용됩니다(Merkle 커밋, RFC 6962).
암호화된 영수증은 AI 에이전트에게 다음과 같은 감사 추적을 제공합니다:
이것은 입력 검증, 정책 집행, 신원 인프라를 대체하지 않습니다. 그 기반을 마련하는 역할을 합니다. 규제된 업무, 다중 조직 워크플로, 미래의 감사자가 신뢰할 수 없는 환경에 에이전트를 배포할 때 영수증이 감사 추적을 정직하게 만드는 방법입니다.
가장 중요한 요점: 영수증은 누가 언제 무엇을 말했는지 증명하지만, 말한 내용이 진실이거나 옳았다는 것을 증명하지는 않습니다. 이 차이를 명확히 인식해야 합니다. 정직한 출처 시스템과 오해를 불러일으키는 시스템의 차이입니다.
이 수업을 졸업하고 실제 환경에 영수증 서명 에이전트를 배포할 준비가 되면:
https://your-org.example.com/.well-known/agent-keys.json 입니다.Microsoft Foundry Discord에서 다른 학습자들과 만나고, 오피스 아워에 참석하며, AI 에이전트 관련 질문을 하세요.
이 수업은 단일 영수증 서명과 해시 체인 시퀀스를 다룹니다. 동일한 프리미티브는 거버넌스 태세가 성숙해질 때 만날 수 있는 여러 고급 패턴으로 구성됩니다:
authorization_*)과 실행 후(result_*) 절반으로 나누어 각각 독립적으로 서명합니다. 권한 결정과 관찰된 결과가 서로 다른 행위자나 시점에 생성될 때 유용하며, 이번 수업에서 배운 영수증 형식 위에 추가할 수 있습니다.result_hash에 넣는 바이트는 무엇이든 봉인할 수 있습니다. 실제 페이로드는 단일 도구 호출 결과보다 더 풍부한 경우가 많습니다: 의사결정 전 추론(모델 예측, 고려 옵션, 증거 및 완전성, 위험 상태, 책임 사슬, 게이트 결과)도 포함되어 단일 영수증으로 봉인할 수 있습니다. 이는 영수증 형식을 최소화하면서 도메인별 페이로드 스키마 진화를 허용합니다.signature.alg 필드에 필요 시 NIST 포스트 양자 서명 표준인 ML-DSA-65를 담을 수 있습니다. 마이그레이션 기간 동안 영수증은 이중 서명될 계획을 세우세요.면책 조항: 이 문서는 AI 번역 서비스 Co-op Translator를 사용하여 번역되었습니다. 정확성을 기하기 위해 노력하고 있으나, 자동 번역은 오류나 부정확한 부분이 있을 수 있음을 유의하시기 바랍니다. 원본 문서의 원어본이 권위 있는 자료로 간주되어야 합니다. 중요한 정보의 경우, 전문가의 인간 번역을 권장합니다. 이 번역 사용으로 인해 발생하는 오해나 잘못된 해석에 대해 당사는 책임을 지지 않습니다.