주 콘텐츠로 건너뛰기
가이드

서명된 AI 에이전트 요청을 검증하는 방법

자동화된 클라이언트의 RFC 9421 서명을 검증하는 실무 가이드로, 유효한 서명이 무엇을 증명하는지와 로그에 무엇을 남겨야 하는지를 설명합니다.

Toshendra Sharma

작성자 Toshendra Sharma창립자 겸 CEO, AgentCredia · Updated November 13, 2026

고급 · 11분 읽기

서명된 요청은 무엇을 증명하나요?

대부분의 자동화 트래픽은 user agent string으로 자신을 식별합니다. 그 문자열은 주장일 뿐이고, 복사하는 데 비용이 들지 않습니다. 서명된 요청은 이 상황을 바꿉니다. 자동화된 클라이언트가 자신의 HTTP 요청에 개인 키로 서명하고, 운영자와 연결된 위치에 일치하는 공개 키를 게시하면, 서버는 요청이 실제로 그 키를 보유한 주체에 의해 생성되었는지 확인할 수 있습니다.

이 표준은 HTTP Message Signatures이며, RFC 9421로 공개되어 있습니다. IETF Web Bot Auth working group는 자동화된 클라이언트를 위한 관례를 이 위에 얹습니다. 에이전트가 자신의 키 디렉터리를 가리키는 방법과 어떤 요청 구성 요소에 서명해야 하는지가 여기에 포함됩니다. 2026년 11월 기준으로, 이 working group's protocol document는 draft-ietf-webbotauth-httpsig-protocol이며, Signature-Agent 헤더, JSON Web Key Sets 기반의 키 디렉터리 형식, 그리고 이를 제공하기 위한 well-known URI를 정의합니다. 검증은 미묘하게 잘못하기 쉬우므로, 이 둘을 세부적으로 이해하는 것이 중요합니다.

기계적인 절차를 보기 전에 결과를 정확히 구분해야 합니다. 유효한 서명은 요청이 운영자의 도메인 같은 공개된 식별자와 연결된 키로 서명되었고, 요청의 서명 대상 부분이 변경되지 않았음을 보여줍니다. 그러나 누가 에이전트에게 지시했는지, 사람이 권한을 위임했는지, 또는 행동이 안전한지는 보여주지 않습니다. 신원 증거와 권한 부여는 서로 다른 질문이며, 애플리케이션이 답해야 하는 것은 두 번째 질문입니다. 서명된 AI agent requests가 무엇을 증명하는지에 대한 블로그 글에서 이 경계를 예시와 함께 설명합니다.

관련 헤더

서명된 요청에는 일반적으로 세 개의 헤더가 포함됩니다.

  • Signature-Input (RFC 9421, Section 4.1)은 각 서명을 설명합니다. 라벨, 적용 대상 구성 요소 목록, 그리고 created, expires, keyid, alg, nonce 및 tag 같은 매개변수를 포함합니다.
  • Signature (RFC 9421, Section 4.2)는 각 라벨에 대한 서명 바이트를 structured field로 base64 인코딩해 담습니다.
  • Signature-Agent(Web Bot Auth 관례에서 가져옴)는 클라이언트의 공개 키가 게시된 위치를 가리킵니다. 보통 운영자 도메인의 HTTPS URL입니다.

간단한 예시는 다음과 같습니다.

HTTP
GET /api/catalog?page=2 HTTP/1.1
Host: shop.example
Signature-Agent: "https://agent.example"
Signature-Input: sig1=("@method" "@authority" "@path" "signature-agent");created=1762934400;expires=1762934700;keyid="kq1";alg="ed25519";tag="web-bot-auth";nonce="d9f1c2"
Signature: sig1=:Base64SignatureBytes==:

가독성을 위해 예시는 단순화했습니다. 현재 working group draft에서 Signature-Agent은 signature label을 키로 하는 structured field 자체입니다(예: sig1="https://agent.example"). 따라서 검증 라이브러리가 어떤 형식을 기대하는지 확인하세요.

1단계: structured field를 올바르게 파싱하기

Signature-Input과 Signature는 HTTP Structured Fields(RFC 9651, RFC 8941을 대체함)입니다. 문자열 분할로 파싱하지 마세요. quoted string, parameter, inner list는 모두 정확한 문법을 가지며, 허술한 파서는 오탐 차단과 실제 보안 공백을 함께 만듭니다.

유지 관리되는 structured-field parser를 사용하거나, 더 나아가 파싱과 검증을 함께 처리하는 유지 관리되는 RFC 9421 라이브러리를 사용하세요. 사양을 처음부터 구현하는 것은 학습용으로는 좋지만, 운영 환경에서는 좋은 선택이 아닙니다. 아래 단계는 그런 라이브러리가 무엇을 하는지 설명하므로, 올바르게 설정하고 출력을 검토할 수 있습니다.

2단계: 어떤 서명을 평가할지 선택하기

한 요청에 여러 signature label이 있을 수 있습니다. 정책에 맞는 것을 선택하세요. 예를 들어 tag가 web-bot-auth인 라벨입니다. 다음 경우에는 검증 목적상 요청을 거부하세요. 서비스 제공 목적이 아니라 검증 목적입니다.

  • 필요한 매개변수가 있는 서명이 없음
  • 알고리즘이 허용 목록에 없음(예: ed25519)
  • 적용 대상 구성 요소에 필요한 최소 집합이 포함되지 않음

자동화된 클라이언트의 합리적인 최소 기준은 @method, @authority, @path이며, 해당 헤더가 존재할 때는 signature-agent도 포함됩니다. @authority이 없으면 한 사이트에서 캡처한 서명이 다른 사이트에 재사용될 수 있습니다. @path이 없으면 여러 endpoint에 걸쳐 재사용될 수 있습니다. Web Bot Auth protocol draft은 에이전트에게 @authority 또는 @target-uri을 포함하도록 요구하며, authority만 포함한 서명은 만료될 때까지 해당 host의 어떤 method나 path에도 유효하다고 경고합니다.

3단계: 시간 매개변수 확인하기

RFC 9421, Section 2.3에 정의된 시간 매개변수인 created과 expires를 읽습니다. 그런 다음 다음을 수행하세요.

  1. created 시간이 작은 clock skew 허용 범위를 넘어서 미래인 서명은 거부합니다.
  2. expires가 지난 서명은 거부합니다.
  3. expires가 없으면 created를 기준으로 자체 최대 수명을 적용합니다.

유효 기간이 짧을수록 캡처된 요청이 재전송될 수 있는 시간이 줄어듭니다.

4단계: 키를 찾기

Signature-Agent 헤더는 운영자의 키 디렉터리를 가리킵니다. HTTPS로 가져오세요. 오직 HTTPS만 허용하세요. Web Bot Auth protocol draft은 이 디렉터리를 JSON Web Key Set으로 설명합니다. 키는 해당 origin의 http-message-signatures-directory well-known URI에서 JWK 형식으로 제공됩니다. 그런 다음 식별자가 keyid 매개변수와 일치하는 키를 찾습니다.

TS
type Jwk = { kid: string; kty: string; crv?: string; x?: string };

const keyCache = new Map<string, { keys: Jwk[]; fetchedAt: number }>();
const KEY_TTL_MS = 60 * 60 * 1000;

export async function getKey(directoryUrl: string, keyId: string): Promise<Jwk | null> {
  const url = new URL(directoryUrl);
  if (url.protocol !== "https:") return null;

  const cached = keyCache.get(url.origin);
  const fresh = cached && Date.now() - cached.fetchedAt < KEY_TTL_MS;
  const keys = fresh ? cached.keys : await fetchKeys(url.origin);
  return keys.find((k) => k.kid === keyId) ?? null;
}

async function fetchKeys(origin: string): Promise<Jwk[]> {
  const res = await fetch(`${origin}/.well-known/http-message-signatures-directory`, {
    headers: { accept: "application/http-message-signatures-directory+json, application/json" },
    signal: AbortSignal.timeout(2000),
  });
  if (!res.ok) return [];
  const body = (await res.json()) as { keys?: Jwk[] };
  const keys = Array.isArray(body.keys) ? body.keys : [];
  keyCache.set(origin, { keys, fetchedAt: Date.now() });
  return keys;
}

키 탐색에 대한 실무 규칙은 다음과 같습니다.

  • 키를 캐시하되, 수명을 제한하여 검증이 모든 요청에 네트워크 호출을 추가하지 않도록 합니다.
  • 짧은 timeout을 설정하고, 조회 실패는 절대로 "verified"가 아니라 "검증할 수 없음"으로 처리합니다.
  • 가능하면 가져올 origin을 제한하세요. 최소한 사설 주소 범위는 차단하여 server-side request forgery를 방지합니다. protocol draft는 바로 이 이유로 security considerations에 server-side request forgery를 포함합니다.
  • 식별자를 자유 형식 user agent 문자열이 아니라 origin에 묶으세요. 기록하는 식별자는 디렉터리 origin입니다.

5단계: 서명 기반 문자열 다시 구성하기

서명자는 원시 요청에 서명한 것이 아닙니다. 적용 대상 구성 요소를 순서대로 배치한 뒤 @signature-params 줄을 붙여 만든 canonical string, 즉 signature base(RFC 9421, Section 2.5)에 서명했습니다. 검증자는 받은 요청으로부터 같은 문자열을 다시 구성합니다.

위 예시에서 base는 다음과 같습니다.

TEXT
"@method": GET
"@authority": shop.example
"@path": /api/catalog
"signature-agent": "https://agent.example"
"@signature-params": ("@method" "@authority" "@path" "signature-agent");created=1762934400;expires=1762934700;keyid="kq1";alg="ed25519";tag="web-bot-auth";nonce="d9f1c2"

proxy 뒤에서 검증을 깨뜨리는 세부 사항에 주의하세요.

  • @authority은 클라이언트가 대상으로 지정한 host여야 합니다. load balancer가 Host를 다시 쓰는 경우, 전달하는 원래 값을 사용하세요.
  • @path는 query string을 제외합니다. 클라이언트가 @query를 포함했다면, 받은 그대로 정확히 다시 구성하세요.
  • header value는 명세대로 canonicalize됩니다. 공백이 정리되고, 여러 값은 결합됩니다. 좋은 라이브러리는 이 작업을 대신 수행합니다.

6단계: 서명 검증하기

base와 공개 키가 있으면, RFC 9421, Section 3.2의 절차에 따라 검증은 단일 호출로 끝납니다. Node.js에서 Ed25519를 사용할 때는 내장 crypto module를 사용합니다.

TS
import { createPublicKey, verify } from "node:crypto";

export function verifyEd25519(jwk: Jwk, base: string, signatureB64: string): boolean {
  const key = createPublicKey({ key: jwk as any, format: "jwk" });
  return verify(null, Buffer.from(base, "utf8"), key, Buffer.from(signatureB64, "base64"));
}

검사가 실패하면 요청은 검증되지 않은 것입니다. 그래도 서비스할 수는 있습니다. 다만 "verified" basis를 얻지 못할 뿐입니다.

7단계: 재전송 방지하기

유효한 서명은 만료될 때까지 캡처되어 다시 전송될 수 있습니다. RFC 9421, Section 7.2.2는 signature replay를 다루며, Web Bot Auth protocol draft는 자체 nonce 규칙을 추가하지 않고 그 기준을 따릅니다. 대부분의 재전송 창을 닫는 제어 수단은 두 가지입니다.

  1. 짧은 만료 시간, 3단계에서 강제합니다.
  2. Nonce 추적. nonce가 있으면, 서명이 만료될 때까지 key identifier와 함께 저장하고 두 번째 사용은 거부합니다.
TS
const seen = new Map<string, number>();

export function acceptNonce(keyId: string, nonce: string, expiresAt: number): boolean {
  const id = `${keyId}:${nonce}`;
  const now = Date.now() / 1000;
  for (const [k, exp] of seen) if (exp < now) seen.delete(k);
  if (seen.has(id)) return false;
  seen.set(id, expiresAt);
  return true;
}

여러 인스턴스로 배포하는 경우, 남은 유효 기간과 같은 time to live를 가진 Redis 같은 공유 저장소에 nonce를 보관하세요.

8단계: basis와 함께 결과 기록하기

검증은 판단이 아니라 증거를 생성합니다. 분석과 정책이 사용할 수 있는 형식으로 기록하세요.

TS
const result = {
  actor: "agent",
  basis: "verified",
  operator: "https://agent.example",
  keyId: "kq1",
  coveredComponents: ["@method", "@authority", "@path", "signature-agent"],
  verifiedAt: new Date().toISOString(),
};

검증이 실패하거나 실행할 수 없을 때도, 이유와 함께 기록하세요. 헤더 누락, 지원하지 않는 알고리즘, key not found, 만료, 재전송, 서명 불일치 등이 해당합니다. 이런 이유는 가치가 있습니다. 한 운영자에게서 "key not found"가 급증하면, 공격 중이라는 뜻이 아니라 키를 교체했다는 뜻인 경우가 많습니다.

서명 검증에서 무엇을 로그해야 하나요?

운영자 origin, key identifier, 적용 대상 구성 요소, 시간 매개변수, 결과, 실패 사유를 로그하세요. 전체 서명 바이트나 전체 요청 본문은 로그하지 마세요. 추세를 분석할 수 있을 만큼은 충분히, 프라이버시 약속을 지킬 수 있을 만큼은 짧게 보관하세요.

검증된 서명은 정책을 어떻게 바꿔야 하나요?

요청을 검증할 수 있게 되면, 의미 있는 방식으로 다르게 처리할 수 있습니다. 사람과 AI agent를 위한 app policy 설계 글에서 더 넓은 정책 모델을 다룹니다.

  • 검증된 operator에게는 읽기 전용 route에서 검증되지 않은 자동화보다 더 높은 rate limit을 적용합니다.
  • 전체 HTML page 대신 JSON이나 Markdown 같은 구조화된 응답을 제공합니다.
  • 영향이 큰 작업은 기존 authorization 뒤에 둡니다. 검증된 서명은 어떤 client가 호출했는지를 알려줄 뿐, account를 변경할 수 있다는 뜻은 아닙니다.

먼저 observation mode로 시작하세요. 어떤 automated traffic이 서명되어 있는지, 어떤 operator가 나타나는지, 검증 실패가 얼마나 자주 발생하는지 측정한 뒤에 policy를 적용하세요.

Proof of Actor는 어디에 맞물리나요?

서명 검증은 여러 증거 소스 중 하나입니다. Proof of Actor는 이를 다른 signal과 결합하고, 모든 결과에서 basis를 드러내므로, 검증된 agent가 단지 자신이라고 주장한 agent와 혼동되지 않습니다. 다음에 무엇을 할지는 애플리케이션이 결정합니다.

이 기능을 엔지니어와 함께 운영 환경의 app에 넣으려면 early access를 신청하세요. 증거가 무엇을 보여주고 무엇을 보여주지 못하는지 전체적으로 보려면 Detection & Limitations를 읽어보세요.

출처

함께 구축하세요.

Proof of Actor founding developer program에 참여하세요.

먼저 이야기하고 싶으신가요? 창립자와 상담하세요