簡易実装から本番実装へ:Pythonで学ぶML-KEMとliboqs
はじめに
私はPQC(耐量子計算機暗号)やQKD(量子鍵配送)などの最先端暗号技術に強い関心を持つ大学3年生です。将来的には大学院で研究を深め、より安全なデジタル社会の構築に貢献したいと考えています。独学で得た知識を体系的に整理し、分かりやすく情報発信することを目標にしています。
以前の記事である「ML-KEMをPythonで簡易実装試みる!?」では、ML-KEMを簡易パラメータでPython実装することを試みました。しかし、簡易化の影響で、暗号としての肝である「共通鍵の完全一致」を達成できませんでした。
本記事では、この課題を乗り越え、PQCアルゴリズムのオープンソースCライブラリであるliboqs(およびそのPythonバインディングであるliboqs-python)を使用して、ML-KEMの鍵交換処理を確実に動作させることを目指します。これにより、理論と実践のギャップを埋め、ML-KEMの理解を深めます。
※補足:liboqsは、OpenSSL 3.x以降でPQCを導入する際に、OQS Providerを通じて利用される重要なPQCアルゴリズム実装モジュールです。
前提
本記事では、ML-KEMの鍵生成、カプセル化、デカプセル化といった手順について基本的な知識があることを前提として進めます。ML-KEMやK-PKEの理論について詳しく知りたい方は、先に以下の記事をご覧ください。
ML-KEMアルゴリズムの概要(再確認)
ML-KEMは、詳細な数理計算を一旦脇に置くと、以下の流れで整理できます。
-
鍵生成
- K-PKE鍵生成アルゴリズムを使用して、共通鍵を安全に受け取るためのカプセル化鍵(公開鍵)とデカプセル化鍵(秘密鍵)を生成します。
-
カプセル化(Encapsulation)
- 送信者は、受信者のカプセル化鍵(公開鍵)を使用し、共通鍵と暗号文を生成します。
- 送信者は受信者に暗号文のみを送信します。
-
デカプセル化(Decapsulation)
- 受信者は送られてきた暗号文を秘密鍵で復元し、共通鍵を取り出します。
- このとき、暗号文が途中で改ざんされていないかを検証し、安全性が確認できた場合のみ、その共通鍵を有効にします。一致しなかった場合は、ランダム値を代替として共通鍵にします。(IND-CCA安全の実現)
環境構築
PQCアルゴリズムのCライブラリであるliboqsをPythonから利用するための準備をします。具体的には、下記のGitHubリポジトリを使用します。
インストール手順(Mac)
-
liboqs
のビルド、インストール
最新のリビジョンのみをクローンし、ビルド、インストールを実行します。最後のコマンドで、UNIX系システムではsudo
を付けて実行する必要がある可能性があります。
git clone --depth=1 https://github.com/open-quantum-safe/liboqs
cmake -S liboqs -B liboqs/build -DBUILD_SHARED_LIBS=ON
cmake --build liboqs/build --parallel 8
cmake --build liboqs/build --target install
-
liboqs-python
のインストール- 仮想環境の作成と有効化
Python仮想環境を作成し、有効化します。python3 -m venv venv . venv/bin/activate python3 -m ensurepip --upgrade
- ラッパーの設定とインストール
Pythonラッパーliboqs-python
のソースコードをクローンし、liboqs-python
をインストールします。git clone --depth=1 https://github.com/open-quantum-safe/liboqs-python cd liboqs-python pip install .
- 仮想環境の作成と有効化
- サンプルの実行
インストールが完了したら、以下のコマンドで機能を確認してください。python3 liboqs-python/examples/kem.py python3 liboqs-python/examples/sig.py python3 liboqs-python/examples/stfl_sig.py python3 liboqs-python/examples/rand.py
インストール後に、liboqsとliboqs-pythonの二つのディレクトリができていたら成功です。
├── liboqs # C言語実装ライブラリ本体(PQCアルゴリズムの実装を含む)
├── liboqs-python # Pythonバインディング(内部でliboqsを呼び出す)
liboqs-pythonの役割
liboqs-python
は、Open Quantum Safe(OQS)プロジェクトが提供するPythonバインディングで、C言語実装のライブラリ liboqs の機能を、Pythonから簡単に利用できるようにしてくれます。具体的には、liboqs-python
は、実行時にliboqs
の共有ライブラリ(.so/.dylib/.dll)を読み込んでCの関数を呼び出せるようにしています。
- Python メソッドと C関数の対応関係
Python | C liboqs |
---|---|
generate_keypair() |
OQS_KEM_keypair |
encap_secret() |
OQS_KEM_encaps |
decap_secret() |
OQS_KEM_decaps |
free() |
OQS_KEM_free |
つまり、Python 側で kem.generate_keypair()
と書けば、内部でOQS_KEM_keypair(...)
が呼ばれて、liboqs の実装が動きます。
テストコードを実行してみる
まずPythonでML-KEMを実際に動かすテストコードを以下に示します。ここでは、ML-KEM-512を実装しています。これはliboqs-python/examples/kem.py
の公式サンプルコードに基づいています。
test_pqc.py
は好みの場所に作成してください。
(私は、liboqs
とliboqs-python
と同じ階層に作りました。)
テストコード
import oqs
def main():
ALG_NAME = "ML-KEM-512"
# 1. Aliceのセットアップ (withブロックで安全にリソースを管理)
with oqs.KeyEncapsulation(ALG_NAME) as alice:
print(f"--- Algorithm: {ALG_NAME} (NIST PQC Level 1) ---")
# 2. 鍵ペア生成 (KeyGen)
# # Pythonのラッパーが、liboqs C言語の OQS_KEM_keypair() を呼び出す。
public_key = alice.generate_keypair()
# [データサイズの表示]
pk_len, sk_len = len(public_key), len(alice.export_secret_key())
print(f" > 公開鍵 (ek) サイズ: {pk_len} bytes")
print(f" > 秘密鍵 (dk) サイズ: {sk_len} bytes")
# --- 公開鍵の送信(Alice -> Bob) ---
# 3. Bob(鍵をカプセル化する側)の処理(鍵カプセル化 (Encaps))
with oqs.KeyEncapsulation(ALG_NAME) as bob:
# C言語の OQS_KEM_encaps() が実行され、格子上の暗号文(ct)と共通鍵(ss)を生成。
ciphertext, shared_secret_bob = bob.encap_secret(public_key)
print(f" > 暗号文 (ct) サイズ: {len(ciphertext)} bytes")
print(f" > Bobの共有秘密鍵 (ss_b) [先頭8B]: {shared_secret_bob[:8].hex()}...")
# --- 暗号文 (ct) の送信(Bob -> Alice) ---
# 4. Aliceの処理(秘密鍵を使って鍵復元 (Decaps))
# C言語の OQS_KEM_decaps() が実行され、秘密鍵と暗号文から共通鍵を復元。
shared_secret_alice = alice.decap_secret(ciphertext)
print(f" > Aliceの復元鍵 (ss_a) [先頭8B]: {shared_secret_alice[:8].hex()}...")
# 4. 鍵の一致確認
is_match = shared_secret_alice == shared_secret_bob
print("\n=== 最終検証: 共有秘密鍵の一致確認 ===")
print(f"結果: {is_match}")
print("SUCCESS: 共有秘密鍵が正常に確立されました。" if is_match else "FAILURE")
if __name__ == "__main__":
main()
実行方法:
python3 [test_pqc.pyのパス]
期待される出力
--- Algorithm: ML-KEM-512 (NIST PQC Level 1) ---
> 公開鍵 (ek) サイズ: 800 bytes
> 秘密鍵 (dk) サイズ: 1632 bytes
> 暗号文 (ct) サイズ: 768 bytes
> Bobの共有秘密鍵 (ss_b) [先頭8B]: 5cc98e193723ea33...
> Aliceの復元鍵 (ss_a) [先頭8B]: 5cc98e193723ea33...
=== 最終検証: 共有秘密鍵の一致確認 ===
結果: True
SUCCESS: 共有秘密鍵が正常に確立されました。
※エラーが出る場合は、Pythonの仮想環境で実行しているか確認してください。
【実践】ML-KEMの基本的な流れを理解する
ここからは、テストコードを使って実際の処理の流れを確認していきます。C言語での内部処理の詳細に興味がある方は、併せて「Pythonから追う ML-KEM の鍵生成と暗号処理の内部実装」もご覧ください。
-
初期化とリソース管理:
with oqs.KeyEncapsulation(...)
# 1. Aliceのセットアップ (withブロックで安全にリソースを管理) with oqs.KeyEncapsulation(ALG_NAME) as alice: # ...
-
内部で起きていること
-
liboqs-python/oqs/oqs.py
で定義されているKeyEncapsulation
クラスのインスタンスを生成します。 - インスタンス生成時に、
KeyEncapsulation
クラスの__init__
メソッドが実行され、C言語のOQS_KEM_new
が呼び出されます。この呼び出しによって、C言語コアとの接続を確立し、その接続を通じてアルゴリズムの仕様(鍵サイズなど)をPython側に正確に持ち込む役割を担っています。この初期化によって、KeyEncapsulation
クラス内で定義されているgenerate_keypair()
などのメソッドがC言語で定義されている機能を利用できるようになります。 - また、
with
ブロックを抜ける際には、C言語メモリの安全な解放を行うOQS_KEM_free
関数が自動的に呼び出されます。
-
-
内部で起きていること
-
鍵ペア生成:
alice.generate_keypair()
# 2. 鍵ペア生成 (KeyGen) # Pythonのラッパーによって、liboqs C言語の OQS_KEM_keypair() が実行される。 public_key = alice.generate_keypair()
-
内部で起きていること
-
KeyEncapsulationクラス
で定義されているgenerate_keypair()
関数を実行します。 - 関数内では、インスタンス時にC言語コアから取得した鍵バイト数分の空の領域(バッファ)を確保します(
public_key
、self.secret_key
)。その後、C言語のOQS_KEM_keypair
が呼び出され、Cライブラリ内で鍵ペアが生成されます。 - 成功した場合は公開鍵をPythonのbytesとして返されます。
-
-
内部で起きていること
-
鍵カプセル化:
bob.encap_secret(public_key)
# 3. Bobによる鍵カプセル化処理 with oqs.KeyEncapsulation(ALG_NAME) as bob: # C言語レベルでは OQS_KEM_encaps() が実行される # 暗号文 (ciphertext) と共通鍵 (shared_secret_bob) を生成 ciphertext, shared_secret_bob = bob.encap_secret(public_key)
-
内部で起きていること
-
KeyEncapsulationクラス
で定義されているencap_secret()
関数を実行します。 -
関数内では、公開鍵をC言語で扱えるように変換後、暗号文
ciphertext
と共通鍵shared_secret_bob
を格納するための空メモリ領域を確保します。その後、C言語のOQS_KEM_encaps
が呼び出され、Cライブラリ内で共通鍵が生成されます。 -
成功した場合は暗号文と共通鍵がPythonのbyteとして返されます。
-
-
内部で起きていること
-
鍵デカプセル化:
alice.decap_secret(ciphertext)
# 4. Aliceの処理(秘密鍵を使って鍵復元 (Decaps)) # C言語の OQS_KEM_decaps() が実行され、秘密鍵と暗号文から共通鍵を復元。 shared_secret_alice = alice.decap_secret(ciphertext)
-
内部で起きていること
-
KeyEncapsulationクラス
で定義されているdecap_secret()
関数を実行します。 - 関数内では、暗号文
ciphertext
をC言語で扱えるように変換後、共有鍵shared_secret_alice
を格納するための空メモリ領域を確保します。その後、C言語のOQS_KEM_decaps
が呼び出され、Cライブラリ内で共通鍵が復元されます。 - 成功した場合は、共有鍵がPythonのbyteとして返され、失敗された場合は例外を投げます。
-
-
内部で起きていること
暗号パラメータ比較
ML-KEMにおける暗号パラメータは以下の通りです。テストコードでは、ML-KEM-512を採用しましたが、以下にそれぞれの結果を示します。公開鍵、秘密鍵、暗号文のバイト数に注目してください。
パラメータ | n | q | k | 公開鍵(カプセル化鍵) (Bytes) | 秘密鍵(デカプセル化鍵) (Bytes) | 暗号文 (Bytes) | 共有秘密鍵 (Bytes) | 安全性レベル |
---|---|---|---|---|---|---|---|---|
ML-KEM-512 | 256 | 3329 | 2 | 800 | 1,632 | 768 | 32 | レベル1 |
ML-KEM-768 | 256 | 3329 | 3 | 1,184 | 2,400 | 1,088 | 32 | レベル3 |
ML-KEM-1024 | 256 | 3329 | 4 | 1,568 | 3,168 | 1,568 | 32 | レベル5 |
ML-KEM-768
--- Algorithm: ML-KEM-768 (NIST PQC Level 3) ---
> 公開鍵 (ek) サイズ: 1184 bytes
> 秘密鍵 (dk) サイズ: 2400 bytes
> 暗号文 (ct) サイズ: 1088 bytes
> Bobの共有秘密鍵 (ss_b) [先頭8B]: 428ece8e0f72fed6...
> Aliceの復元鍵 (ss_a) [先頭8B]: 428ece8e0f72fed6...
=== 最終検証: 共有秘密鍵の一致確認 ===
結果: True
SUCCESS: 共有秘密鍵が正常に確立されました。
ML-KEM-1024
--- Algorithm: ML-KEM-1024 (NIST PQC Level 5) ---
> 公開鍵 (ek) サイズ: 1568 bytes
> 秘密鍵 (dk) サイズ: 3168 bytes
> 暗号文 (ct) サイズ: 1568 bytes
> Bobの共有秘密鍵 (ss_b) [先頭8B]: 1cc8229fa4848516...
> Aliceの復元鍵 (ss_a) [先頭8B]: 1cc8229fa4848516...
=== 最終検証: 共有秘密鍵の一致確認 ===
結果: True
SUCCESS: 共有秘密鍵が正常に確立されました。
終わりに
本記事では、前回簡易パラメータ実装では達成できなかったML-KEMの「共通鍵一致」を、liboqs-pythonという実用的なライブラリを通じて確実に動作させ、確認することができました。
今後はソフトウェアやアプリケーション、組み込みシステムへのPQC統合、アルゴリズム効率化、さらにはML-KEMのバックアップ候補であるHQCの比較・検証など、より実践的なテーマに取り組みたいと考えています。
Discussion