터널이 아닌 메시지에 서명하세요: AI 에이전트를 위한 N-AALP 소개
요약
N-AALP는 AI 에이전트의 보안을 연결(connection) 중심에서 메시지(message) 중심으로 전환하는 애플리케이션 계층 객체 프로토콜입니다. 메시지 자체에 신원, 권한, 승인, 감사 정보를 포함하여 전송 계층과 무관하게 오프라인에서도 검증 가능한 보안 모델을 제안합니다.
핵심 포인트
- 기존 TLS/mTLS 기반 보안의 한계인 메시지 전달 시 증명력 상실 문제 해결
- 메시지 자체가 신원, 권한, 승인, 감사 정보를 포함하는 단일 객체 구조
- 결정론적 CBOR을 사용하여 전송 계층에 상관없이 일관된 서명 검증 가능
- 인프라가 아닌 메시지 자체를 보안 및 거버넌스의 기본 단위로 설정
오늘날의 에이전트 보안은 연결(connection)로부터 상속됩니다. N-AALP는 메시지 자체가 신원(identity), 권한 부여(authorization), 승인(approval) 및 감사(audit) 정보를 포함하도록 하며, 어떤 전송 계층(transport)에서도 오프라인으로 검증 가능하게 만듭니다.
당신의 에이전트가 방금 운영 환경의 테이블을 삭제했습니다. 감사 로그(audit log)에는 해당 요청이 승인되었다고 기록되어 있습니다.
이제 그것을 증명해 보세요. 단순히 "로그 라인을 보여달라"는 식이 아니라, 당신의 로그, 게이트웨이(gateway), 또는 데이터베이스를 신뢰하지 않는 누군가에게 직접 증명해야 합니다. 어떤 키가 승인했습니까? 승인된 인자가 정확히 그 내용이었습니까, 아니면 승인 후에 무언가가 이를 다시 작성했습니까? 그 승인이 이미 한 번 사용된 것은 아닙니까? 만약 이 질문들에 대한 답변이 "음, 우리 게이트웨이가 그걸 체크합니다"로 시작한다면, 그 증거는 메시지가 아닌 인프라(infrastructure)에 존재하는 것입니다. 메시지를 다른 곳에서 재생(replay)하면 증거는 사라집니다.
이것이 제가 연구해 온 간극입니다. 이를 명확하게 표현할 이름이 있습니다: 오늘날의 에이전트 보안은 연결(connection)로부터 상속됩니다. TLS는 터널(tunnel)이 비공개였다는 것을 알려줍니다. mTLS는 어떤 서비스가 터널을 열었는지 알려줍니다. 베어러 토큰(bearer token)은 누군가가 자격 증명(credential)을 가지고 있었다는 것을 알려줍니다. 하지만 이 중 어느 것도 메시지가 큐(queue)에 기록되거나, 릴레이(relay)에 의해 전달되거나, 로그에 남거나, 재생되거나, 두 번째 에이전트에게 전달되는 순간에는 유지되지 않습니다. 메시지가 도착했던 연결을 벗어나는 순간, 그것은 증명 가능한 출처가 없는 단순한 바이트(bytes)일 뿐입니다.
N-AALP는 이를 해결하기 위한 저의 시도입니다. 이는 연결이 아닌 메시지가 보안 및 거버넌스(governance)의 단위가 되는 애플리케이션 계층 객체 프로토콜(application-layer object protocol)입니다.
더 읽기 전에 솔직히 밝힙니다: 제가 작성했습니다. 저는 유일한 편집자이자 유지 관리자이며, 이는 draft-bubblefish-naalp-00인 독립 제출물(Independent Submission)이고, IETF 워킹 그룹(working-group)의 합의를 주장하지 않습니다. 아래 내용에 대해 제 말을 믿기보다는, 사양(spec)을 읽고 제가 틀린 부분을 말씀해 주시기를 바랍니다. 마지막 부분에는 이 프로토콜이 수행하지 않는 기능들을 나열한 섹션이 있습니다.
단일 객체(one-object) 개념
모든 N-AALP 메시지는 하나의 서명된 객체입니다. 요청 유형(request type)도 아니고, 봉투와 페이로드(envelope-plus-payload)의 결합도 아니며, 본문 규약이 포함된 헤더 형식도 아닙니다. 하나의 구조, 하나의 서명, 하나의 신원 모델(identity model), 하나의 권한 모델(authorization model), 하나의 감사 모델(audit model)을 가지며, 모든 채널과 모든 전송 계층(transport)은 변형 없이 이를 재사용합니다.
나머지 개념들이 이 두 가지에 의존하므로, 두 가지 명확한 정의를 먼저 살펴보겠습니다.
CBOR은 구조화된 데이터를 위한 이진 형식(binary format)으로, JSON과 유사하지만 더 작고 이진(binary) 형태입니다. 결정론적 CBOR(Deterministic CBOR)이란 동일한 논리적 데이터가 항상 정확히 동일한 바이트를 생성함을 의미합니다. 즉, 키(key)는 고정된 방식으로 정렬되고, 숫자는 가능한 가장 짧은 방식으로 인코딩되며, 모호함이 없습니다. 이는 서명이 바이트(bytes)를 대상으로 이루어지기 때문에 매우 중요합니다. 만약 두 구현체가 동일한 객체를 서로 다르게 인코딩한다면, 한쪽에서 생성한 서명은 다른 쪽에서 검증되지 않을 것입니다.
COSE는 CBOR에 서명하는 표준 방식으로, JWS가 JSON에 서명하는 방식과 같습니다. COSE_Sign1은 그중 단일 서명자(single-signer) 형태입니다.
spec/naalp-draft-00.cddl의 와이어 권한(wire authority)에 정의된 실제 서명 구조는 다음과 같습니다:
naalp-object = {
1 : bstr, ; id - 이 객체의 콘텐츠 ID
2 : uint, ; kind - 이 객체의 종류
...
맵(map) 전체가 서명된 페이로드(payload)입니다. 어떤 필드라도 변경하면 서명이 깨집니다.
두 개의 필드가 이례적인 역할을 수행합니다.
id는 콘텐츠 ID(content id)입니다. 이는 필드 1을 제외한 객체 자체 본문의 SHA-384 해시값이며, 해시 알고리즘이 자기 기술적(self-describing)이 되도록 멀티해시(multihash)로 감싸져 있습니다. 따라서 id는 누군가가 임의로 할당하는 무작위 UUID가 아닙니다. 이는 콘텐츠의 함수입니다. 누구나 이를 다시 계산하여 확인할 수 있습니다. 이 단 하나의 선택이 나머지 기능들을 작동하게 만들며, 이후 두 번 더 이 내용을 다루겠습니다.
signer는 공개 키(public key)로부터 유도되므로, 신원(identity)은 키 자체의 함수입니다. 찾아볼 디렉토리도, 물어볼 인증 기관(certificate authority)도, 온라인 확인 절차도 없습니다. 당신은 손에 쥐고 있는 것만으로 객체를 검증합니다.
그 결과: N-AALP 객체는 오프라인에서 검증되며, N-PAMP, QUIC, WebSocket, 일반 HTTP 또는 USB 스틱을 통해 전달되었는지 여부와 관계없이 동일하게 검증됩니다.
서명 및 검증 예시
참조 구현(reference implementation)의 TypeScript 코드:
import { cose, identity, envelope } from 'naalp';
import { U, T, M } from 'naalp/cbor';
...
Rust와 함께 주요 참조 언어로 사용되는 Go에서의 동일한 구현:
var seed [mldsa65.SeedSize]byte
pk, sk := mldsa65.NewKeyFromSeed(&seed)
...
verify 함수의 마지막 실제 인자에 주목하십시오. 당신은 어떤 채널과 종류(kind)를 수용할 것인지에 대한 검증기(validator)를 전달합니다. 검증은 단순히 "서명이 유효한가"를 확인하는 것이 아니라, "이것이 내가 처리하기로 동의한 종류의 객체인가"를 확인하는 과정입니다. 모든 실패는 명명된 에러와 함께 차단(fail closed)됩니다: ContentIdMismatch, HeaderBodyMismatch, UnknownCriticalExt, NonCanonical, UnknownKind.
서명 알고리즘은 NIST가 FIPS 204로 발표한 양자 내성 서명 표준(post-quantum signature standard)인 ML-DSA이며, 결정론적 모드(deterministic mode)로 실행됩니다. 고전적 서명(classical signature)을 병행하고 싶다면 선택 사항으로 Ed25519 하이브리드 단계(hybrid leg)를 사용할 수 있습니다. 이것에 관심을 가져야 하는 이유는 다음 주 화요일에 양자 컴퓨터가 등장하기 때문이 아닙니다. 영수증, 승인 및 감사 체인(audit chains)은 수년간 보관해야 하는 기록이기 때문입니다. 2040년에도 여전히 유효해야 하는 서명은 미래의 기계가 소급하여 위조할 수 있는 것이어서는 안 됩니다.
이를 통해 구축할 수 있는 것들
여기서 주의를 기울이고 싶습니다. 프로토콜 관련 게시물에서 "이전에는 불가능했던"이라는 프레임을 사용하는 것을 보았는데, 이는 거의 항상 틀린 말이기 때문입니다. 서비스별로 직접 조립하고 영원히 재검토할 의사가 있다면, 이 중 거의 모든 것은 이전에도 가능했습니다. 변화하는 점은, 이것이 신뢰하고 매번 재구현해야 하는 단일 게이트웨이의 동작(behavior)이 아니라, 메시지가 이동하는 모든 곳에서 유지되는 메시지 자체의 속성(property)이 된다는 것입니다.
- 요청과 함께 이동하는 권한 부여(Authorization): 이것이 제가 가장 중요하게 생각하는 부분이며, 제가 N-AALP를 구축하여 메우고자 했던 구체적인 격차(gap)입니다.
에이전트의 행동에 레이블을 지정하는 대부분의 시스템은 해당 레이블을 의도(intent)에 대한 힌트로 취급합니다. 제가 만든 운송 프로토콜(transport protocol)인 N-PAMP도 정확히 그렇게 동작합니다. 즉, 안전 레이블을 전달하면서 해당 레이블이 의도를 설명할 뿐 권한 부여(authorization)를 대체하지 않는다고 명시적으로 밝힙니다. 이는 정직한 방식이지만, 허점이 남습니다. 아무런 검증도 수행하지 않는 힌트는 그저 장식에 불과합니다.
N-AALP에서 effect 필드는 권한 부여 입력값(authorization input)입니다. 값은 정확히 네 가지로 제한되며, 확장이 불가능한 폐쇄형(closed) 구조입니다. 각 값은 무엇을 허용하고 무엇을 거부하는지를 명시합니다. read_only는 관찰(observation)을 허용하고 모든 쓰기(write)를 거부합니다. destructive는 가장 높은 단계에 위치하며 되돌릴 수 없는 변경(irreversible change)을 허용합니다. 어떤 작업이 실행되기 전에, 엔드포인트(endpoint)는 객체의 effect를 실제로 부여된 권한(capability)과 대조하여 확인하며, effect가 권한을 초과하는 객체는 EffectNotAuthorized와 함께 거부됩니다.
그다음 가장 중요한 부분은 다음과 같습니다: 시스템이 인식하지 못하는 effect 값은 destructive로 취급됩니다. 상태를 변경하는 요청(state-mutating request)에서 값이 누락된 경우에도 destructive로 취급됩니다. 이는
인자(arguments)가 그 내용 해시(content hash)에 의해 명명되기 때문에, 어떤 인자라도 변경하면 ID가 바뀌게 되어 승인(approval)이 더 이상 일치하지 않게 됩니다. 이 경우 ApprovalMismatch가 발생합니다. "transfer 50"을 승인하고 "transfer 5000"이 실행되도록 할 수는 없습니다. 이는 검증자(validator)가 잡아냈기 때문이 아니라, 승인 내용이 산술적으로 서로 다른 바이트(bytes)에 관한 것이기 때문입니다.
Single use는 승인의 내용 ID(content id)에 대해 원자적 비교 및 설정(atomic compare-and-set)을 수행하는 해시 체인형 원장(hash-chained ledger)입니다. 먼저 추가(append)된 것이 승리합니다. 동일한 ID에 대한 두 번째 추가 시도는 AlreadyConsumed와 함께 거부됩니다.
또한, 필요하지만 아직 부여되지 않은 승인은 별도로 서명된 ApprovalHeld 객체를 생성합니다. 이는 조용한 성공도 아니고, 실패처럼 보이는 거부도 아닙니다. 실제로 대응할 수 있는 제3의 상태이며, 이것이 바로 인간 참여형(human-in-the-loop) 대기열에 필요한 것입니다.
- 당신을 신뢰하지 않고도 외부인이 확인할 수 있는 감사 추적(audit trail): 순서 권한 부여 기관(ordering authority)은 수락하는 각 객체에 대해 서명된 영수증
{ prev: , obj: , seq, at }을 추가합니다. 무엇이든 순서를 바꾸거나, 누락하거나, 대체하면prev링크가 깨지거나seq가 중복되어 검증자(verifier)가 이를 발견하게 됩니다. 권한 부여 기관은 순서를 정하기 위해 객체를 수정하지 않으므로, 원래의 서명은 유효하게 유지됩니다. 즉, 순서 정하기(ordering)는 외부 레이어(outer layer)입니다.
그 아래에는 제가 더 흥미로운 프리미티브(primitive)라고 생각하는 것이 있습니다. 모든 객체는 8번 필드에서 내용 ID(content id)를 통해 자신의 원인(causes)을 명명할 수 있습니다. 이는 서명된 부분 순서(signed partial order)입니다. "A가 B를 유발했다"라는 엣지(edge)는 A의 내용 ID에 대한 B의 서명으로 증명됩니다. 이를 확인하기 위해 권한 부여 기관이 존재할 필요는 없습니다. 누군가에게 객체 꾸러미를 건네주면, 그들은 오프라인에서 인과 그래프(causal graph)를 재구성하고 검증할 수 있습니다. 순환(cycles)이나 결과가 원인보다 앞서는 경우(causes-after-effects)는 CausalViolation으로 거부됩니다.
독립적인 감사인은 서명된 영수증만으로도 이중 말하기(equivocation)를 탐지할 수 있습니다. 즉, 하나의 권한 부여 기관이 서로 다른 객체를 명시하며 동일한 순서 번호(sequence number)로 두 개의 영수증을 발행하는 경우입니다. 이는 영수증을 통해 증명 가능한 포크(fork)입니다.
한계를 솔직하게 말씀드리자면, 설계 문서의 내용은 다음과 같습니다: 체인은 변절(equivocation)을 드러내고 귀하가 알고 있는 이벤트의 누락(omission)을 드러냅니다. 하지만 권한 있는 주체(authority)가 숨기기로 선택한 이벤트를 강제로 넘겨주게 할 수는 없습니다. 그러한 잔여물은 권한 있는 주체의 신뢰 속성(trust property)이며, 어떤 와이어 포맷(wire format)으로도 이를 제거할 수 없습니다.
- 거버넌스(governance)를 포기하지 않고 MCP 및 A2A를 래핑(wrapping)하기
귀하도 저도 스택을 새로 작성하는 것이 아닙니다. N-AALP는 프로토콜별 어댑터(per-protocol adapter) 방식이 아닌 운송 클래스(carriage class) 방식을 통해, 외부 에이전트 프로토콜을 서명된 객체(signed object) 내부에 옥텟(octet) 단위로 그대로 담아 운반합니다. 총 6개의 클래스가 이를 지원합니다: JSONRPC(MCP 및 A2A 코어가 위치하는 곳), HTTP, MSG, STREAM, DOC, 그리고 아직 정의되지 않은 모든 것을 위한 범용 캐치올(catch-all)인 OPAQUE입니다.
운반되는 바이트(bytes)는 재직렬화(re-serialized), 정규화(canonicalized), 요약(summarized) 또는 재작성(rewritten)되어서는 안 됩니다. N-AALP 메타데이터는 외부 메시지 내부가 아닌 외부에 위치합니다. 따라서 운반된 MCP 호출은 기존에 없던 신원(identity), 효과(effect), 승인 바인딩(approval binding) 및 감사 위치(audit position)를 획득하면서도, 바이트 단위로 동일하게(byte-identical) 왕복합니다.
이 중 한 가지 규칙은 별도로 강조할 가치가 있는데, 이는 위반 보고(breach report)의 대상이 될 수 있는 종류의 것이기 때문입니다. 외부 프로토콜의 신원은 결코 N-AALP 권한 신원(authorization identity)이 되지 않습니다. 특정 주체(principal)로부터
따라서 스트림(stream)은 세 개의 서명된 객체(signed objects)와 서명되지 않은 청크(chunks) 본체로 구성됩니다. StreamOpen은 스트림의 신원(identity), 그 효과(effect), 그리고 효과를 발생시키는 경우 해당 효과에 대한 승인 바인딩(approval binding)을 설정합니다. 청크 자체는 개별적으로 서명되지 않는데, 이는 전송 계층(transport)의 암호화가 이미 각 청크를 피어(peer)에게 인증하기 때문입니다. 그 다음 StreamCommit은 전체 순서가 정해진 스트림에 대해 롤링(rolling) SHA-384를 수행하며, 이를 통해 N개의 서명 대신 단 하나의 서명만으로 전체 콘텐츠에 대해 부인 방지(non-repudiation)를 구현합니다. 선택적인 서명된 체크포인트(signed checkpoints)를 사용하면 검증자(verifier)가 스트림의 끝을 기다리지 않고도 접두사(prefix)를 확인할 수 있습니다.
재계산된 다이제스트(digest)가 일치하지 않으면 StreamDigestMismatch가 발생하며, 권한이 없는 효과(effect)는 단 하나의 청크도 이동하기 전인 StreamOpen 단계에서 스트림을 거부합니다.
N-PAMP 상에서의 구조
N-AALP는 애플리케이션 계층(application layer)입니다. N-PAMP는 그 아래의 전송 계층(transport)입니다. 이들은 의도적으로 별개의 초안(drafts)이자 별개의 리포지토리(repositories)로 분리되어 있습니다.
application agent
| emits / consumes N-AALP objects
+--------------------------------------------------+
...
이러한 분리는 보장 사항(guarantees)의 깔끔한 구분을 의미합니다. 무결성(integrity), 신원(identity), 부인 방지(non-repudiation), 효과(effect) 및 감사(audit)는 객체 수준(object-level)의 기능이며 네 가지 전송 계층 모두에서 제공됩니다. 기밀성(confidentiality), 전방향 비밀성(forward secrecy) 및 연결 인증(connection authentication)은 전송 계층에서 제공하며 조건부적입니다. 객체 계층은 자신의 정확성을 위해 필요한 보장 사항을 전송 계층으로부터 읽어오지 않습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기