Một yêu cầu đã ký chứng minh điều gì?
Phần lớn lưu lượng tự động tự nhận dạng bằng một user agent string. Chuỗi đó chỉ là một lời khai báo, và việc sao chép nó không tốn gì. Yêu cầu đã ký thay đổi cục diện. Khi một client tự động ký các HTTP requests của nó bằng một private key và công bố public key tương ứng tại một vị trí gắn với người vận hành, server của bạn có thể kiểm tra rằng yêu cầu thực sự được tạo ra bởi một người đang giữ khóa đó.
Tiêu chuẩn đứng sau cơ chế này là HTTP Message Signatures, được công bố dưới dạng RFC 9421. IETF Web Bot Auth working group xây dựng trên đó với các quy ước cho client tự động: cách một agent chỉ tới thư mục khóa của nó và những thành phần nào của request mà nó nên ký. Tính đến tháng 11 năm 2026, tài liệu giao thức của working group là draft-ietf-webbotauth-httpsig-protocol, tài liệu này định nghĩa header Signature-Agent, một định dạng thư mục khóa dựa trên JSON Web Key Sets và một URI well-known để phục vụ nó. Cả hai đều đáng để hiểu kỹ, vì xác minh rất dễ sai một cách tinh vi.
Trước khi đi vào cơ chế, hãy nói chính xác về kết quả. Một chữ ký hợp lệ cho thấy request đã được ký bằng một khóa gắn với một định danh đã công bố, chẳng hạn domain của operator, và những phần của request được ký đã không bị sửa đổi. Nó không cho thấy ai đã chỉ thị cho agent, liệu một người có ủy quyền cho nó hay không, hoặc hành động có an toàn hay không. Bằng chứng về danh tính và ủy quyền là hai câu hỏi riêng, và ứng dụng của bạn sẽ trả lời câu hỏi thứ hai. Bài viết trên blog của chúng tôi về những gì các yêu cầu AI agent đã ký chứng minh đi qua ranh giới đó bằng các ví dụ.
Các header liên quan
Một yêu cầu đã ký thường mang ba header:
Signature-Input(RFC 9421, Section 4.1) mô tả từng chữ ký: một nhãn, danh sách các thành phần được bao phủ, và các tham số nhưcreated,expires,keyid,alg,noncevàtag.Signature(RFC 9421, Section 4.2) chứa các byte chữ ký cho từng nhãn, được mã hóa base64, dưới dạng một structured field.Signature-Agent(theo quy ước của Web Bot Auth) chỉ tới nơi public keys của client được công bố, thường là một HTTPS URL trên domain của operator.
Ví dụ rút gọn:
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==:Ví dụ này đã được giản lược để dễ đọc. Trong bản nháp hiện tại của working group, Signature-Agent tự nó là một structured field được khóa theo signature label (ví dụ sig1="https://agent.example"), vì vậy hãy kiểm tra định dạng mà thư viện xác minh của bạn mong đợi.
Bước 1: Phân tích structured fields đúng cách
Signature-Input và Signature là HTTP Structured Fields (RFC 9651, tài liệu này thay thế RFC 8941). Đừng phân tích chúng bằng cách tách chuỗi. Chuỗi trong dấu ngoặc kép, tham số và inner list đều có ngữ pháp chính xác, và một bộ phân tích cẩu thả sẽ tạo ra cả từ chối sai lẫn lỗ hổng bảo mật thực sự.
Hãy dùng một structured-field parser được duy trì, hoặc tốt hơn là một thư viện RFC 9421 được duy trì, xử lý cả phân tích lẫn xác minh cùng lúc. Tự triển khai đặc tả từ đầu là một bài tập học tập tốt nhưng là một lựa chọn kém cho production. Các bước dưới đây mô tả việc một thư viện như vậy làm gì, để bạn có thể cấu hình đúng và xem xét đầu ra của nó.
Bước 2: Chọn chữ ký sẽ đánh giá
Một request có thể mang nhiều hơn một signature label. Hãy chọn nhãn phù hợp với chính sách của bạn, ví dụ nhãn có tag là web-bot-auth. Từ chối request cho mục đích xác minh (không phải cho mục đích phục vụ) nếu:
- không có chữ ký nào có các tham số bạn yêu cầu,
- algorithm không nằm trong allowlist của bạn (ví dụ
ed25519), hoặc - các thành phần được bao phủ không bao gồm tập tối thiểu bạn yêu cầu.
Một mức tối thiểu hợp lý cho client tự động là @method, @authority và @path, cộng thêm signature-agent khi header đó có mặt. Nếu không có @authority, một chữ ký bị chụp lại cho một site có thể được phát lại sang site khác. Nếu không có @path, nó có thể được dùng lại trên nhiều endpoint. Bản nháp Web Bot Auth protocol draft yêu cầu agent bao phủ @authority hoặc @target-uri, và cảnh báo rằng một chữ ký chỉ bao phủ authority sẽ xác minh được với mọi method hoặc path trên host đó cho đến khi nó hết hạn.
Bước 3: Kiểm tra các tham số thời gian
Đọc created và expires, các tham số thời gian được định nghĩa trong RFC 9421, Section 2.3. Sau đó:
- Từ chối những chữ ký có thời gian
creatednằm trong tương lai vượt quá một mức lệch đồng hồ nhỏ. - Từ chối những chữ ký đã vượt quá
expires. - Nếu thiếu
expires, áp dụng tuổi tối đa của riêng bạn dựa trêncreated.
Các cửa sổ hiệu lực ngắn sẽ giới hạn thời gian một request bị chụp lại có thể được phát lại.
Bước 4: Tìm khóa
Header Signature-Agent chỉ tới thư mục khóa của operator. Lấy nó qua HTTPS, và chỉ qua HTTPS. Bản nháp Web Bot Auth protocol draft mô tả thư mục này là một JSON Web Key Set (khóa ở định dạng JWK) được phục vụ từ http-message-signatures-directory well-known URI trên origin đó. Sau đó tìm khóa có identifier khớp với tham số keyid.
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;
}Các quy tắc thực tế cho việc tìm khóa:
- Cache khóa với thời hạn giới hạn để việc xác minh không thêm một network call vào mọi request.
- Đặt timeout ngắn và xem một lần fetch thất bại là "không thể xác minh", không bao giờ là "đã xác minh".
- Hạn chế origin bạn fetch từ nếu có thể, hoặc ít nhất chặn các dải địa chỉ riêng để tránh server-side request forgery. Bản nháp giao thức liệt kê server-side request forgery trong phần cân nhắc bảo mật chính vì lý do này.
- Gắn danh tính với origin, không phải với tên văn bản tự do trong user agent. Identifier bạn ghi lại là origin của thư mục.
Bước 5: Dựng lại signature base
Bên ký không ký request thô. Nó ký một chuỗi chuẩn hóa gọi là signature base (RFC 9421, Section 2.5), được tạo từ các thành phần được bao phủ theo đúng thứ tự, sau đó là dòng @signature-params. Bộ xác minh của bạn sẽ dựng lại cùng một chuỗi từ request mà nó nhận được.
Với ví dụ ở trên, base trông như sau:
"@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"Hãy chú ý các chi tiết làm hỏng việc xác minh phía sau proxy:
@authorityphải là host mà client đã nhắm tới. Nếu load balancer viết lạiHost, hãy dùng giá trị gốc mà nó chuyển tiếp.@pathloại trừ chuỗi truy vấn. Nếu client đã bao phủ@query, hãy dựng lại đúng như đã nhận.- Các giá trị header được chuẩn hóa theo quy định: được cắt gọn, và nhiều giá trị được kết hợp lại. Một thư viện tốt sẽ làm việc này cho bạn.
Bước 6: Xác minh chữ ký
Với base và public key, việc xác minh chỉ là một lời gọi duy nhất, theo các bước trong RFC 9421, Section 3.2. Với Ed25519 trong Node.js, dùng crypto module tích hợp sẵn:
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"));
}Nếu kiểm tra thất bại, request chưa được xác minh. Nó vẫn có thể được phục vụ. Nó chỉ không nhận basis "verified".
Bước 7: Ngăn phát lại
Một chữ ký hợp lệ có thể bị chụp lại và gửi lại cho đến khi nó hết hạn. RFC 9421, Section 7.2.2 nói về signature replay, và bản nháp Web Bot Auth protocol chuyển tiếp sang tài liệu đó thay vì thêm các quy tắc nonce riêng. Hai biện pháp kiểm soát sẽ đóng phần lớn cửa sổ đó:
- Thời hạn ngắn, được thực thi ở Bước 3.
- Theo dõi nonce. Khi có
nonce, hãy lưu nó cùng với key identifier của nó cho đến khi chữ ký hết hạn, rồi từ chối lần dùng thứ hai.
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;
}Trong một triển khai nhiều instance, hãy giữ nonce trong một kho chia sẻ như Redis với thời gian sống bằng thời gian hiệu lực còn lại.
Bước 8: Ghi lại kết quả cùng basis của nó
Việc xác minh tạo ra bằng chứng, không phải phán quyết. Hãy ghi lại nó dưới dạng mà analytics và policy của bạn có thể dùng:
const result = {
actor: "agent",
basis: "verified",
operator: "https://agent.example",
keyId: "kq1",
coveredComponents: ["@method", "@authority", "@path", "signature-agent"],
verifiedAt: new Date().toISOString(),
};Khi việc xác minh thất bại hoặc không thể chạy, hãy ghi lại điều đó nữa, cùng với lý do: thiếu header, algorithm không được hỗ trợ, không tìm thấy key, đã hết hạn, đã bị phát lại, hoặc chữ ký không khớp. Những lý do này rất có giá trị. Một đợt tăng vọt "key not found" từ một operator thường có nghĩa là họ đã xoay vòng khóa, chứ không phải họ đang tấn công bạn.
Bạn nên ghi gì từ việc xác minh chữ ký?
Hãy ghi operator origin, key identifier, các thành phần được bao phủ, các tham số thời gian, kết quả và lý do thất bại. Đừng ghi toàn bộ chữ ký hoặc toàn bộ nội dung request body. Giữ log đủ lâu để nghiên cứu mẫu hình và đủ ngắn để tôn trọng các cam kết về quyền riêng tư của bạn.
Một chữ ký đã xác minh nên thay đổi policy của bạn như thế nào?
Khi request có thể được xác minh, bạn có thể xử lý chúng khác đi theo một cách có ý nghĩa. Bài viết của chúng tôi về thiết kế policy ứng dụng cho con người và AI agent trình bày mô hình policy rộng hơn:
- Cho phép operator đã xác minh đi vào các route chỉ đọc với rate limit cao hơn so với tự động hóa chưa được xác minh.
- Cung cấp cho họ các phản hồi có cấu trúc, chẳng hạn như JSON hoặc Markdown, thay vì toàn bộ trang HTML.
- Giữ các hành động có tác động cao sau lớp authorization bình thường của bạn. Một chữ ký đã xác minh cho bạn biết client nào đang gọi, chứ không cho biết nó có thể thay đổi một account hay không.
Hãy bắt đầu ở chế độ quan sát. Đo xem bao nhiêu phần trăm lưu lượng tự động của bạn đã được ký, những operator nào xuất hiện, và việc xác minh thất bại bao lâu một lần, trước khi bất kỳ policy nào phụ thuộc vào nó.
Proof of Actor phù hợp ở đâu
Xác minh chữ ký là một nguồn bằng chứng trong số nhiều nguồn. Proof of Actor kết hợp nó với các tín hiệu khác và giữ basis hiển thị trên mọi kết quả, để một agent đã được xác minh không bao giờ bị nhầm với một agent chỉ tự khai báo. Ứng dụng của bạn quyết định điều gì xảy ra tiếp theo.
Để xây dựng điều này vào một app production cùng với các kỹ sư của chúng tôi, hãy đăng ký quyền truy cập sớm. Để có cái nhìn đầy đủ về những gì bằng chứng có thể và không thể cho thấy, hãy đọc Detection & Limitations.
Nguồn
- RFC 9421: HTTP Message Signatures: tiêu chuẩn ký và xác minh, bao gồm các tham số chữ ký, signature base và các cân nhắc về phát lại
- RFC 9651: Structured Field Values for HTTP: ngữ pháp được dùng bởi
Signature-InputvàSignature - IETF Web Bot Auth working group: charter và trạng thái của công việc xác thực client tự động
- draft-ietf-webbotauth-httpsig-protocol: Internet-Draft định nghĩa
Signature-Agent, thư mục khóa JWKS và URI well-known của nó (đang triển khai tính đến tháng 11 năm 2026) - RFC 7517: JSON Web Key (JWK): định dạng khóa được dùng trong các thư mục khóa đã công bố
- RFC 8615: Well-Known URIs: cách các đường dẫn well-known như thư mục khóa được đăng ký
- RFC 8032: Edwards-Curve Digital Signature Algorithm (EdDSA): thuật toán Ed25519 được dùng trong ví dụ
- Node.js crypto documentation:
createPublicKeyvàverifyđược dùng ở Bước 6