Signed Agent Cards:基於 JWS 防範智慧體名片偽造與投毒
當 AI 智慧體在公開網路上透過擷取 agent-card.json 檔案相互發現時,信任不能建立在「網路可信」的假設上。一張未簽名的名片,只要一次 DNS 欺騙或 CDN 設定失誤,就可能把自主系統引向惡意端點。在 A2A v1.0 中,簽章 Agent Card 用 JSON Web Signature 解決了這個問題。
為什麼未簽名名片存在安全隱患
在 A2A 協定規範 中,探索機制被設計得輕量、靜態且無需驗證:客戶端智慧體只需對 /.well-known/agent-card.json 發出一個簡單的 HTTP GET 請求。
這種簡潔性推動了生態的快速採用,同時也暴露出一個可以稱為 Agent Card 投毒 的攻擊面:
- 端點重新導向:入侵 DNS 記錄或邊緣代理的攻擊者可以改寫
supportedInterfaces[].url欄位,把敏感的提示詞酬載悄悄傳送給竊聽或冒充的伺服器; - 能力與提示詞注入:竄改
skills陣列或description欄位,惡意方可以誘使呼叫方智慧體把高權限的金融或管理類任務委派給一個未經審查的模型; - 身分冒充:沒有密碼學驗證,任何人都可以託管一張名片,宣稱
provider.organization是某家知名金融機構或軟體廠商。
為了讓多智慧體架構抵禦這些風險,a2aproject/A2A 程式庫維護的規範允許名片攜帶一個 signatures 陣列,其中的元素是 RFC 7515:JSON Web Signature (JWS) 物件(規範第 8.4 節)。
signatures 陣列的結構剖析
在 A2A v1.0 的 Agent Card 中,頂層的 signatures 屬性是一個 AgentCardSignature 物件陣列。允許有多個項目,金鑰輪換就是靠這一點實現的:輪換期間,名片同時帶有一個舊金鑰的簽章與一個新金鑰的簽章。
{
"name": "Treasury Reconciliation Agent",
"version": "1.2.0",
"supportedInterfaces": [
{
"url": "https://agents.acme-finance.com/a2a/v1",
"protocolBinding": "JSONRPC",
"protocolVersion": "1.0"
}
],
"signatures": [
{
"protected": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpPU0UiLCJraWQiOiJrZXktMSIsImprdSI6Imh0dHBzOi8vYWNtZS1maW5hbmNlLmNvbS8ud2VsbC1rbm93bi9qd2tzLmpzb24ifQ",
"signature": "QFdkNLNszlGj3z3u0YQGt_T9LixY3qtdQpZmsTdDHDe3fXV9y9-B3m2-XgCpzuhiLt8E0tV6HXoZKHv4GtHgKQ"
}
]
}
每個項目都具有扁平 JWS 的欄位,此外沒有其他必填項:
-
protected(必填):經 Base64URL 編碼的 JSON 物件,即 JWS 受保護標頭。它必須包含:alg:簽章演算法,如ES256或RS256;kid:金鑰 ID,用於識別簽章所用的金鑰;typ:應設為JOSE。
它還可以包含
jku,即存放公鑰的 JSON Web Key Set 的 URL。上面的範例解碼後是{"alg":"ES256","typ":"JOSE","kid":"key-1","jku":"https://acme-finance.com/.well-known/jwks.json"}。 -
signature(必填):經 Base64URL 編碼的簽章值; -
header(選填):未受保護標頭參數組成的 JSON 物件,不做 Base64URL 編碼。
使用 ES256 為 agent-card.json 產生簽章
被簽署的並不是對外提供的檔案本身,而是它的正規化形式。依照規範第 8.4.1 節,你需要:
- 移除
signatures欄位本身,避免循環相依; - 遵循 Protocol Buffer 的欄位存在性規則:捨棄取預設值的欄位(空陣列、空字串),除非它們是必填欄位,或是被明確設定的 optional 欄位;
- 用 JSON 正規化方案(RFC 8785) 對結果做正規化:物件鍵排序,無多餘空白。
JWS 簽署輸入就是 BASE64URL(受保護標頭) + "." + BASE64URL(正規化後的名片)。下面是一個依這些步驟實作、不依賴任何第三方套件的 Node.js 版本。對於一般名片,只需對鍵排序並使用 JSON.stringify 就足以得到 RFC 8785 形式;如果你的名片裡有特殊數字,請改用經過充分驗證的 JCS 函式庫。
import crypto from 'node:crypto';
import fs from 'node:fs';
// RFC 8785 (JCS): keys sorted by UTF-16 code units, no whitespace. JSON.stringify
// already serializes strings and numbers the way JCS requires.
const canonicalize = value =>
Array.isArray(value)
? `[${value.map(canonicalize).join(',')}]`
: value !== null && typeof value === 'object'
? `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${canonicalize(value[key])}`).join(',')}}`
: JSON.stringify(value);
// 1. Read the card and drop any existing signatures.
const card = JSON.parse(fs.readFileSync('agent-card.json', 'utf8'));
const { signatures, ...unsigned } = card;
// 2. Build the JWS protected header: alg, typ and kid are required.
const header = {
alg: 'ES256',
typ: 'JOSE',
kid: 'key-1',
jku: 'https://acme-finance.com/.well-known/jwks.json'
};
const protectedB64 = Buffer.from(JSON.stringify(header)).toString('base64url');
const payloadB64 = Buffer.from(canonicalize(unsigned)).toString('base64url');
// 3. Sign with your P-256 private key. A JWS ES256 signature is the raw R||S
// pair (RFC 7518 section 3.4), not Node's default DER encoding.
const signature = crypto.sign('sha256', Buffer.from(`${protectedB64}.${payloadB64}`), {
key: fs.readFileSync('private-key.pem'),
dsaEncoding: 'ieee-p1363'
}).toString('base64url');
// 4. Attach the signature to the card.
const signedCard = { ...unsigned, signatures: [{ protected: protectedB64, signature }] };
fs.writeFileSync('agent-card.signed.json', JSON.stringify(signedCard, null, 2));
console.log('Agent Card signed with ES256');
客戶端智慧體如何驗證簽章
客戶端智慧體從一個未經審查的對等方取得 Agent Card 時,驗證應當發生在檢查能力或呼叫介面之前。規範第 8.4.3 節給出了步驟:
- 擷取:從
signatures陣列中取出一個簽章。如果沒有,則依據你的安全策略,把該名片視為未驗證或直接拒絕; - 取得金鑰:取得
kid所指的公鑰,來源可以是jku處的 JWKS,也可以是已知提供者的受信任金鑰庫; - 重建酬載:從收到的名片中移除取預設值的屬性與
signatures欄位,然後用 RFC 8785 做正規化; - 驗證:用公鑰對
BASE64URL(protected) + "." + BASE64URL(正規化後的名片)驗證簽章。
除了機制本身,規範對信任問題的態度也很明確:客戶端應當在信任名片之前至少驗證一個簽章,公鑰應當透過 HTTPS 取得,已過期或已撤銷的金鑰不得用於驗證。允許存在多個簽章,這樣輪換金鑰時驗證不會中斷。
在此基礎上,還有兩條規範並未要求但值得採用的策略:拒絕低於你安全基線的演算法(none 永遠不可接受,對稱演算法同理),並檢查金鑰的來源與名片宣告的 provider.url 是否一致,避免某個組織的簽章為另一個組織的名義背書。
金鑰輪換與安全實踐清單
在正式環境中管理簽章 Agent Card 時,請遵循以下維運準則:
- 讓金鑰生命週期與部署解耦:用
kid為金鑰命名,並把金鑰發布在由jku指向的 JWKS(/.well-known/jwks.json)中,而不是寫死靜態金鑰。輪換時,先把新金鑰加入 JWKS,用兩把金鑰同時簽署一段時間,再下線舊金鑰; - 每次變更都重新簽署:簽章涵蓋整張名片。把簽署做成建置步驟,讓版本號變更與新簽章一起發布;
- 全程強制 HTTPS:
supportedInterfaces中列出的每個介面以及每個金鑰端點,都必須透過 HTTPS 提供,並使用有效憑證; - 讓網域與提供者保持一致:託管 Agent Card 的網域、
provider.url以及 JWKS 的來源應保持一致; - 發布前稽核:在把完成的名片發布到公開根路徑之前,先用 驗證器 檢查,並替換 產生器 輸出的任何佔位簽章。
常見問題解答
A2A v1.0 中 Agent Card 必須帶 JWS 簽章嗎?
不必須。規範規定 Agent Card 可以(MAY)被簽署,並且客戶端在名片帶有簽章時應當(SHOULD)至少驗證其中一個,再決定是否信任。對於暴露在公開網路或多租戶網路中的智慧體,仍然建議加上簽章,因為沒有簽章的話,客戶端無法判斷名片在傳輸途中是否被改動。
什麼是「Agent Card 投毒」?
指竄改智慧體的名片,讓呼叫方信任錯誤的對象:控制了 DNS 記錄、CDN 設定或遭入侵儲存桶的攻擊者,改寫名片,把介面端點重新導向到惡意基礎設施,或注入偽造的技能描述。
應該使用哪種簽章演算法?
規範的範例使用 ES256 與 RS256。ES256(基於 P-256 曲線與 SHA-256 的 ECDSA)是不錯的預設選擇:簽章短小,函式庫支援廣泛。切勿使用 none 演算法,也要避免 HS256 這類對稱演算法,因為客戶端是用公鑰來驗證 Agent Card 的。
公鑰應該託管在哪裡?
放在提供者自己網域下透過 HTTPS 提供的 JSON Web Key Set 中,常見路徑是 /.well-known/jwks.json。受保護標頭透過 kid 指明金鑰,並可以用 jku 指向該金鑰集;客戶端也可以改用已知提供者的受信任金鑰庫。
Agent Card 驗證器如何評估簽章?
只檢查格式。每個 signatures 項目需要一個宣告了 alg 與 kid 的 base64url 受保護標頭(typ 應為 JOSE),以及一個不是佔位符的 base64url 簽章。它不會驗證簽章本身,因為那需要簽署者的公鑰與名片的正規化形式;請用你的 SDK 或 JWS 函式庫來完成驗證。
參考資料
相關工具: Agent Card 校驗器
瀏覽更多指南
下一步
在正式部署前檢測您的 Agent Card 完整性。
驗證器會檢查名片結構,核對 JWS 簽章標頭中的 alg、kid 與 typ,並標記佔位簽章與未加密的介面端點。