tenant-pass がウィンドウとパスワードをどう計算するかの仕様です。方式を検証したいときや、同じ計算を別の言語で書きたいときに使ってください。末尾のテストベクター(検証用の入力と正解の組)は、本体の Go の実装とは別に Python で書いた実装で作ったもので、両者の結果が一致することを確かめてあります。
表記
LP(x):xのバイト長を 4 バイトのビッグエンディアン整数で表し、その後ろにx(UTF-8)を続けたもの。BE64(n):nを符号付き 64 ビットのビッグエンディアン整数(2 の補数)で表したもの。||: バイト列の連結。A = LP(store) || LP(node) || LP(account)。storeはpostgres・mysql・redisのどれか、nodeは設定ファイルに書いたノード名、accountはアカウント名です。W: ウィンドウの長さ(秒)。設定のrotation.windowの値です。
ウィンドウのオフセットと番号
offset = uint64_be(SHA-256("tenant-pass/v1/offset" || A)[0:8]) mod W
n(t) = floor((unix(t) − offset) / W)
Start(n) = n·W + offset (Unix 時刻の秒)
End(n) = Start(n + 1)
オフセットのおかげで、ローテーションの時刻はアカウントごとにずれ、ウィンドウの中に均等に散らばります。オフセットの計算に鍵は使いません。
鍵の選び方
ローテーションキーには、それぞれ ID と not_before(使い始める時刻)があります。ウィンドウ n では、not_before ≤ Start(n) を満たす鍵のうち、not_before がいちばん新しい鍵を使います。条件を満たす鍵がなければ、そのウィンドウのパスワードはありません。
パスワードとソルト
選んだ鍵のシークレットを K、その ID を id とすると、パスワードとソルトは次のように計算します。
password(n) = base64url_nopad( HMAC-SHA256(K, "tenant-pass/v1/password" || A || LP(id) || BE64(W) || BE64(n)) ) → 43 文字
salt(n) = HMAC-SHA256(K, "tenant-pass/v1/salt" || A || LP(id) || BE64(W) || BE64(n)) → 32 バイト
ソルトは、tenant-pass がパスワードをハッシュしてからストアに渡すときに使います。使い方はストアごとに違います。
PostgreSQL では SCRAM-SHA-256 のベリファイアを作ります。ソルトは salt(n)[0:16]、反復回数は 4096 です。
MySQL(caching_sha2_password)では、salt(n) の先頭 20 バイトを 1 バイトずつ alphabet[b & 0x3f] で crypt の文字集合 ./0-9A-Za-z の文字に置き換え、それをソルトにします。ラウンド数は 5000 です。
Redis ではソルトを使いません。ACL に登録するハッシュは hex(SHA-256(password)) です。
返すウィンドウと更新の時刻
時刻 t にどのウィンドウのパスワードを返すか(target(t))と、クライアントに更新を促す時刻(refresh_at(n))は、次の式で決まります。renew_before は、ウィンドウが終わる何秒前から次のウィンドウのパスワードに切り替えるかを表します。
target(t) = n(t) + 1 if End(n(t)) − t ≤ renew_before
= n(t) otherwise
refresh_at(n) = End(n) − renew_before
テストベクター
入力の条件は次のとおりです。
- 鍵のシークレット: バイト列
00 01 02 … 1f(32 バイト) - 鍵 ID:
k1(not_beforeはどのウィンドウよりも前) W = 900- 時刻:
t = 2026-11-03T12:00:00Z(Unix 時刻 1793707200)
| ストア / ノード / アカウント | offset | n | Start | password(n) |
|---|---|---|---|---|
| postgres / pg1 / t_acme | 539 | 1993007 | 1793706839 | lRG6msAkg88kIIjJU-N6yWZ6wzx1gQj62GYG71hcgjE |
| mysql / my1 / t_acme | 601 | 1993007 | 1793706901 | e1J8ptbuAhJdMRKoN64HY-aPS_M_8qT5JBYJPCuixDg |
| redis / rd1 / t_acme | 86 | 1993007 | 1793706386 | 10MHE6qMKh6XzzFzHwmXBvgWoezTwvsUYb91z3guUHc |
各行のソルト(16 進)は次のとおりです。
- postgres:
abf9ab9a389f481fee4908dd172c9cccbadfec535752012f6409fd656d729d1a - mysql:
b810862d689e69fa9b9e7505734ead2377cc468d2c81eeb6f48c7fc2556b8034 - redis:
51ffc4190e3dc0774a7711bf75a433650b3d7d0235b8c879ec569274e11ff1b5
postgres の行に対応する PostgreSQL の SCRAM ベリファイアは次のとおりです。
SCRAM-SHA-256$4096:q/mrmjifSB/uSQjdFyyczA==$1vO+SFZOWRWMJtkvT14UZD23M4IOBqVfMP6JjpxyVnA=:LKeE/8nXPt7v7OoC/qRpjmb66Fd0fA5wclO1IK9ZOII=
redis の行に対応する Redis の ACL ハッシュは 181dc3a4def9e83202b8ec73eb06014c09a45f58e3637756857dd1a1b47b6278 です。
MySQL 用の SHA-crypt の計算は、OpenSSL(openssl passwd -5)が作るハッシュとも、実際の MySQL 8.4 サーバーが作るハッシュとも一致することをテストで確かめています。
セキュリティ上の性質
各要素の前に長さを付けているので、違う入力が同じ HMAC の入力にまとまってしまうことはありません。たとえば ("ab","c") と ("a","bc") は別物として扱われます。用途ごとに別のラベル(password・salt・offset)も付けているので、同じ鍵から作った値が用途をまたいで一致することもありません。
入力にはストアの種類、鍵 ID、ウィンドウの長さ W も入っています。その結果、次のことが言えます。
- PostgreSQL のノードと Redis のノードに同じ名前のアカウントがあっても、同じ鍵から互いに無関係なパスワードができる。
- 鍵の ID を変えると、その鍵から導出するパスワードはすべて変わる。
rotation.windowを変えても、過去のウィンドウと同じ番号のパスワードがもう一度使われることはない。
逆に言えば、K を持っている人なら誰でも、その鍵から導出するパスワードをすべて計算できます。ローテーションキーはそれに見合うだけ厳重に保護し、漏洩が疑われたら入れ替えてください。
この文書は、リポジトリにある docs/ja/derivation.md をもとにしています(英語版は docs/en/derivation.md)。