🐕

簡易実装から本番実装へ: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は、詳細な数理計算を一旦脇に置くと、以下の流れで整理できます。

  1. 鍵生成

    • K-PKE鍵生成アルゴリズムを使用して、共通鍵を安全に受け取るためのカプセル化鍵(公開鍵)とデカプセル化鍵(秘密鍵)を生成します。
  2. カプセル化(Encapsulation)

    • 送信者は、受信者のカプセル化鍵(公開鍵)を使用し、共通鍵と暗号文を生成します。
    • 送信者は受信者に暗号文のみを送信します。
  3. デカプセル化(Decapsulation)

    • 受信者は送られてきた暗号文を秘密鍵で復元し、共通鍵を取り出します。
    • このとき、暗号文が途中で改ざんされていないかを検証し、安全性が確認できた場合のみ、その共通鍵を有効にします。一致しなかった場合は、ランダム値を代替として共通鍵にします。(IND-CCA安全の実現)

環境構築

PQCアルゴリズムのCライブラリであるliboqsをPythonから利用するための準備をします。具体的には、下記のGitHubリポジトリを使用します。
https://github.com/open-quantum-safe/liboqs-python

インストール手順(Mac)

  1. 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
  1. liboqs-pythonのインストール
    1. 仮想環境の作成と有効化
      Python仮想環境を作成し、有効化します。
      python3 -m venv venv
      . venv/bin/activate
      python3 -m ensurepip --upgrade
      
    2. ラッパーの設定とインストール
      Pythonラッパーliboqs-pythonのソースコードをクローンし、liboqs-pythonをインストールします。
      git clone --depth=1 https://github.com/open-quantum-safe/liboqs-python
      cd liboqs-python
      pip install .
      
  2. サンプルの実行
    インストールが完了したら、以下のコマンドで機能を確認してください。
    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
    

インストール後に、liboqsliboqs-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は好みの場所に作成してください。
(私は、liboqsliboqs-pythonと同じ階層に作りました。)

テストコード

test_pqc.py
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 の鍵生成と暗号処理の内部実装」もご覧ください。

  1. 初期化とリソース管理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 関数が自動的に呼び出されます。
  2. 鍵ペア生成alice.generate_keypair()

    # 2. 鍵ペア生成 (KeyGen)
    # Pythonのラッパーによって、liboqs C言語の OQS_KEM_keypair() が実行される。
    public_key = alice.generate_keypair()
    
    • 内部で起きていること
      • KeyEncapsulationクラスで定義されているgenerate_keypair()関数を実行します。
      • 関数内では、インスタンス時にC言語コアから取得した鍵バイト数分の空の領域(バッファ)を確保します(public_keyself.secret_key)。その後、C言語のOQS_KEM_keypair が呼び出され、Cライブラリ内で鍵ペアが生成されます。
      • 成功した場合は公開鍵をPythonのbytesとして返されます。
  3. 鍵カプセル化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として返されます。

  4. 鍵デカプセル化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