feat(auth): bind the subscriber hash to its environment#79
Conversation
From the launch security audit (dodopayments#55): the MAC input was only the subscriber id, so isolation rested entirely on per-environment secrets. If two environments ever shared one (manual DB edit, restored dump), a hash would transfer between them. The hash becomes hex(HMAC-SHA256(secret, env_typeid || 0x00 || subscriber_id)), checked against both secret slots so it composes with rotation. The legacy unbound form stays accepted while CHIMELY_SUBSCRIBER_HASH_LEGACY_ACCEPT is true (the default), giving deployed backends a migration window. Docs updated to the bound form. Closes dodopayments#55
Greptile SummaryThis PR binds subscriber hashes to their environment. The main changes are:
Confidence Score: 5/5This looks safe to merge.
Important Files Changed
Reviews (1): Last reviewed commit: "feat(auth): bind the subscriber hash to ..." | Re-trigger Greptile |
|
Independently implemented this against the same issue and hit a few edge cases while testing that may apply here, since the core approach (same MAC input, same
|
Closes #55.
The subscriber hash was
hex(HMAC-SHA256(secret, subscriber_id))-- the environment was not part of the MAC input, so isolation rested entirely on per-environment server-minted secrets. If two environments ever shared a secret (manual DB edit, restored dump), a hash would transfer between them.What
hex(HMAC-SHA256(secret, env_typeid || 0x00 || subscriber_id)), following the formula sketched in the issue.env_typeidis the environment'senv_…TypeID -- immutable, and customer-visible in the same dashboard view as the secret. Verification checks the bound form against both secret slots, so it composes with the two-slot rotation.CHIMELY_SUBSCRIBER_HASH_LEGACY_ACCEPTistrue, which is the default -- this PR changes nothing for deployed backends. Operators flip it tofalseonce their backends compute the bound form; the project can flip the default at a scheduled minor. Both comparisons stay constant-time.info.description, and the self-hosting env table.packages/client/src/generated/api.d.tsregenerated from the yaml -- byte-identical, since types don't embed the info description.Decision points (flagging explicitly)
compute/verifyplus docs.truefor legacy accept keeps this merge-safe now and makes the strict flip a scheduled decision rather than a breaking surprise.Tests
New
redteam_hmac_env_bindingsuite:Verification
Full CI (fmt, clippy -D warnings, nextest against real Postgres/Redis, cargo-deny, docker build) green on my fork before opening this PR. No SQL changes, so the
.sqlxcache is untouched.