BOOSTRY Tech Blog

株式会社BOOSTRY の技術ブログです

3者間Diffie-Hellmanを利用したエスクロー決済の効率的な秘匿化

こんにちは。BOOSTRY CTOの麻生です。

本記事では、今年3月のニュースリリースで発表した弊社のクロスチェーンDVP決済の取り組みに関連し、現行プロトコルの改良方式について研究開発した内容をご紹介します。

野村総合研究所様、野村證券様、ディーカレットDCP様、三井住友銀行様と共同で実施したDVP決済の概念実証については、弊社ブログで詳しく解説していますので、ぜひご覧ください。

1. 現状のDVP処理方式とその課題

1-1. 現状の方式

BOOSTRYでは”クロスチェーン”によるDVP決済プロトコルの開発を進めています。このプロトコルには、以下の特徴があります。

  • 証券部分はibetで、資金部分はibet外のシステムで決済します。
  • 決済に関わる当事者間の合意形成は、すべてibet上のコントラクトでオンチェーンにて行われます。

DVP決済の大まかな流れは次の通りです。

  1. 証券の売手(seller)と買手(buyer)がDVPコントラクト上で決済内容の合意を行います。
  2. buyerは資金移動業者(payment agent)に、以下の情報とともに金銭の振込指図を行います。
    • seller口座番号(資金移動業者内の口座番号)
    • 決済ID(コントラクトで採番)
    • 約定金額
  3. payment agentはコントラクト情報を参照し、取引内容の整合性を確認します。
  4. 問題がなければ、payment agentはbuyer口座からseller口座へ資金を振替え、最終的にコントラクト上の決済ステータスを完了状態にします。これによりbuyerはコントラクトから証券を引き出せるようになります。

現状のDVP方式

この決済スキームはエスクロー決済」に類するものです。本義的なDVP決済は日銀ネットの当預系と完全に連動する必要がありますが(参考:日本銀行)、本プロトコルではpayment agentが資金決済のファイナリティを保証することで“準DVP”を実現しています。

プロトコルでは、取引内容の機密性を一時的に担保するため、オンチェーン上の資金決済情報は当事者間のみが参照できるよう秘匿化しています。現状の秘匿化は関係者間で共通鍵をセットアップし上で、共通鍵で暗号化して行われます。

1-2. 課題

このように、現状のDVP決済プロトコルでは、事前に当事者間でセットアップした共通鍵でメッセージの暗号化を行っていますが、以下のような課題があります。

  • セットアップフェーズが煩雑でスケールしない
    • 3者間で高いセキュリティレベルの通信方式により共通鍵を共有する必要があり、自動化が難しく、拡張性に欠けます。
  • 鍵の変更の負荷が高く、セキュリティレベルが犠牲になる
    • 鍵を変更する際、3者の鍵を同時にセットアップする必要があり、調整の負担が大きくなります。そのため、同じ鍵を長期間使い回すインセンティブが生じ、セキュリティ上望ましくありません。

2. 改良方式

現行プロトコルの暗号鍵セットアップに関する課題を解決するため、「3者間 Diffie-Hellmanスキームの導入を検討しました。本章では、まず3者間 Diffie-Hellman の概要を説明し、その後DVP決済プロトコルの改良方式について解説します。

2-1. 単純な Diffie-Hellman

まずは、基本となるDiffie-Hellman鍵共有について簡単におさらいします。

Diffie-Hellman鍵共有は、TLS/SSLなどの暗号通信で暗号鍵を生成する際に使われるプロトコルです。事前に秘密情報を共有していなくても、盗聴の可能性がある通信路を使って安全に暗号鍵を共有できます。

Diffie-Hellman 鍵共有スキーム

  1. 事前に以下のパラメータを決めます。
    • 巨大な素数P
    • Pの生成元G
  2. AliceとBobは、それぞれ秘密の数(private)を生成します。
  3. 公開鍵 G^{private} mod P を計算し、互いに公開します。
  4. 受け取った公開鍵を自分の秘密鍵でべき乗し、{shared public}^{private} mod P を計算します。これにより、両者は同じ共通鍵を得られます。

Diffie-Hellman 鍵共有 https://www.practicalnetworking.net/series/cryptography/diffie-hellman/ より

なお、楕円曲線を用いたものはECDH(楕円曲線Diffie-Hellman)と呼ばれます。

通常のDiffie-Hellmanは1ラウンド(1回のデータ送受信)で2者間の鍵共有しかできません。3者間でECDHを行う場合は3ラウンド必要となり、効率が悪くなります。3者間で効率的に鍵交換を行うには、別の仕組みが必要です。

2-2. Joux の3者間 Diffie-Hellman

3者間のDiffie-Hellman拡張として、Joux(ジョー)は2000年にペアリング暗号を用いた方式を提案しました。この方式では、1ラウンドで3者間の鍵共有が可能です。

Joux, A. (2000). A One Round Protocol for Tripartite Diffie–Hellman. In: Bosma, W. (eds) Algorithmic Number Theory. ANTS 2000. Lecture Notes in Computer Science, vol 1838. Springer, Berlin, Heidelberg. https://doi.org/10.1007/10722028_23

ペアリング暗号の詳細は本稿では割愛しますが、必要な部分のみ簡単に説明します。

G₁, G₂をn位数の加法巡回群、G_Tをn位数の乗法巡回群とし、P, QをG₁, G₂の生成元(ねじれ点)とします。ペアリングは、eₙという写像関数で表され、次のような双線型性を持ちます。

双線型性写像

  • 任意の  a, b \in \mathbb{Z} なるa, bに対して、次の式が成り立つ。
 \displaystyle
    e_{n}(aP, bQ) = e_{n}(P,Q)^{ab}

Jouxの3者間Diffie-Hellmanでは、この双線型性が重要な役割を果たします。

ここで、Alice、Bob、Charlieの3者が鍵交換を行うことを考えます。Alice、Bob、Charlieの3者がそれぞれ乱数を生成し、公開鍵を作成して公開します。

この時、Alice、Bob、Charlie の3者は、自身の秘密鍵と、他2者の公開鍵を用いて以下の値を計算することができます。

  • Alice: e_{n}(bP, cQ)^{a}
  • Bob: e_{n}(cP, aQ)^{b}
  • Charlie: e_{n}(aP, bQ)^{c}

これら3つの値は、ペアリングの双線型性により  e_{n}(P,Q)^{abc} と等しくなります。したがって、各々で導出された値を用いることにより、3者間で同一の共通鍵を生成することが可能になります。

2-3. 改良方式の鍵交換

Jouxの3者間Diffie-Hellmanを使うことで、DVP決済プロトコルの鍵交換セットアップを非同期で行えるようになります。改良方式の流れは以下の通りです。

  1. seller、buyer、payment agentがそれぞれ秘密鍵を生成し、公開鍵を作成します。
  2. 公開鍵を「鍵交換用コントラクト(KeyExchange contract)」に登録します。
  3. 各自が自分の秘密鍵と他2者の公開鍵から共通鍵を生成します。

改良方式における鍵交換

鍵を変更する場合は、自分の秘密鍵・公開鍵を差し替えるだけで済みます。ただし、いずれか1つでも鍵が変わると共通鍵全体が変わるため、実運用では変更タイミングの検知や順次切り替えなどの工夫が必要です。

この方式を使えば、buyer : seller : payment agent = 1 : 1 : 1 の単位で共通鍵を作成することも可能です。一方で、共通鍵の適用範囲を細かくすると鍵管理が複雑になるというトレードオフもあります。

2-4. 改良方式の実現上の課題

ここまで理論構成を説明しましたが、実際にシステムへ導入するにはフィージビリティの観点で課題があります。

まず、Jouxの3者間Diffie-Hellmanは、製品レベルの実装例がほとんど公開されていません。GitHub等で調査したところ、実験的な実装がいくつかある程度です。そのため、自分たちで本番運用可能な実装を作り、フィージビリティを評価する必要がありました。また、ブロックチェーン(スマートコントラクト)との組み合わせで大きな問題がないかも見極める必要があります。

さらに、安全性や処理速度の観点から、ペアリングに適した楕円曲線を使う必要があります。本稿では、BLS12-381曲線を用いた実装を検討しました。

補足:BLS12-381 曲線

BLS12-381曲線は、仮想通貨Zcashの提案の一環として2017年初頭にSean Bowe氏が設計しました。現在はBLS署名との相性の良さから、多くの暗号ライブラリでサポートが進んでいます。IETFでも以下の標準化文書で採用が進んでいます:

BLS12-381は、Barreto、Lynn、Scottが提案したBLS曲線ファミリーの一種です。「12」は埋め込み次数(embedding degree)を、「381」は曲線が定義される素体のビット長(おおよそ381ビット)を表しています。この381ビットというサイズは実用上も扱いやすく、たとえば48バイト(384ビット)に収まるため、残りの3ビットをフラグや最適化のために活用することができます。これらの数値は、暗号的安全性と実装効率の両立を目的として慎重に選定されています。

3. 実装

改良方式のフィージビリティを検証するため、DVP決済プロトコルの鍵交換フェーズのプロトタイプ実装を行いました。作成したソースコードOSSとして以下のリポジトリで公開しています。

github.com

3-1. ライブラリ選定

ペアリング演算については、他の暗号技術と同様、独自実装は避け、信頼できるサードパーティ製ライブラリを厳選して利用すべきです。今回は事前調査の結果、ethereum/py_eccを採用しました。

github.com

ethereum/py_ecc は Ethereum Foundation によって開発されている Ethereum 向け Python ライブラリであり、BOOSTRY の現状のバックエンド技術スタックとの親和性も高いです。EthereumエコシステムにおいてBLS署名を用いる目的で作られているライブラリであり、BLS12-381をサポートしています。BLS12-381を用いたペアリングの演算に利用することも可能です。

3-2. 鍵生成

各自の秘密鍵・公開鍵は以下のように実装できます。秘密鍵は32バイトの乱数を正の整数に変換して利用し、公開鍵はG1、G2(前述のP、Q)それぞれに対して算出します。

import secrets
from py_ecc.bls12_381 import G1, G2, multiply, curve_order

A_sk = int.from_bytes(secrets.token_bytes(32)) % curve_order
A_pk1 = multiply(G1, A_sk)
A_pk2 = multiply(G2, A_sk)

算出される秘密鍵・公開鍵は次のようなフォーマットです。G1の公開鍵は1×2、G2の公開鍵は2×2の行列となり、各要素は381ビット(約115桁)の正の整数です。

<A_sk>
48504464656614128748810727505606585212970767068906776632677249187557527048250
<A_pk1>
(3601029079103801678979025001108635334613321544325901274257453117702517675214350313718994239699519999636729757754629, 2238296275487970299483943455774669729169336608074610195174438775154719770606697208882042383629001884826124197256886)
<A_pk2>
((2561012464168234979130851503615979002752529580374144003338916913719963523386666641995900377434825947332841177782710, 1625247846228801585179289607617905082129925459494353477574525843496329720078402835823620191947142044233110618779704), (3824377781434581801001727407835743236566695867997478732780641281041468859132433236708030948491108581581302721130715, 1377706417334059720462638002155418041825964943144504148692399945980221580712450904385058476128683311324197269697583))

DVP改良方式では、公開鍵をコントラクトに登録します。得られた行列はシリアライズするか、要素ごとに専用項目を設けて保存します。

3-3. 鍵交換

例として、Bob・Charlieが提出した公開鍵からAliceが共通鍵を導出する場合は、次のように実装できます。

A_dh: bls12_381_FQ12 = pairing(B_pk2, C_pk1) ** A_sk

得られるのは1×12の行列で、各要素は381ビットの正の整数です。これを「AES256」の鍵に変換するには、以下のような仕様を規定した上で変換する必要があります。

_hash = sha256()
for _item in A_dh.coeffs:
    _item = int(_item)
    # バイト長を48byte((381+7)/8)に設定しBigEndianでbytesに変換
    _hash.update(_item.to_bytes(48))  
A_shared_key = _hash.digest()

このようにしてAlice・Bob・Charlieの全員が同じ共通鍵を得られます。導出した鍵を使い、例えばAES256(CBCモード)で暗号化する場合は次の通りです。

message_org = "A One Round Protocol for Tripartite Diffie–Hellman"
aes_iv = secrets.token_bytes(AES.block_size)
aes_cipher = AES.new(A_shared_key, AES.MODE_CBC, aes_iv)
pad_message = pad(message_org.encode("utf-8"), AES.block_size)
encrypted_message = base64.b64encode(aes_iv + aes_cipher.encrypt(pad_message)).decode()

鍵交換から共通鍵生成までの一連の処理は、以下のスクリプトで正常に動作することを確認しています。

この結果をもとに、実際のプロトコルで利用するKeyExchangeコントラクトや鍵交換用モジュールのプロトタイプも実装しました。

3-4. 3者間 Diffie-Hellman の実装

3-4-1. KeyExchangeコントラクト

KeyExchangeコントラクトの実装例は以下の通りです。

Secret-Escrow/contracts/utils/TripartiteKeyExchange.sol at main · BoostryJP/Secret-Escrow · GitHub

実装時のポイントは次の通りです。

  • 381ビットの整数はEVMのuint256では扱えないため、コントラクトではstring型で保存しています。
  • G1・G2の公開鍵は、各行列要素を個別項目として保存しています。
pragma solidity ^0.8.0;

/// @title Joux's tripartite diffie-hellmann key exchange
contract TripartiteKeyExchange {
    struct G1PublicKey {
        string g1pk_11;
        string g1pk_12;
    }
    mapping(address => G1PublicKey) public G1PK;

    struct G2PublicKey {
        string g2pk_11;
        string g2pk_12;
        string g2pk_21;
        string g2pk_22;
    }
    mapping(address => G2PublicKey) public G2PK;

    event RegisterPublicKey(address indexed account_address);

    // [CONSTRUCTOR]
    constructor() {}

    /// @notice Register public key
    function registerPublicKey(
        string memory g1pk_11,
        string memory g1pk_12,
        string memory g2pk_11,
        string memory g2pk_12,
        string memory g2pk_21,
        string memory g2pk_22
    ) public {
        G1PublicKey storage g1_pubkey = G1PK[msg.sender];
        G2PublicKey storage g2_pubkey = G2PK[msg.sender];

        g1_pubkey.g1pk_11 = g1pk_11;
        g1_pubkey.g1pk_12 = g1pk_12;

        g2_pubkey.g2pk_11 = g2pk_11;
        g2_pubkey.g2pk_12 = g2pk_12;
        g2_pubkey.g2pk_21 = g2pk_21;
        g2_pubkey.g2pk_22 = g2pk_22;

        emit RegisterPublicKey(msg.sender);
    }
}

3-4-2. 鍵交換用モジュール

次に、上記KeyExchangeコントラクトを利用した鍵交換用モジュールのプロトタイプです。

Secret-Escrow/app/blockchain/tripartite_key_exchange.py at main · BoostryJP/Secret-Escrow · GitHub

このモジュールはKeyExchangeコントラクトと連携し、

  • 鍵の生成
  • 公開鍵の登録
  • G1・G2公開鍵の取得
  • 共通鍵の生成

といった機能を提供します。各機能が想定通り動作することはテストコードで確認済みです。主要部分の実装例は以下の通りです。

import secrets
from hashlib import sha256

from py_ecc.bls12_381 import G1, G2, curve_order, multiply, pairing
from py_ecc.fields import bls12_381_FQ, bls12_381_FQ2, bls12_381_FQ12
from pydantic import BaseModel

from app.blockchain.type.eth_account import EOA
from app.utils.contract_utils import ContractUtils
from config import CHAIN_ID, TX_GAS_LIMIT


class TKERegisterPublicKeyParams(BaseModel):
    g1pk_11: str
    g1pk_12: str
    g2pk_11: str
    g2pk_12: str
    g2pk_21: str
    g2pk_22: str


class TripartiteKeyExchangeContract:
    def __init__(self, account: EOA, contract_address: str):
        self.key_exchange_contract = ContractUtils.get_contract(
            contract_name="TripartiteKeyExchange", contract_address=contract_address
        )
        self.account = account

    @staticmethod
    def generate_key():
        sk = int.from_bytes(secrets.token_bytes(32)) % curve_order
        g1pk: tuple[bls12_381_FQ, bls12_381_FQ] = multiply(G1, sk)
        g2pk: tuple[bls12_381_FQ2, bls12_381_FQ2] = multiply(G2, sk)
        return sk, g1pk, g2pk

    def register_public_key(self, public_key: TKERegisterPublicKeyParams):
        tx = self.key_exchange_contract.functions.registerPublicKey(
            public_key.g1pk_11,
            public_key.g1pk_12,
            public_key.g2pk_11,
            public_key.g2pk_12,
            public_key.g2pk_21,
            public_key.g2pk_22,
        ).build_transaction(
            {
                "chainId": CHAIN_ID,
                "from": self.account.address,
                "gas": TX_GAS_LIMIT,
                "gasPrice": 0,
            }
        )
        tx_hash, tx_receipt = ContractUtils.send_transaction(
            transaction=tx, private_key=self.account.private_key
        )
        return tx_hash, tx_receipt

    def get_G1_public_key(self, address: str):
        g1pk = self.key_exchange_contract.functions.G1PK(address).call()
        return g1pk

    def get_G2_public_key(self, address: str):
        g2pk = self.key_exchange_contract.functions.G2PK(address).call()
        return g2pk

    def generate_shared_key(self, secret_key, address1: str, address2: str):
        Q: tuple[bls12_381_FQ2, bls12_381_FQ2]
        P: tuple[bls12_381_FQ, bls12_381_FQ]

        address1_g2pk = self.get_G2_public_key(address1)
        Q = (
            bls12_381_FQ2([int(address1_g2pk[0]), int(address1_g2pk[1])]),
            bls12_381_FQ2([int(address1_g2pk[2]), int(address1_g2pk[3])]),
        )

        address2_g1pk = self.get_G1_public_key(address2)
        P = (
            bls12_381_FQ(int(address2_g1pk[0])),
            bls12_381_FQ(int(address2_g1pk[1])),
        )

        dh_paring: bls12_381_FQ12 = pairing(Q, P) ** secret_key
        _hash = sha256()
        for _item in dh_paring.coeffs:
            _item = int(_item)
            _hash.update(_item.to_bytes(48))
        shared_key = _hash.digest()
        return shared_key

4. まとめ

本記事では、既存のDVP決済プロトコルにおける鍵交換フェーズの課題を解決するため、Jouxの3者間Diffie-Hellmanを活用した改良方式を提案しました。プロトタイプ開発を通じて、実装上のフィージビリティも検証し、仮説通りの仕組みが実現できることを確認しています。

なお、本記事の内容は社内勉強会資料をもとに再整理したものです。BOOSTRYでは毎週、社内で技術勉強会を開催し、ブロックチェーンをはじめとする先端技術の知見をメンバー間で共有しています。この記事を通じて、BOOSTRYの技術への取り組みやブロックチェーン技術の可能性に少しでも興味を持っていただければ幸いです。今後も研究開発およびプロダクト開発を通じて、より効率的で機能性の高い、先進的な金融市場の実現に向けて取り組んでまいります。引き続きBOOSTRYの活動にご注目ください。

ご興味のある方は、ぜひ採用情報もご覧ください。私たちは、金融×ブロックチェーンという難しくもやりがいのある領域で、新しい市場を共に創る仲間をいつでも歓迎しています。

〒101-0032 東京都千代田区岩本町3丁目9-2 PMO岩本町4F