從 User-Agent 到簽章:拆開 Web Bot Auth 怎麼證明爬蟲身分
你架的站收到一個請求,User-Agent 上面寫著 ClaudeBot。你怎麼知道那真的是 ClaudeBot?
答案是你不知道。那行字是對方自己打上去的,一行 curl 就能填成任何值。robots.txt 那套「我叫某某某,請照規則對待我」的協議從第一天就建立在自律上:站方寫規則,bot 自己認領身分,中間沒有一步在驗證。IETF 的 use-cases 草案(draft-nottingham-webbotauth-use-cases-02,2026 年 4 月 1 日,11 頁)講得比我直白,談 User-Agent 那節寫「trivially spoofed」,談 robots.txt 比對那節又說一次「easily spoofable on the wire」。
第二條路是看 IP。可以,但同一份草案 §2.1 寫了代價:封掉一個 IP,會連帶封掉那個位址當下以及往後的所有用途。雲端 IP 是輪流用的,你今天封的那個,下週可能在某個真人手機上。
第三條是發共用 API key。這條的問題不用草案講你也知道。
IETF 那個叫 webbotauth 的工作組,章程狀態是 Approved、最後更新 2025 年 10 月 23 日,開頭就把這三種做法點名,說「現有解法(例如 IP allowlisting、User-Agent strings、shared API keys)在安全性、擴展性與可管理性上都有明顯限制」。這句話是章程裡的問題陳述。工作組要做的,就是把「請求方是誰」從一句宣告換成一個可以算的東西。
這份草案五天前才進工作組
從 RFC 到工作組文件
2024 年 2 月 RFC 9421 發布
HTTP Message Signatures,Standards Track,正式 RFC。後面所有東西都長在它上面。
2026 年 3 月 2 日 架構草案走到 -05
draft-meunier-web-bot-auth-architecture 在 datatracker 上標的是 Expired & archived,被後面那份取代。
2026 年 8 月 19 日 合併成 draft-meunier-webbotauth-httpsig-protocol-02
架構草案跟金鑰目錄草案併成一份。狀態 Expired,還是個人投稿。
2026 年 9 月 1 日 draft-ietf-webbotauth-httpsig-protocol-00
名字裡多了 ietf,代表工作組採納了。Active、Intended status 是 Standards Track、2027 年 3 月 5 日到期、44 頁,作者是 Cloudflare 的 Thibault Meunier 跟 Google 的 Sandor Major。
我寫這篇的時候,這份 -00 進工作組才五天。它是 WG draft,不是 RFC,Intended status 只是說打算走 Standards Track。工作組列的九份相關文件裡,正式採納的就這一份。
簽一個 HTTP 請求,到底簽了什麼
那個「可以算的東西」,拆開來是這樣:對方拿私鑰對這個請求的某幾個部分做簽章,你拿他的公鑰去驗。驗過了,代表發這個請求的人握著那把私鑰。這件事沒辦法用嘴巴宣稱。
底層規格是時間軸第一格那份 RFC 9421:HTTP Message Signatures,2024 年 2 月發布,Standards Track,已經是正式 RFC。它定義三個 header field:Signature-Input、Signature、Accept-Signature。前兩個是這篇的主角。
草案裡的範例長這樣:
1 | GET /path/to/resource HTTP/1.1 |
(行尾的 \ 是草案排版的換行記號,不是協定的一部分。)
Signature 放算出來的結果,Signature-Input 放「這個結果是怎麼算出來的」。這跟寄掛號信有點像。User-Agent 是信封角落手寫的寄件人姓名,寫誰都行;簽章是把信裡指定的那幾張紙壓一個蠟封,哪幾張紙、怎麼壓都寫在 Signature-Input 裡,收信的人照著重壓一次,對得上才算數。
四個必填參數,跟一個不簽就會被掉包的 header
分號後面那串是簽章參數。RFC 9421 定義的全集有六個:created、expires、nonce、alg、keyid、tag。Web Bot Auth 沒有發明新參數,它做的是收緊,其中四個變必填。
created跟expires撐起有效期。keyid的要求最有意思:規格寫的是 MUST be a base64url JWK SHA-256 Thumbprint,那是公鑰自己算出來的指紋,不能隨便取,對不對得上你自己算一次就知道。tag必須是字串web-bot-auth,讓站方一眼分得出這組簽章的用途。- 被簽進去的東西叫 covered components,規格要求至少要有
@authority或@target-uri其中之一。
範例裡括號內那一串就是 covered components。以 @ 開頭的這種叫 derived component,RFC 9421 的全集是九個(@method、@target-uri、@authority、@scheme、@request-target、@path、@query、@query-param,加上只給回應用的 @status),草案另外建議簽 @method、@path、@target-uri、@query-param,但不強制。
真正關鍵的是另一條:對應那個簽章 label 的 Signature-Agent member,MUST be signed as a component。原因很直接,Signature-Agent 指的是「去哪裡拿我的公鑰」,它要是沒被簽進去,任何中間人都能把它換成自己的目錄,再配上自己算的簽章。
整條驗證鏈會變成一場自問自答。
公鑰要放在哪,網站才找得到
Signature-Agent 在最新的草案裡是 Dictionary Structured Header(RFC 9651 §3.2),member value 必須是含 URI 的 String Item,scheme 必須是 https。member 的 key 要跟簽章的 label 對上(範例裡兩邊都是 sig),一個請求掛好幾組簽章時才分得開。
值後面可以帶一個 type 參數,決定金鑰怎麼找。官方定義的值只有三個:directory 是預設,去那個網域的 well-known URI 抓金鑰目錄;jwks_uri 是把那個值直接當成 JWK Set 的網址;cimd 是把它當成 Client ID Metadata Document 的網址,再從文件裡的 jwks 或 jwks_uri 取金鑰。規格明寫「When type is absent, its value is directory.」
三條路最後都落到同一種東西上,一份 JWKS。走 directory 那條長這樣:
1 | GET /.well-known/http-message-signatures-directory HTTP/1.1 |
kid 就是前面那個 thumbprint,是把簽章跟公鑰接起來的那顆螺絲:站方讀到 keyid,來目錄裡找 kid 一樣的那把。nbf 跟 exp 給輪替用。這份資源必須用 200 回應,媒體型別是 application/http-message-signatures-directory+json,只走 HTTPS。
演算法規格刻意不指定,只要求 alg 是 IANA 那份 HTTP Signature Algorithms 清單裡註冊過的,草案的測試向量用了 RSA-PSS-SHA512 跟 Ed25519。這裡有個容易記混的點:Cloudflare 的實作只吃 Ed25519,那是它的實作限制,不是規格限制。
往 OAuth 那邊看一眼
三個 type 裡面,cimd 最值得停一下。它指向的是 draft-ietf-oauth-client-id-metadata-document,OAuth 那邊的草案。
同一個模式正在兩條完全不同的線上被採用:拿一個 https URL 當身分,公鑰跟中繼資料放在那個 URL 底下,不預先註冊。MCP server 認 client 在用它,網站認爬蟲也在用它。一邊問「這個 client 能不能拿 token」,一邊問「這隻爬蟲是誰」,題目差很遠,答案的形狀一樣。
再往上還有一層。簽章只證明「是同一把金鑰」,不說明這隻 bot 想做什麼。那層在另一份個人投稿 draft-meunier-webbotauth-registry-03 裡,2026 年 6 月 26 日的版本,26 頁,叫 Signature Agent card,是 bot 自己發布來描述自己的 JSON。欄位一半沿用 OAuth Client Metadata,另一半掛在 web_bot_auth 物件底下,有 purpose、rate-expectation、known-urls,還有一個叫 rfc9309-compliance。
RFC 9309 就是 robots.txt 的 RFC。這個欄位讓 bot 宣告自己遵守 robots.txt 的哪幾條指令,而這份宣告掛在一個可以驗證的身分底下。繞了一大圈又回到 robots.txt,差別是這次宣告的人跑不掉。
部署跑在規格前面
Cloudflare 的 Verified Bots policy 把驗證方式寫成兩種,就只有兩種:Web Bot Auth 跟 IP validation。它的操作文件教你產 Ed25519 金鑰、架金鑰目錄、到後台登記網址;那份目錄回應本身也要簽,tag 填 http-message-signatures-directory。而文件裡示範的 Signature-Agent 仍是舊的字串形式,草案 -00 已經改成 Dictionary、舊形式只為相容保留。規格前幾天才進工作組、實作還停在前一版,這個落差很正常。
參考實作在 cloudflare/web-bot-auth,Apache-2.0,我查的時候 156 顆星、最後一次 push 是 2026 年 9 月 5 日;文件另外提醒,Signature-Agent 的值忘了加雙引號或填非 HTTPS 網址,請求就會失敗。
規格自己寫下的限制
Security Considerations 那節有十一個小標,其中一條把很多人的期待直接劃掉了。§6.11 談沒簽章的請求,原文是「Absence of a signal is not evidence about the party that did not send it.」規格明說它既不要求、也不提供理由去區別對待未簽章的請求。
翻成白話:這套機制擋不住假裝成一般瀏覽器的惡意爬蟲。它解的是「誠實的一方怎麼證明自己誠實」,認出壞人它不管。回到那封掛號信,蠟封證明得了信是誰寄的,但證明不了沒蓋蠟封的那一疊信裡有沒有詐騙。你原本用來擋惡意流量的那套,一樣都不能拆。
另外三條比較實務。§6.9 說簽章不是長效通行證,不可以把預先算好的簽章當成 long-lived access credential。§6.6 說中間層不可以把簽章 header 剝掉;中介者若改動被簽進去的 Signature-Agent member,就得重算簽章。
§6.10 最陰險:目錄抓得到但裡面沒有那把金鑰,代表金鑰被換掉了;抓失敗則什麼都不代表,規格明寫這時候 must not evict a cached entry。兩種狀況在日誌裡長得一模一樣,該做的事卻相反。
Privacy Considerations 那節也很坦白:這個機制的前提就是主動自我識別,會減少匿名性。規格建議金鑰代表一個角色而不是某個自然人、支援輪替、目錄伺服器不要記錄可識別個人的資料。這個張力另有一份草案在處理:draft-rescorla-anonymous-webbotauth-01,2026 年 7 月 19 日,16 頁,還是個人投稿。
我的判斷,跟會讓我收回的條件
如果你在寫會去抓別人網站的東西,現在就值得把金鑰目錄架起來。走簽章這條,機器換到哪都是同一把金鑰;走 IP 驗證等於綁死出口 IP,換機器、上 serverless、跟別人共用出口都會出事,use-cases 草案 §3.1 跟 §3.2 兩個小標就叫 IP Address Mobility 跟 Sharing IP Addresses。
如果你是站方,先做的只有一件事:確認你的 WAF 或代理沒有把 Signature-Agent、Signature-Input、Signature 這三個 header 吃掉。這是整套裡最容易靜默壞掉的一環,不會有錯誤訊息告訴你。
會讓我收回這個建議的條件也寫清楚。章程列了三項交付,工作組到目前只採納 httpsig-protocol 一份,第二項(用既有廣泛使用的識別碼傳達更多 bot 資訊)跟第三項(金鑰生命週期與營運指南)都還沒有對應的 WG 文件。要是第二項最後走出一條不靠簽章的路,或者 Signature-Agent 的形式再翻一次版,這個建議就得往後挪。
講一下這篇的邊界:我只讀了規格草案跟 Cloudflare 的文件,沒有自己送過一個簽過名的請求,也沒有跑 openssl 產過金鑰。Anthropic 官方的爬蟲說明頁列了 ClaudeBot、Claude-User、Claude-SearchBot 三隻,驗證方式寫的是 IP 清單,那頁沒有提到 Web Bot Auth。Claude Code 的 WebFetch 這類本機工具送出的請求有沒有帶簽章,我查不到官方說法,所以不猜。
回到最開始那個問題
「你怎麼知道那真的是 ClaudeBot」,拆到最底下,跟 bot 沒什麼關係。它問的是一句宣告要怎麼變成一個可以驗算的東西。
答案的形狀在很多地方都一樣:身分綁在一個 https URL 上,公鑰放在那個 URL 底下的固定路徑,簽章裡放公鑰的指紋當索引,然後把「去哪裡拿公鑰」這件事本身也一起簽進去。最後那一步最容易被省略,省略了就全盤皆輸。哪天在別的協定裡看到類似形狀,先找找它有沒有做這件事。










