Перейти к основному содержимому
РУКОВОДСТВО

Как проверить подписанные запросы ИИ-агента

Практическое руководство по проверке подписей RFC 9421 от автоматизированных клиентов, что доказывает действительная подпись и что следует фиксировать в логах.

Toshendra Sharma

Автор Toshendra SharmaОснователь и CEO, AgentCredia · Updated November 13, 2026

Продвинутый · 11 мин чтения

Что доказывает подписанный запрос?

Большая часть автоматизированного трафика идентифицирует себя строкой user agent. Эта строка - утверждение, а копирование ничего не стоит. Подписанные запросы меняют картину. Когда автоматизированный клиент подписывает свои HTTP-запросы закрытым ключом и публикует соответствующий открытый ключ по адресу, связанному с его оператором, ваш сервер может проверить, что запрос действительно был создан тем, кто владеет этим ключом.

За этим стоит стандарт HTTP Message Signatures, опубликованный как RFC 9421. Рабочая группа IETF Web Bot Auth working group развивает его для автоматизированных клиентов с соглашениями о том, как агент указывает на свой каталог ключей и какие компоненты запроса он должен подписывать. По состоянию на ноябрь 2026 года документ протокола рабочей группы - это draft-ietf-webbotauth-httpsig-protocol, который определяет заголовок Signature-Agent, формат каталога ключей на основе JSON Web Key Sets и well-known URI для его публикации. Разобраться в обоих этих механизмах важно, потому что ошибиться в проверке очень легко, и ошибка может быть незаметной.

До механики важно точно понимать результат. Действительная подпись показывает, что запрос был подписан ключом, связанным с опубликованным идентификатором, например доменом оператора, и что подписанные части запроса не были изменены. Она не показывает, кто поручил агенту выполнить действие, передавал ли человек ему полномочия или безопасно ли это действие. Доказательства идентичности и авторизации - это разные вопросы, и второй из них решает уже ваше приложение. В нашей статье что доказывают подписанные запросы ИИ-агента эта граница разбирается на примерах.

Какие заголовки участвуют

Подписанный запрос обычно содержит три заголовка:

  • Signature-Input (RFC 9421, Section 4.1) описывает каждую подпись: метку, список покрытых компонентов и параметры, такие как created, expires, keyid, alg, nonce и tag.
  • Signature (RFC 9421, Section 4.2) передает байты подписи для каждой метки, закодированные в base64, как structured field.
  • 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==:

Пример упрощен для удобства чтения. В текущем черновике рабочей группы Signature-Agent сам является structured field, привязанным к метке подписи, например sig1="https://agent.example", поэтому проверьте, какой формат ожидает ваша библиотека проверки.

Шаг 1: Корректно распарсите structured fields

Signature-Input и Signature - это HTTP Structured Fields (RFC 9651, который делает RFC 8941 устаревшим RFC 8941). Не разбирайте их через простое деление строк. Кавычки, параметры и внутренние списки имеют точную грамматику, и неаккуратный парсер создает и ложные отказы, и реальные пробелы в безопасности.

Используйте поддерживаемый parser structured fields или, что лучше, поддерживаемую библиотеку RFC 9421, которая выполняет разбор и проверку вместе. Реализовывать спецификацию с нуля - полезное учебное упражнение и плохой выбор для production. Шаги ниже описывают, что такая библиотека делает, чтобы вы могли правильно ее настроить и проверить ее результат.

Шаг 2: Выберите, какую подпись проверять

Один запрос может содержать несколько меток подписи. Выберите ту, которая соответствует вашей политике, например метку, у которой tag равен web-bot-auth. Отклоните запрос для целей проверки, но не для обслуживания, если:

  • ни одна подпись не содержит нужных вам параметров,
  • алгоритм не входит в ваш allowlist, например ed25519, или
  • покрытые компоненты не включают минимальный набор, который вам нужен.

Разумный минимум для автоматизированных клиентов - это @method, @authority и @path, плюс signature-agent, если этот заголовок присутствует. Без @authority подпись, перехваченная для одного сайта, можно было бы повторно использовать против другого. Без @path ее можно было бы повторно применять между endpoint'ами. Черновик Web Bot Auth protocol draft требует, чтобы агенты покрывали @authority или @target-uri, и предупреждает, что подпись, покрывающая только authority, подтверждается для любого метода или path на этом host до истечения срока ее действия.

Шаг 3: Проверьте временные параметры

Прочитайте created и expires - временные параметры, определенные в RFC 9421, Section 2.3. Затем:

  1. Отклоняйте подписи, у которых время created находится в будущем за пределами небольшого допустимого сдвига часов.
  2. Отклоняйте подписи, у которых уже истек expires.
  3. Если expires отсутствует, применяйте собственный максимальный возраст на основе created.

Короткие окна действия ограничивают время, в течение которого перехваченный запрос можно повторно использовать.

Шаг 4: Найдите ключ

Заголовок Signature-Agent указывает на каталог ключей оператора. Получайте его по HTTPS и только по HTTPS. В draft-ietf-webbotauth-httpsig-protocol каталог описан как JSON Web Key Set (ключи в формате JWK), опубликованный с http-message-signatures-directory well-known URI на этом origin. Затем найдите ключ, чей идентификатор совпадает со значением параметра 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", а как "could not verify".
  • Ограничьте origin, из которых вы получаете ключи, если можете, или как минимум блокируйте приватные диапазоны адресов, чтобы избежать server-side request forgery. В черновике протокола server-side request forgery перечислен в разделе о соображениях безопасности именно по этой причине.
  • Привязывайте идентичность к origin, а не к текстовому имени в user agent. Идентификатор, который вы фиксируете, - это origin каталога.

Шаг 5: Пересоберите базу подписи

Подписант не подписывал сырой запрос. Он подписал каноническую строку, называемую signature base (RFC 9421, Section 2.5), построенную из покрытых компонентов в заданном порядке, после чего идет строка @signature-params. Ваш verifier пересобирает ту же самую строку из полученного запроса.

Для примера выше база выглядит так:

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"

Следите за деталями, которые ломают проверку за прокси:

  • @authority должен быть тем host, который выбрал клиент. Если load balancer переписывает Host, используйте исходное значение, которое он передает дальше.
  • @path исключает query string. Если клиент покрывал @query, пересоберите его точно в том виде, в котором он был получен.
  • Значения заголовков canonicalized так, как указано в спецификации: обрезаны по краям, а множественные значения объединены. Хорошая библиотека делает это за вас.

Шаг 6: Проверьте подпись

Когда у вас есть база и открытый ключ, проверка - это один вызов, следующий шагам из RFC 9421, Section 3.2. Для Ed25519 в Node.js, используя встроенный 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".

Шаг 7: Защититесь от повторного воспроизведения

Действительную подпись можно перехватить и отправить снова, пока она не истечет. RFC 9421, Section 7.2.2 рассматривает replay подписи, а черновик протокола Web Bot Auth отсылает к нему, не добавляя собственные правила nonce. Два контроля закрывают большую часть этого окна:

  1. Короткий срок действия, который обеспечивается на шаге 3.
  2. Отслеживание nonce. Когда присутствует nonce, сохраните его вместе с идентификатором ключа до истечения срока действия подписи и отклоните повторное использование.
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;
}

В развертывании с несколькими экземплярами храните nonce в общем хранилище, например Redis, с time to live, равным оставшемуся сроку действия.

Шаг 8: Запишите результат с его основанием

Проверка создает evidence, а не вердикт. Сохраняйте ее в форме, которую смогут использовать аналитика и политики:

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" от одного оператора часто означает, что он сменил ключи, а не то, что он атакует вас.

Что нужно логировать из проверки подписи?

Логируйте origin оператора, идентификатор ключа, покрытые компоненты, временные параметры, результат и причину сбоя. Не логируйте полные байты подписи или целые тела запросов. Храните логи достаточно долго, чтобы изучить закономерности, и достаточно недолго, чтобы соблюдать обязательства по конфиденциальности.

Как проверенная подпись должна менять вашу политику?

Когда запросы можно проверять, вы можете по-разному и осмысленно к ним относиться. В нашей статье проектирование политик приложения для людей и ИИ-агентов описана более широкая модель политики:

  • Разрешайте проверенным операторам доступ к route только для чтения с более высоким rate limit, чем для непроверенной автоматизации.
  • Предлагайте им структурированные ответы, например JSON или Markdown, вместо полноценных HTML-страниц.
  • Оставляйте действия с высоким риском под вашей обычной авторизацией. Проверенная подпись показывает, какой client обращается к вам, но не то, что он может изменять account.

Начинайте в режиме наблюдения. Измерьте, какая доля вашего автоматизированного трафика подписана, какие операторы появляются и как часто проверка не проходит, прежде чем какая-либо политика начнет зависеть от этого.

Где здесь подходит Proof of Actor

Проверка подписи - это один из источников evidence среди нескольких. Proof of Actor объединяет ее с другими сигналами и сохраняет basis видимым в каждом результате, поэтому проверенный agent никогда не смешивается с тем, кто только declared себя. Ваше приложение решает, что будет дальше.

Чтобы внедрить это в production-приложение вместе с нашими инженерами, подайте заявку на ранний доступ. Для полной картины того, что evidence может и не может показать, прочитайте Detection & Limitations.

Источники

Постройте это вместе с нами.

Присоединяйтесь к founding developer program Proof of Actor.

Предпочитаете сначала поговорить? Свяжитесь с основателями