跳到主要內容
邊界實驗室 · Boundary Lab
正在啟動 Python 環境(首次約 15 秒)...

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-bot
  • service:bookmark-sync
  • worker:memory-consolidator
  • user: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 防護一定是成套的:

  1. request 帶 timestamp
  2. timestamp 包進簽章
  3. 伺服器檢查可接受窗口

規則二: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, 那不是簡單。 那是未來的事故債。 最小做法不難:

  1. 每把 key 都有 X-Key-Id
  2. server 端維護多把 active key
  3. 新舊 key 重疊至少七天
  4. 寫入端先切新 key,觀察穩定後再移除舊 key

你不需要一開始就做超複雜 KMS 整合。 但你至少要讓 protocol 本身支援 rotation。 不然哪天 key 外洩, 你會發現連換 key 的路都沒有。

HMAC 跟 JWT 不是誰高級誰低級

很多人談 auth, 很容易變成工具崇拜。 這堂課不做那套。 先看兩者常見用途:

方案適合場景你要付出的複雜度
HMACservice-to-service、webhook、內部 pipeline共享 secret 管理
JWTuser 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, 大概是這樣:

  1. 取出 X-SignatureX-TimestampX-Key-Id
  2. 缺任何一項就拒絕
  3. 檢查 timestamp 是否在五分鐘窗口內
  4. X-Key-Id 找對應 key
  5. f"{ts}.{body}" 重算 expected signature
  6. hmac.compare_digest(...) 比對
  7. 驗證 principal 是否允許做這個動作

注意最後一步。 簽章正確, 不代表授權正確。 它只能證明: 這個 request 來自持有該 key 的一方。 至於這個 principal 可不可以寫 shared, 可不可以碰 restricted, 那是下一層 policy 的事。 下一課講 namespace 時, 我們會把兩者接起來。

一段你可以直接照抄的最小觀念

如果你今天只帶走一句工程規則, 就帶這句:

驗證 sender 身分時,先求 fail-closed,再求功能完整。

很多 PoC 一開始做不好 auth, 不是因為不懂理論。 而是太想留後門給自己方便。 結果方便留給了攻擊面。

這一課你應該先記住的四件事

  1. principal 是 request 代表的身份,不一定是人
  2. HMAC 很適合內部 service-to-service 寫入 API
  3. timestamp、replay window、constant-time compare 缺一個都不算完整
  4. 沒有 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-SignatureX-TimestampX-Key-Id,簽法固定為 f\"{ts}.{body}\",5 分鐘 replay window,用 hmac.compare_digest,缺任何必要欄位就直接回 403。不要幫我偷偷保留 dev-local fallback。

Do

互動示範

DEMO 1可以修改程式碼試玩

挑戰任務

Task 1

請寫一個 verify_signature(headers, body, key, now) 函式,規則如下:1. X-Timestampnow 相差超過 300 秒就回傳 False;2. 用 f"{ts}.{body}" 做 HMAC-SHA256;3. 用 hmac.compare_digest 比對;4. 驗證通過回傳 True。請用固定值測試兩次:now = 1700000000body = "hello"key = b"boundary-secret"。第一筆 headers 使用 X-Timestamp = "1700000000" 和正確 signature;第二筆只把 timestamp 改成 1699999000。最後分兩行印出驗證結果。

BackNext Lesson →