Auth:HMAC + Principal Verification
資料邊界不是只有「資料分級」。你還要知道,是誰把資料寫進來的。
Hook
你幫 memhall 做了一個簡單寫入 API。
本來只打算給自己的 agent 用。
因為是內部工具,
你一開始圖快,
把 auth 做得很鬆。
dev 環境預設 principal 是 dev-local。
如果 request 沒帶 signature,
系統就自動套這個 principal。
你當時覺得沒差。
反正只是區網。
反正只有自己在用。
結果某天,
nginx 設定錯了。
這個 endpoint 被暴露到 LAN。
家裡同網段上的其他裝置,
甚至一個隨手裝的測試盒子,
都能直接打這個 API。
更糟的是,
它們不需要知道任何 key。
因為你的系統在沒有 signature 時,
還是會幫對方補一個 dev-local 身分。
這時你才發現,
自己做的不是「簡化 auth」。
而是幾乎沒 auth。
資料邊界到這一步,
開始跟身份驗證綁在一起。
因為你不只要知道資料是什麼,
還要知道:
是誰寫的、
誰要求讀、
誰有資格碰這個 namespace。
這堂課先不做大型 IAM。
我們先做一個工程上最小、
但夠硬的版本:
HMAC + principal verification
Learn
先把 principal 想清楚
在 agent / service world 裡,
principal 可以先用一句話理解:
這個 request 代表誰在做事?
它不一定是人。 很多時候, 它是某個 service、 某個 agent、 某個 pipeline worker。 例如:
agent:review-botservice:bookmark-syncworker:memory-consolidatoruser:maki
資料邊界要成立, 你不能只看 request body 的內容。 你還要知道 body 是誰送來的。 如果 sender 身分是假的, 後面所有 namespace、tier、ACL 規則都只是樣子。
為什麼先學 HMAC
身份驗證有很多做法。 為什麼這堂課先講 HMAC? 因為它對很多 service-to-service 寫入 API 來說, 有幾個很實際的優點:
- 標準函式庫就有
- 不需要額外 token issuer
- 簽章格式短
- 驗證邏輯容易 review
- 很適合內部服務之間共享 secret
HMAC 不是萬能。 它不是拿來解決所有 user auth。 但對於:
- 內部 API
- webhook-like ingestion
- agent 寫入記憶層
- pipeline worker 回報任務結果
它通常是很合理的第一步。
最小標頭格式
這堂課用的格式很簡單:
X-Signature: sha256=<hex_digest>
X-Timestamp: <unix_seconds>
X-Key-Id: <id>
X-Principal: <principal_name>
這裡每個欄位都有工作。
| Header | 作用 |
|---|---|
X-Signature | 真正的 HMAC 簽章 |
X-Timestamp | 防 replay,用來限制可接受時間窗 |
X-Key-Id | 支援 key rotation,知道該拿哪把 key 驗 |
X-Principal | 聲明誰在送這個 request |
注意一件事。
X-Principal 不是只靠宣告就算數。
它必須被一起簽進 HMAC 或跟 key 綁定,
否則對方可以亂填 principal 名字。
這堂課先用最小設計:
body 至少要跟 timestamp 一起簽。
如果你想再更硬,
可以把 principal 也一起簽進 canonical string。
標準簽法
最常見也最好 review 的 canonical string 是:
{timestamp}.{body}
對應的 Python 長這樣:
這段 code 沒有依賴外部套件。 review 起來也很直接。 你真正要關心的是: 有沒有把 timestamp 簽進去, 以及後面驗證時是不是用同一種 canonicalization。
規則一:Replay window
第一條規則很硬:
拒收
abs(now - timestamp) > 300的 request。
也就是五分鐘窗口。 這個數字不是神奇定律。 它是一個工程折衷:
- 足夠短,降低封包重放價值
- 又不至於讓正常時鐘誤差太容易失敗
很多人會犯兩種錯: 第一種, 完全不驗 timestamp。 這等於任何曾經截到的合法 request, 之後都能一再重放。 第二種, 驗了 timestamp, 但沒把 timestamp 簽進 HMAC。 這也不夠。 因為攻擊者可以直接把 timestamp 換成新的。 所以 replay 防護一定是成套的:
- request 帶 timestamp
- timestamp 包進簽章
- 伺服器檢查可接受窗口
規則二:Constant-time compare
第二條規則很多人知道名詞, 卻常常沒真的做到:
用
hmac.compare_digest(...),不要用==
原因不是語法偏好。 而是 timing attack。 如果你用一般字串比對, 某些實作會在前面幾個字元就提前退出。 攻擊者可能藉由回應時間差, 慢慢猜到正確簽章。 這類攻擊不一定每次都很容易打。 但防禦成本幾乎是零。 既然標準庫已經給你 constant-time compare, 就不要自己冒險。
規則三:簽章一定要包含 timestamp
這條前面提過, 但值得單獨再講一次。 有些團隊會寫成:
HMAC(body)
然後另外檢查 header 裡的 timestamp。 這種做法看起來像有兩層驗證。 實際上是把 replay 防護拆散了。 正確做法是:
hmac.new(key, f"{ts}.{body}".encode(), hashlib.sha256)
這樣 body 跟 timestamp 才是一體。 任何一項被改, signature 都會失效。
規則四:Key rotation
再來是很多 PoC 最愛跳過的部分: key rotation。 如果你的系統只有一把永不更換的 secret, 那不是簡單。 那是未來的事故債。 最小做法不難:
- 每把 key 都有
X-Key-Id - server 端維護多把 active key
- 新舊 key 重疊至少七天
- 寫入端先切新 key,觀察穩定後再移除舊 key
你不需要一開始就做超複雜 KMS 整合。 但你至少要讓 protocol 本身支援 rotation。 不然哪天 key 外洩, 你會發現連換 key 的路都沒有。
HMAC 跟 JWT 不是誰高級誰低級
很多人談 auth, 很容易變成工具崇拜。 這堂課不做那套。 先看兩者常見用途:
| 方案 | 適合場景 | 你要付出的複雜度 |
|---|---|---|
| HMAC | service-to-service、webhook、內部 pipeline | 共享 secret 管理 |
| JWT | user session、攜帶 claims、多方驗證 | issuer、expiry、claims 設計 |
HMAC 的優勢是:
- 短
- 直接
- stdlib only
JWT 的優勢是:
- 可以攜帶 claims
- 適合 user / federation 場景
- 可搭配公私鑰驗證
但如果你的問題只是: 「內部 agent 寫記憶層 API 時,怎麼證明它真的是自己人?」 那先上 HMAC 往往比較務實。
Fail-closed:不要 default to dev-local
這堂課最想你戒掉的習慣, 就是 fail-open auth。 像這種邏輯:
- 沒有 signature 也接受
- header 缺欄位就補預設
- principal 沒傳就用
dev-local
看起來很方便。 實際上只要環境變數一錯、 反向代理一漏、 測試 endpoint 一暴露, 整個系統就等於裸奔。 正確心態應該是:
沒有 signature,就 403。沒有 timestamp,就 403。沒有對應 key,就 403。
不是「讓開發方便」不重要。 而是這種方便, 通常會活得比你想像中久得多。 最後一路活進 production 邊上。
一個最小 verify flow
如果把整條驗證流程壓成 checklist, 大概是這樣:
- 取出
X-Signature、X-Timestamp、X-Key-Id - 缺任何一項就拒絕
- 檢查 timestamp 是否在五分鐘窗口內
- 依
X-Key-Id找對應 key - 用
f"{ts}.{body}"重算 expected signature - 用
hmac.compare_digest(...)比對 - 驗證 principal 是否允許做這個動作
注意最後一步。
簽章正確,
不代表授權正確。
它只能證明:
這個 request 來自持有該 key 的一方。
至於這個 principal 可不可以寫 shared,
可不可以碰 restricted,
那是下一層 policy 的事。
下一課講 namespace 時,
我們會把兩者接起來。
一段你可以直接照抄的最小觀念
如果你今天只帶走一句工程規則, 就帶這句:
驗證 sender 身分時,先求 fail-closed,再求功能完整。
很多 PoC 一開始做不好 auth, 不是因為不懂理論。 而是太想留後門給自己方便。 結果方便留給了攻擊面。
這一課你應該先記住的四件事
- principal 是 request 代表的身份,不一定是人
- HMAC 很適合內部 service-to-service 寫入 API
- timestamp、replay window、constant-time compare 缺一個都不算完整
- 沒有 signature 時要直接拒絕,不要默默套
dev-local
做到這裡, 你才有資格說: 這筆資料不只是分級了, 而且我知道是誰寫進來的。
AI 協作:學了這個,跟 AI 怎麼配合?
這個技能在 AI 協作中的定位,是讓你要求 AI 產生可 review 的最小驗簽程式,而不是一堆沒說清楚 canonical string 的 auth 包裝。 你的人類優勢:
- 只有你能決定哪一些 internal API 值得用 HMAC 先快速硬化,哪些其實需要完整 user auth 或更複雜的 claims。
- 只有你能判斷某個 principal 在你的流程裡是 service 身分、agent 身分、還是應該映射到人類責任人。
可以這樣跟 AI 說:
請幫我把這個內部寫入 API 改成 fail-closed 的 HMAC 驗簽流程。需求是:header 帶
X-Signature、X-Timestamp、X-Key-Id,簽法固定為f\"{ts}.{body}\",5 分鐘 replay window,用hmac.compare_digest,缺任何必要欄位就直接回 403。不要幫我偷偷保留dev-localfallback。
Do
互動示範
挑戰任務
請寫一個 verify_signature(headers, body, key, now) 函式,規則如下:1. X-Timestamp 與 now 相差超過 300 秒就回傳 False;2. 用 f"{ts}.{body}" 做 HMAC-SHA256;3. 用 hmac.compare_digest 比對;4. 驗證通過回傳 True。請用固定值測試兩次:now = 1700000000、body = "hello"、key = b"boundary-secret"。第一筆 headers 使用 X-Timestamp = "1700000000" 和正確 signature;第二筆只把 timestamp 改成 1699999000。最後分兩行印出驗證結果。