คำขอที่ลงนามแล้วยืนยันอะไรได้บ้าง?
ทราฟฟิกอัตโนมัติส่วนใหญ่ระบุตัวเองด้วย user agent string สตริงนั้นเป็นเพียงข้ออ้างอิง และคัดลอกได้โดยไม่เสียค่าใช้จ่าย คำขอที่ลงนามแล้วเปลี่ยนภาพนี้ เมื่อไคลเอนต์อัตโนมัติลงนามคำขอ HTTP ของตนด้วย private key และเผยแพร่ public key ที่จับคู่กันไว้ในตำแหน่งที่ผูกกับผู้ปฏิบัติการ เซิร์ฟเวอร์ของคุณก็ตรวจได้ว่าคำขอนั้นถูกสร้างโดยคนที่ถือคีย์นั้นจริง
มาตรฐานเบื้องหลังเรื่องนี้คือ HTTP Message Signatures ซึ่งเผยแพร่เป็น RFC 9421 IETF Web Bot Auth working group สร้างต่อยอดจากมาตรฐานนี้ด้วยแนวปฏิบัติสำหรับไคลเอนต์อัตโนมัติ: วิธีที่ agent ชี้ไปยังไดเรกทอรีคีย์ของตน และคอมโพเนนต์ใดของคำขอที่ควรลงนาม ณ เดือนพฤศจิกายน 2026 เอกสารโปรโตคอลของ working group คือ draft-ietf-webbotauth-httpsig-protocol ซึ่งกำหนด header Signature-Agent รูปแบบไดเรกทอรีคีย์ที่อิงกับ JSON Web Key Sets และ well-known URI สำหรับให้บริการมัน ทั้งสองส่วนคุ้มค่าที่จะเข้าใจอย่างละเอียด เพราะการยืนยันตรวจได้ง่ายแต่ก็ทำผิดพลาดแบบแยบยลได้ง่ายเช่นกัน
ก่อนลงไปที่กลไก ให้แม่นยำเกี่ยวกับผลลัพธ์เสียก่อน ลายเซ็นที่ถูกต้องแสดงว่าคำขอนั้นถูกลงนามด้วยคีย์ที่เชื่อมโยงกับตัวระบุที่เผยแพร่ เช่น โดเมนของผู้ปฏิบัติการ และส่วนของคำขอที่ถูกลงนามไม่ได้ถูกแก้ไข มันไม่ได้บอกว่าใครเป็นผู้สั่ง agent ไม่ได้บอกว่ามีคนมอบอำนาจให้มันหรือไม่ และไม่ได้บอกว่าการกระทำนั้นปลอดภัยหรือไม่ หลักฐานด้านตัวตนและการอนุญาตเป็นคนละคำถามกัน และแอปพลิเคชันของคุณเป็นผู้ตอบคำถามข้อที่สอง โพสต์บล็อกของเราเรื่อง signed AI agent requests prove อธิบายขอบเขตนี้พร้อมตัวอย่าง
ส่วนหัวที่เกี่ยวข้อง
คำขอที่ลงนามแล้วมักมีส่วนหัวสามรายการ:
Signature-Input(RFC 9421, Section 4.1) อธิบายลายเซ็นแต่ละรายการ: ป้ายกำกับ รายการของคอมโพเนนต์ที่ครอบคลุม และพารามิเตอร์อย่างcreated,expires,keyid,alg,nonceและtagSignature(RFC 9421, Section 4.2) บรรจุไบต์ของลายเซ็นสำหรับแต่ละป้ายกำกับในรูป base64 เป็น structured fieldSignature-Agent(จากแนวปฏิบัติ Web Bot Auth) ชี้ไปยังตำแหน่งที่เผยแพร่ public key ของไคลเอนต์ โดยทั่วไปเป็น HTTPS URL บนโดเมนของผู้ปฏิบัติการ
ตัวอย่างแบบย่อ:
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==:ตัวอย่างนี้ถูกย่อเพื่อให้อ่านง่าย ใน draft ปัจจุบันของ working group Signature-Agent เป็น structured field เองที่จัดกลุ่มตามป้ายกำกับลายเซ็น (เช่น sig1="https://agent.example") ดังนั้นให้ตรวจดูว่าไลบรารีสำหรับยืนยันของคุณคาดหวังรูปแบบใด
ขั้นที่ 1: แยกวิเคราะห์ structured fields ให้ถูกต้อง
Signature-Input และ Signature เป็น HTTP Structured Fields (RFC 9651 ซึ่งมาแทนที่ RFC 8941) อย่าแยกวิเคราะห์ด้วยการตัดสตริง Quoted strings พารามิเตอร์ และ inner lists ล้วนมีกฎไวยากรณ์ที่แม่นยำ และ parser ที่ทำแบบลวกๆ จะก่อให้เกิดทั้งการปฏิเสธที่ผิดพลาดและช่องโหว่ด้านความปลอดภัยจริง
ใช้ structured-field parser ที่ดูแลต่อเนื่อง หรือยิ่งดีกว่านั้นคือใช้ไลบรารี RFC 9421 ที่ดูแลต่อเนื่องและจัดการทั้งการแยกวิเคราะห์และการยืนยันในตัวเอง การลงมือเขียนสเปกจากศูนย์เป็นแบบฝึกหัดที่ดีสำหรับการเรียนรู้ แต่เป็นตัวเลือกที่ไม่ดีสำหรับการใช้งานจริง ขั้นตอนด้านล่างอธิบายสิ่งที่ไลบรารีลักษณะนี้ทำ เพื่อให้คุณตั้งค่ามันได้ถูกต้องและตรวจสอบผลลัพธ์ของมัน
ขั้นที่ 2: เลือกลายเซ็นที่จะประเมิน
คำขอหนึ่งรายการอาจมี label ของลายเซ็นมากกว่าหนึ่งรายการ เลือกรายการที่ตรงกับนโยบายของคุณ เช่น label ที่มี tag เป็น web-bot-auth ปฏิเสธคำขอเพื่อวัตถุประสงค์การยืนยันเท่านั้น (ไม่ใช่การให้บริการ) หาก:
- ไม่มีลายเซ็นใดมีพารามิเตอร์ที่คุณต้องการ
- อัลกอริทึมไม่ได้อยู่ใน allowlist ของคุณ เช่น
ed25519 - คอมโพเนนต์ที่ครอบคลุมไม่รวมชุดขั้นต่ำที่คุณต้องการ
ชุดขั้นต่ำที่เหมาะสมสำหรับไคลเอนต์อัตโนมัติคือ @method, @authority และ @path พร้อมด้วย signature-agent เมื่อมี header นี้อยู่ หากไม่มี @authority ลายเซ็นที่จับมาจากไซต์หนึ่งอาจถูกเล่นซ้ำกับอีกไซต์หนึ่งได้ หากไม่มี @path ก็อาจนำกลับมาใช้ซ้ำข้าม endpoint ได้ Web Bot Auth protocol draft กำหนดให้ agent ครอบคลุม @authority หรือ @target-uri และเตือนว่าลายเซ็นที่ครอบคลุมแค่ authority จะยืนยันได้กับทุก method หรือ path บน host นั้นจนกว่าจะหมดอายุ
ขั้นที่ 3: ตรวจสอบพารามิเตอร์เวลา
อ่าน created และ expires ซึ่งเป็นพารามิเตอร์เวลาที่กำหนดไว้ใน RFC 9421, Section 2.3 จากนั้น:
- ปฏิเสธลายเซ็นที่เวลา
createdอยู่ในอนาคตเกินค่าคลาดเคลื่อนของนาฬิกาที่กำหนดไว้เล็กน้อย - ปฏิเสธลายเซ็นที่ผ่าน
expiresแล้ว - หากไม่มี
expiresให้บังคับอายุสูงสุดของคุณเองตามcreated
หน้าต่างเวลาที่สั้นช่วยจำกัดระยะเวลาที่คำขอที่ถูกจับไว้สามารถถูกเล่นซ้ำได้
ขั้นที่ 4: ค้นหาคีย์
header Signature-Agent ชี้ไปยังไดเรกทอรีคีย์ของผู้ปฏิบัติการ ดึงมันผ่าน HTTPS และเฉพาะ HTTPS เท่านั้น Web Bot Auth protocol draft อธิบายไดเรกทอรีนี้ว่าเป็น JSON Web Key Set (คีย์ในรูปแบบ JWK) ที่ให้บริการจาก http-message-signatures-directory well-known URI บน origin นั้น จากนั้นหาคีย์ที่ตัวระบุตรงกับพารามิเตอร์ 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;
}กฎปฏิบัติสำหรับการค้นหาคีย์:
- แคชคีย์ ด้วยอายุการเก็บที่จำกัด เพื่อให้การยืนยันไม่เพิ่ม network call ไปกับทุกคำขอ
- ตั้ง timeout ให้สั้น และถือว่าการดึงข้อมูลล้มเหลวเป็น "ยืนยันไม่ได้" ไม่ใช่ "ยืนยันแล้ว"
- จำกัด origin ที่คุณดึงข้อมูลจากได้ หากทำได้ หรืออย่างน้อยบล็อก private address ranges เพื่อหลีกเลี่ยง server-side request forgery เอกสารร่างของ protocol ระบุ server-side request forgery ไว้ในข้อพิจารณาด้านความปลอดภัยด้วยเหตุผลนี้โดยตรง
- ผูกตัวตนกับ origin ไม่ใช่กับชื่อแบบ free-text ใน user agent ตัวระบุที่คุณบันทึกคือ origin ของไดเรกทอรี
ขั้นที่ 5: สร้าง signature base ขึ้นใหม่
ผู้ลงนามไม่ได้ลงนามคำขอแบบ raw มันลงนามสตริงมาตรฐานที่เรียกว่า signature base (RFC 9421, Section 2.5) ซึ่งสร้างจากคอมโพเนนต์ที่ครอบคลุมตามลำดับ ตามด้วยบรรทัด @signature-params ตัวตรวจสอบของคุณจะสร้างสตริงเดียวกันขึ้นมาใหม่จากคำขอที่ได้รับ
สำหรับตัวอย่างข้างต้น base จะมีลักษณะดังนี้:
"@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 ถูกทำให้เป็นมาตรฐานตามที่ระบุ: ตัดช่องว่างส่วนเกิน และรวมค่าหลายรายการเข้าด้วยกัน ไลบรารีที่ดีจะจัดการสิ่งนี้ให้คุณ
ขั้นที่ 6: ยืนยันลายเซ็น
เมื่อมี base และ public key แล้ว การยืนยันเป็นเพียงการเรียกครั้งเดียว โดยทำตามขั้นตอนใน RFC 9421, Section 3.2 สำหรับ Ed25519 ใน Node.js โดยใช้ crypto module ในตัว:
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"));
}หากการตรวจสอบล้มเหลว คำขอนั้นยังไม่ถูกยืนยัน แต่มันอาจยังถูกให้บริการได้ เพียงแต่จะไม่ได้รับ basis แบบ "verified"
ขั้นที่ 7: ป้องกันการเล่นซ้ำ
ลายเซ็นที่ถูกต้องสามารถถูกจับและส่งใหม่ได้จนกว่าจะหมดอายุ RFC 9421, Section 7.2.2 ครอบคลุมการเล่นซ้ำของลายเซ็น และ draft ของโปรโตคอล Web Bot Auth อ้างอิงไปยังส่วนนี้แทนที่จะเพิ่มกฎ nonce ของตัวเอง การควบคุมสองอย่างนี้ปิดช่องว่างส่วนใหญ่ได้:
- หมดอายุสั้น บังคับในขั้นที่ 3
- ติดตาม nonce เมื่อมี
nonceอยู่ ให้เก็บมันพร้อมกับ key identifier จนกว่าลายเซ็นจะหมดอายุ แล้วปฏิเสธการใช้ซ้ำครั้งที่สอง
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 ไว้ใน shared store เช่น Redis โดยกำหนด time to live เท่ากับอายุที่เหลืออยู่
ขั้นที่ 8: บันทึกผลลัพธ์พร้อม basis ของมัน
การยืนยันสร้างหลักฐาน ไม่ใช่คำตัดสิน บันทึกมันในรูปแบบที่การวิเคราะห์และนโยบายของคุณนำไปใช้ได้:
const result = {
actor: "agent",
basis: "verified",
operator: "https://agent.example",
keyId: "kq1",
coveredComponents: ["@method", "@authority", "@path", "signature-agent"],
verifiedAt: new Date().toISOString(),
};เมื่อการยืนยันล้มเหลวหรือไม่สามารถทำงานได้ ให้บันทึกสิ่งนั้นด้วย พร้อมเหตุผล: ส่วนหัวหายไป อัลกอริทึมไม่รองรับ ไม่พบคีย์ หมดอายุ ถูกเล่นซ้ำ หรือ signature ไม่ตรงกัน เหตุผลเหล่านี้มีคุณค่า การพุ่งขึ้นของ "key not found" จากผู้ปฏิบัติการรายหนึ่งมักหมายความว่าพวกเขาหมุนเวียนคีย์ ไม่ใช่ว่าพวกเขากำลังโจมตีคุณ
ควรบันทึกอะไรจากการยืนยันลายเซ็น?
บันทึก origin ของผู้ปฏิบัติการ ตัวระบุคีย์ คอมโพเนนต์ที่ครอบคลุม พารามิเตอร์เวลา ผลลัพธ์ และเหตุผลที่ล้มเหลว อย่าบันทึกไบต์ของลายเซ็นทั้งหมดหรือเนื้อหาของ request body ทั้งหมด เก็บล็อกไว้นานพอที่จะศึกษารูปแบบ และสั้นพอที่จะเคารพข้อผูกพันด้านความเป็นส่วนตัวของคุณ
ลายเซ็นที่ยืนยันแล้วควรเปลี่ยนนโยบายของคุณอย่างไร?
เมื่อยืนยันคำขอได้แล้ว คุณสามารถปฏิบัติต่อคำขอเหล่านั้นแตกต่างออกไปได้อย่างมีความหมาย โพสต์ของเราเรื่อง designing app policies for humans and AI agents ครอบคลุมโมเดลนโยบายที่กว้างกว่า:
- อนุญาตผู้ปฏิบัติการที่ยืนยันแล้วให้เข้าถึงเส้นทางอ่านอย่างเดียว ด้วย rate limit ที่สูงกว่า automation ที่ไม่ยืนยัน
- เสนอคำตอบแบบมีโครงสร้างให้พวกเขา เช่น JSON หรือ Markdown แทนหน้า HTML แบบเต็ม
- เก็บการกระทำที่มีผลกระทบสูงไว้ภายใต้การอนุญาตตามปกติของคุณ ลายเซ็นที่ยืนยันแล้วบอกคุณว่าไคลเอนต์ใดกำลังเรียกอยู่ ไม่ได้บอกว่ามันอาจเปลี่ยนบัญชีได้หรือไม่
เริ่มในโหมดสังเกตการณ์ วัดว่าทราฟฟิกอัตโนมัติของคุณมีลายเซ็นมากน้อยเพียงใด มีผู้ปฏิบัติการรายใดปรากฏขึ้นบ้าง และการยืนยันล้มเหลวบ่อยแค่ไหน ก่อนที่นโยบายใดๆ จะพึ่งพามัน
Proof of Actor อยู่ตรงไหนในภาพรวมนี้
การยืนยันลายเซ็นเป็นเพียงแหล่งหลักฐานหนึ่งในหลายแหล่ง Proof of Actor ผสานมันเข้ากับสัญญาณอื่นและทำให้ basis มองเห็นได้ในทุกผลลัพธ์ เพื่อไม่ให้ agent ที่ยืนยันแล้วถูกสับสนกับตัวที่เพียงประกาศตัวเอง แอปพลิเคชันของคุณเป็นผู้ตัดสินว่าต้องเกิดอะไรขึ้นต่อไป
หากต้องการนำสิ่งนี้ไปใช้ในแอป production ร่วมกับทีมวิศวกรของเรา สมัครขอสิทธิ์เข้าถึงก่อนใคร หากต้องการภาพรวมเต็มของสิ่งที่หลักฐานสามารถและไม่สามารถแสดงได้ โปรดอ่าน Detection & Limitations
แหล่งอ้างอิง
- RFC 9421: HTTP Message Signatures: มาตรฐานสำหรับการลงนามและการยืนยัน รวมถึงพารามิเตอร์ของลายเซ็น signature base และข้อพิจารณาเรื่องการเล่นซ้ำ
- RFC 9651: Structured Field Values for HTTP: ไวยากรณ์ที่ใช้โดย
Signature-InputและSignature - IETF Web Bot Auth working group: ขอบเขตงานและสถานะของงานยืนยันตัวตนอัตโนมัติของไคลเอนต์
- draft-ietf-webbotauth-httpsig-protocol: Internet-Draft ที่กำหนด
Signature-Agentไดเรกทอรีคีย์แบบ JWKS และ well-known URI ของมัน (งานยังอยู่ระหว่างดำเนินการ ณ เดือนพฤศจิกายน 2026) - RFC 7517: JSON Web Key (JWK): รูปแบบคีย์ที่ใช้ในไดเรกทอรีคีย์ที่เผยแพร่
- RFC 8615: Well-Known URIs: วิธีที่เส้นทางแบบ well-known เช่นไดเรกทอรีคีย์ได้รับการลงทะเบียน
- RFC 8032: Edwards-Curve Digital Signature Algorithm (EdDSA): อัลกอริทึม Ed25519 ที่ใช้ในตัวอย่าง
- Node.js crypto documentation:
createPublicKeyและverifyที่ใช้ในขั้นที่ 6