pqc_railsに「アルゴリズム抽象化レイヤー」を作った
2026.07.07 01:15:03
量子コンピュータ関連で一番喫緊の話はいまのところ暗号の話だと思う。量子コンピュータを使用することで現在の暗号が破られるというのはなかなか大変な話だ。Railsでアプリを作って遊ぼうと思ってるのに、ユーザーのパスワードが破られて乗っ取られるというのはあまり良い話ではない。ちょうど量子コンピュータに興味も湧いているところだったので、gemをつくることにした。liboqs-pythonを触ったのはそれの練習のようなものだ。
gemは pqc_rails と命名した。
gemをつくるに当たって、まずはどういうものにしたいかを定義した。
gemは pqc_rails と命名した。
gemをつくるに当たって、まずはどういうものにしたいかを定義した。
- 一行書くだけでPQC対応が完了する(複雑にしたくない)
- その前後でUXが変わらないようにする(速度を落としたくない)
まあこういうものがあればいいよね、というくらいのざっくりした目標だ。ともあれ、そういう方向で動き出した。
最初、pqc_railsの`Kem`や`Sig`は`"ML-KEM-512"`みたいなliboqsの生の文字列をそのまま受け取る作りだった。動くは動くんだけど、これをこのままRailsアプリ側に持ち込むのはまずいなと思っていた。
最初、pqc_railsの`Kem`や`Sig`は`"ML-KEM-512"`みたいなliboqsの生の文字列をそのまま受け取る作りだった。動くは動くんだけど、これをこのままRailsアプリ側に持ち込むのはまずいなと思っていた。
なぜまずいか。liboqsの文字列はliboqs側の都合で決まっている。Railsの開発者が`encrypts :ssn, algorithm: "ML-KEM-512"`って書くコードを将来書くことになったとき、liboqsが命名を変えたら(実際そういうことは起きる)、Railsアプリのコードまで巻き込まれる。これは「ゼロダウンタイムでPQCを導入する」っていうこのgemの看板と矛盾する。
## 4つの案を出して、3つを今回は捨てた
設計を考えるとき、思いついた案を全部実装するんじゃなくて、まず並べてみるようにしている。今回は4つ出した。
1. アルゴリズムレジストリ(シンボル→liboqs文字列のマッピング)
2. liboqsへの動的な問い合わせ(`OQS_KEM_alg_count`とかで「今のビルドで何が使えるか」を実行時に取る)
3. `Kem`と`Sig`の共通基底を抜くリファクタ(`ensure_not_freed!`とか地味に重複してる)
4. 1と3を両方やる
正直、2番目の「動的問い合わせ」は最初は筋が良さそうに見えた。ハードコードのレジストリってメンテコストかかるし、liboqsが実際にサポートしてるアルゴリズムを正確に知れるのは安全っぽい。
でも、ここで「最終的にActiveRecord::Encryptionに組み込む」というゴールに立ち返って考えると、違う結論になった。AR::Encryption側が欲しいのは「今のビルドで動くアルゴリズムの一覧」じゃなくて「Railsアプリの設定ファイルに書ける、安定したシンボル」だ。動的問い合わせは前者にしか答えてくれない。後者の代わりにはならない。これは典型的な「局所最適化」だなと気づいた。今のビルド環境に対しては正確だけど、もっと先の統合を考えると的を外している。
3番目(共通基底の抽出)はやりたい気持ちはあった。`Kem`と`Sig`、似たようなコードが重複してるのが見えてるとうずうずする。でも、これは内部品質の話で、外部APIは変わらない。「今やらなきゃいけないこと」ではない。次のサイクルに回すことにした。
で、1番(レジストリ)を選んだ。
## 実装は意外とシンプルだった
def resolve_kem_name(name) return name if name.is_a?(String) find_kem(name).liboqs_name end
これだけ。`String`が来たらそのまま素通し(liboqsの生の名前を直接渡す既存の柔軟性は壊さない)、`Symbol`が来たらレジストリで引く。`Kem`側は「シンボルかもしれない」を一切意識しなくていい。
レジストリ自体も、`Struct.new(..., keyword_init: true)`でちょっとしたデータの入れ物を作っただけ。
ml_kem_512: Algorithm.new( name: :ml_kem_512, liboqs_name: "ML-KEM-512", family: :kem, security_level: 1, status: :recommended )
ひとつ迷ったのは、鍵の長さ(公開鍵が何バイトか、とか)をレジストリに持たせるかどうか。最初は「メタデータとして持っておいたほうが便利そう」と思ったけど、よく考えるとliboqsから実行時に正確な値が取れる(`kem.length_public_key`みたいに)。二重管理する理由がない。レジストリの責務は「名前解決とメタデータ」に絞った。これくらいの判断は、手を動かす前に「これは本当に必要か」を一拍置いて考えるようにしている。
## status: :recommended というフィールドの含み
`status`フィールド、今は全部`:recommended`しか入ってない。一見「今使ってない属性を先に作るな」という原則に反してるように見えるかもしれない。
でも、これはAR::Encryptionが持ってる`primary_key`/`previous_keys`という鍵ローテーションの仕組みを意識していて。将来「ML-KEM-512からML-KEM-768への移行期間中は両方読めるようにする」みたいな話が出てきたとき、このフィールドが`:deprecated`を返せる場所になる。今は使わないけど、置き場所だけ作っておく、というのは過剰設計とは少し違う判断だと思っている(とはいえ、これが本当に正しい判断かはまだわからない。あとで実際に使うときに答えが出る)。