はじめに

『DApps開発入門』という本や色々記事を書いているかるでねです。

今回は、BLS12-381鍵をツリー構造で安全に導出する仕組みを提案しているEIP2333についてまとめていきます!

以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。

他にも様々なEIPについてまとめています。

概要

EIP2333は、1つのエントロピーソース(乱数を生成するための元データ)からツリー構造を用いてBLS秘密鍵を導出するための方式を提案しています。 この仕組みにより、BLS12-381(楕円曲線暗号の一種)カーブ上でほぼ無限の鍵を階層的に生成することができ、親鍵と子インデックスのみで子鍵を得られるため、柔軟で安全な鍵管理が可能です。

BLS12-381は、楕円曲線暗号(Elliptic Curve Cryptography, ECC)の一種で、特にペアリングベース暗号に適した曲線です。 Zcash、Ethereum 2.0などの多くのブロックチェーンプロジェクトで使われています。

BLSは「Boneh–Lynn–Shacham」の略で、署名スキームの一種です。 12 はこの曲線が「embedding degree 12(元の数の範囲を12倍に拡張した範囲で計算をする)」を持つことを示しています。 381 はこの楕円曲線で使われるパラメータが約381ビットの大きさであることを意味しています。 大きな数を使うほど安全性が高まります。

このカーブは高速な検証、短い署名、集約署名(複数署名を1つにまとめる)などに優れています。

さらに、BLS12-381が量子コンピュータにより安全でなくなる事態に備え、各鍵に対してポスト量子耐性を持つLamport署名のバックアップを組み込む設計となっています。

Lamport署名とは?

Lamport署名は、最もシンプルで古典的な量子耐性署名方式の1つです。 SHA256などのハッシュ関数だけを使って、安全な電子署名を実現します。

  • ランダムな秘密鍵ペア(たとえば256ビット×2 = 512個の乱数)を生成
  • 公開鍵として、それぞれをハッシュ化した値を用意
  • 署名時には、メッセージの各ビットに応じた秘密鍵の片方を公開
  • 検証者は、公開鍵の対応部分と照合することで署名を確認

これにより、量子コンピュータでも破られにくくなりますが(Shorのアルゴリズムが効かない)、以下のデメリットもあります。

  • 鍵が大きい(公開鍵も署名も数KB~)
  • 使い捨て(一度使ったら再利用できない)

EIP2333ではLamport署名を中間ステップに用いることで、BLS12-381が量子攻撃に破られた場合に備えた緊急時のバックアップ手段として使われています。

この仕様はEthereum 2.0だけでなく、BLS署名を利用する他のエコシステムでも共通で利用できることを目指しています。将来的には中立的なリポジトリへ移行される予定です。

動機

既存方式の問題点

Ethereum 1.xで利用されていた鍵導出方式(BIP32)は、BLS12-381カーブでは非効率です。 BIP32で鍵を生成すると、そのうち約54%以上が曲線の順序(order)を超えてしまい無効となるため、現実的な利用には適していません。

楕円曲線上の秘密鍵は、特定の数値の範囲(0 ~ r-1)内にある必要があります。 この r が曲線の順序(ある点を繰り返し加算すると元に戻る)です。

BIP32では、32バイトのランダムな数(約2²⁵⁶通り)を秘密鍵として扱います。 しかし、BLS12-381の順序 r は 22552^{255} より少し小さいです。 このため、BIP32で生成した鍵のうち、約54%がこの範囲外に出てしまい(生成される値の半分以上が r 以上になってしまうため)無効になります。

無効な鍵を再生成する処理を入れると効率が悪くなり、実用的ではありません。 したがって、BLS12-381向けには新しい鍵導出方式(このEIPで提案されている方法)が必要です。

これに対し、secp256k1では無効な鍵が生成される確率は極めて低く(2の127乗分の1未満)です。

BIP32(Bitcoin Improvement Proposal 32)は、階層的決定性ウォレット(HDウォレット)の標準仕様です。

1つの「マスター鍵」から、複数の秘密鍵や公開鍵をツリー構造で生成でき、子鍵ごとにウォレットを分けられます(例えば支払い用・投資用・個人用など)。 バックアップが非常に簡単(1つのシード=エントロピーソースを保存すれば全鍵を再生成可能)で、EthereumやBitcoin、その他多くのブロックチェーンウォレットで採用されています。 ただし、BIP32はもともとsecp256k1という楕円曲線向けに設計されています。 そのため、他の曲線(たとえばBLS12-381)との相性に注意が必要です。

マルチチェーンでの早期標準化

ユーザーが初めて鍵を生成する前に標準を策定しておくことで、鍵導出方法を共通化できます。 これにより、以下のような利点があります。

  • 万が一の際に量子耐性を持つ署名方式へスムーズに移行できる(Lamport署名による橋渡しが可能)
  • 異なるブロックチェーン間で鍵管理ツールを共通化でき、運用が簡単になる

ポスト量子時代への備え

この導出方式は、中間ステップとしてLamport鍵ペアを生成し、それをバックアップ署名として利用できます。 BLS12-381が安全でなくなった場合、Lamport署名を通じてより安全な署名方式への移行が可能になります。 これは、将来の量子計算機の登場を見越した、安全性への重要な備えです。

仕様

この仕様では、BLS12-381の秘密鍵を階層的に導出する方法を定義しています。 基本的には、1つのエントロピーソース(seed)からマスター鍵を生成し、そこから階層的に子鍵を導出していく構造です。 また、量子コンピュータ時代に備えて、Lamport署名によるバックアップも組み込まれています。

バージョンについて

EIP2333では、BLS署名の標準化を進めるCFRGドラフト(v4)に準拠しています。 過去には hkdf_mod_r 関数の実装がドラフトと乖離していたため、2020年9月17日にEIP自体がアップデートされました。実装する場合は最新版に準拠してください。

鍵のツリー構造

鍵はツリー構造で定義されており、各鍵は親鍵と子インデックスから導出されます。 マスター鍵(ルート)から始まり、子ノード(m/0, m/1, ...)が階層的に展開されます。

text
         [m / 0] - [m / 0 / 0]
        /        \
[m] ---           [m / 0 / 1]
        \
         [m / 1] - [m / 1 / 0]

この構造により、1つの鍵から事実上無限の鍵を導出可能で、アプリケーションごとに異なる鍵を使い分けることができます。

鍵導出の全体像

鍵の導出プロセスは、以下の2つのステップに分かれます。

  • マスター鍵(ルート鍵)の生成
  • 親鍵から子鍵の生成

さらに、各子鍵の導出にはLamport鍵が中間ステップとして用いられ、これがポスト量子署名のバックアップになります。

関数

IKM_to_lamport_SK

sequenceDiagram
    participant ユーザー入力 as IKMとsalt
    participant 抽出器 as HKDF-Extract
    participant 拡張器 as HKDF-Expand
    participant 分割器 as bytes_split
    participant 出力 as Lamport秘密鍵255個

    ユーザー入力->>抽出器: IKMとsaltを渡す
    抽出器->>抽出器: PRKを生成
    抽出器->>拡張器: PRKを渡す
    拡張器->>拡張器: OKM(8160バイト)を生成
    拡張器->>分割器: OKMを渡す
    分割器->>出力: 32バイト×255個に分割して出力

任意の秘密値(IKM)とソルトからLamport秘密鍵を生成する関数

  1. HKDF-Extractで PRK を生成(saltとIKMを使用)
    • IKM
      • 元になる秘密(エントロピーソースや親秘密鍵など)
    • salt
      • 加工用の追加データ(子のインデックスなどが使われる)
  2. PRKを使ってHKDF-ExpandによりOKMを生成(長さ = 255×32バイト)
  3. OKMを32バイトずつに分割し、255個の秘密鍵とする

parent_SK_to_lamport_PK

sequenceDiagram
    participant 入力 as 親秘密鍵と子インデックス
    participant salt変換器 as インデックス → salt変換
    participant IKM変換器 as 親鍵 → IKM変換
    participant Lamport生成器 as IKM_to_lamport_SK
    participant ハッシュ器 as SHA256ハッシュ処理
    participant 出力 as 圧縮Lamport公開鍵

    入力->>salt変換器: 子インデックスを渡す
    salt変換器-->>IKM変換器: salt(4バイト)を出力
    入力->>IKM変換器: 親秘密鍵を渡す
    IKM変換器-->>Lamport生成器: IKM(ビッグエンディアン)を出力

    Lamport生成器->>Lamport生成器: lamport_0 を生成
    Lamport生成器->>Lamport生成器: IKMをビット反転し、lamport_1 を生成

    Lamport生成器->>ハッシュ器: lamport_0[i], lamport_1[i] をSHA256でハッシュ
    ハッシュ器->>ハッシュ器: ハッシュ結果をすべて連結
    ハッシュ器->>出力: 全体をSHA256でまとめる

親ノードの秘密鍵と子インデックスから、Lamport公開鍵を生成する関数。

  1. 子インデックスをsaltに変換(4バイト)
  2. 親の秘密鍵をビッグエンディアン(大きい順に)で変換し、Lamport秘密鍵セット(lamport_0)を生成
  3. 上記秘密鍵のビット反転版(not_IKM)から lamport_1 を生成
  4. 各lamport秘密鍵をSHA256でハッシュして連結
  5. 最後に全体をSHA256でまとめ、32バイトの圧縮Lamport公開鍵を得る

HKDF_mod_r

sequenceDiagram
    participant 入力 as 入力IKM
    participant ソルト初期化 as 初期ソルト設定
    participant 抽出器 as HKDF-Extract
    participant 拡張器 as HKDF-Expand
    participant 整数変換器 as OKM → 整数変換
    participant 出力判定 as SK判定

    入入力->>ソルト初期化: "BLS-SIG-KEYGEN-SALT-" を使用
    loop SKが0である限り繰り返す
        ソルト初期化->>抽出器: salt, IKM || 0x00
        抽出器->>拡張器: PRKを渡す
        拡張器->>整数変換器: OKMを渡す(L = 48バイト)
        整数変換器->>出力判定: SK = OKM mod r を計算
        出力判定-->>ソルト初期化: SK == 0 の場合 salt をハッシュして再試行
    end
    出力判定->>入力: SK(BLS秘密鍵)を返す

任意のバイト列(32バイト以上)をBLS12-381の秘密鍵(0 <= SK < r)に変換する関数。

  1. 初期ソルトとして "BLS-SIG-KEYGEN-SALT-" を使用
  2. HKDFでランダムな出力を得て、それを整数に変換して、r で割った余りを秘密鍵とする
  3. もし得られた鍵が0であれば、saltをハッシュして再試行

derive_child_SK

sequenceDiagram
    participant 入力 as 親秘密鍵と子インデックス
    participant Lamport公開鍵生成器 as parent_SK_to_lamport_PK
    participant BLS鍵変換器 as HKDF_mod_r
    participant 出力 as 子秘密鍵

    入力->>Lamport公開鍵生成器: 親秘密鍵とインデックスを渡す
    Lamport公開鍵生成器->>BLS鍵変換器: 圧縮Lamport公開鍵を渡す
    BLS鍵変換器->>出力: BLS秘密鍵(子鍵)を返す

親の秘密鍵とインデックスから子の秘密鍵を生成する関数。

  1. parent_SK_to_lamport_PK で圧縮Lamport公開鍵を生成
  2. HKDF_mod_r で公開鍵を秘密鍵に変換
  3. 得られたものが子の秘密鍵

derive_master_SK

sequenceDiagram
    participant 入力 as seed(エントロピーソース)
    participant BLS鍵変換器 as HKDF_mod_r
    participant 出力 as マスター秘密鍵(m)

    入力->>BLS鍵変換器: seed を渡す
    BLS鍵変換器->>出力: BLS秘密鍵(マスター鍵)を返す

ルート(マスター)鍵を生成する関数。 通常、BIP39のmnemonicなどから得られるseedを使用します。

  1. HKDF_mod_r を使って、seedから秘密鍵を生成
  2. それがマスター鍵(m)となる

補足

  • Lamport署名を中間に挟むことにより、将来的に量子計算機がBLS12-381を破るような事態が発生しても、安全な別の署名方式に移行できる橋渡しとして使えます。
  • セキュリティ保証はLamport署名において127.5ビット相当です(SHA256の設計上の制限による)。

補足

Lamport署名の採用理由

Lamport署名は、量子コンピュータに耐性がある署名方式の中でも構造が単純で実装しやすいという特徴があります。 暗号の要件としては安全なハッシュ関数さえあればよく、他に複雑な数式や演算は必要ありません。

EIP2333では、Lamport署名はバックアップとしての用途に限定されているため、鍵サイズが大きいという欠点も問題になりません。

また、Lamport署名を使うと、その署名に紐づくBLS秘密鍵が見えてしまいます。 これは逆に言えば、「Lamport署名を使った後はBLS鍵はもう信用してはならない(使い捨て)」ということを意味します。 この設計により、BLS鍵に余計なバックアップ領域を持たせる必要がなくなります。

なお、Lamport鍵の安全性は256ビットではなく255ビット相当ですが、これはSHA256ベースのHKDFの出力制限(最大255ブロック)によるもので、仕様をシンプルに保つための選択です。

SHA256の採用理由

EIP2333では、全てのハッシュ関数にSHA256を使用しています。 これは、IETFによって提案されているBLS署名標準でもSHA256が使用されているため、整合性を保つ意味で採用されています。

異なる暗号処理に複数のハッシュ関数を使うと実装が煩雑になり、安全性にも影響を与える可能性があります。 そのため、単一のハッシュ関数に統一することは実装の簡素化と安全性の両面で有利です。

hkdf_mod_r() 関数について

この関数は、IETFのBLS提案標準で定義されている KeyGen 関数と完全に同一の処理を行います。 つまり、同じseedから得られる秘密鍵は KeyGen でも hkdf_mod_r() でも同じになります。

このように標準化されている関数を使うことで、多くのセキュリティレビューを経た実績ある方式をそのまま利用できます。 また、エンジニアリング上の再利用や相互運用も容易になります。

なお、この関数には「modulo bias(モジュロバイアス)」と呼ばれる偏りの問題がありますが、その影響は非常に小さく(48バイトの出力に対して、曲線の順序より遥かに大きいため)、実用上無視できる程度とされています。

hardened鍵のみを使用

従来のHDウォレット規格(BIP32やBIP44)では、「ハード化鍵」と「非ハード化鍵」の両方が利用可能でした。

  • ハード化鍵
    • 親の秘密鍵がなければ子鍵を計算できない
  • 非ハード化鍵
    • 親の公開鍵だけでも子鍵を作れる

しかしEIP2333では、セキュリティの観点からハード化鍵しか提供しません。

非ハード化鍵はUTXO型(Bitcoinなど)では有用ですが、Accountベース(Ethereumなど)では管理が複雑になりがちです。 また、非ハード化鍵は量子計算機が得意とする群演算(楕円曲線の加算など)に依存しているため、ポスト量子時代には安全性に疑問が残ります。

そのため、EIP2333では初めから安全性を重視したハード化鍵のみに限定されています。

最後に

今回は「BLS12-381鍵をツリー構造で安全に導出する仕組みを提案しているEIP2333」についてまとめてきました! いかがだったでしょうか?

質問などがある方は以下のTwitterのDMなどからお気軽に質問してください!

他の媒体でも情報発信しているのでぜひ他も見ていってください!