💨

ECDSA検証

2022/03/11に公開約5,900字

ECDSAとは

ECDSA(Elliptic Curve Digital Signature Algorithm)とは楕円曲線デジタル署名アルゴリズムと呼ばれる暗号技術の1種で、ブロックチェーンを用いたネットワーク上では仮想通貨の取引が正しく行われているかを検証するために用いられます。

例えば、イーサリアムネットワーク上でアリス(購入者)とボブ(出品者)がこのような取引をするとします。

  • ボブさんは絵画を中心としたアーティストでありイーサリアムネットワーク上に複数のNFTを出品している
  • アリスさんはボブさんのNFTを購入しようとし、1ETHをボブさんに送金した

上記のような取引をする際に最も重要なのが、そのトランザクションは誰が作成したものであり、誰が証明できるのかという点です。

こういった場合に用いられるのがECDSAです。

今回は下記2点に絞って解説していきます。

  1. トランザクションの署名はどのように作成されるのか
  2. どのように署名を検証しているのか

※楕円曲線に関する解説は行いません。

環境

WSL2
Ubuntu 20.04 on Windows(11)

プロジェクトのセットアップ

プロジェクト階層構造

├── node_modules
├── .gitignore
├── index.js
├── package-lock.json
├── package.json
├── usage.js
└── yarn.lock

.envファイル

SIGNED_ACOUNT_ADDRESS_ON_ETHERIUM = 0x89c24a88bad4abe0a4f5b2eb5a86db1fb323832c  //署名者のアドレス
PRIVATE_KEY = 0x61ce8b95ca5fd6f55cd97ac60817777bdf64f1670e903758ce53efc32c3dffeb  //署名者が保有する秘密鍵

利用するモジュール設定

index.js
const EthUtil = require('ethereumjs-util')
require('dotenv').config();
const env = process.env

実装手順

  • 検証したいメッセージ、秘密鍵を用意
  • EthUtil.ecsignメソッドで署名を作成
  • EthUtil.ecrecoverメソッドで署名から署名者の公開鍵を導出
  • EthUtil.toChecksumAddressメソッドで導出アドレスと実際の署名者のアドレスを比較

1. トランザクションの署名はどのように作成されるのか

MESSAGEはMetaMaskを利用していればよく見るモーダル画面のメッセージ部分のテキスト内容です。いつ署名したのかという点も重要な情報になるのでtimestampを取り入れています(本来はtimestampを用いた時間的制御も必要ですが今回はおまけで書いています)。

次に、EthUtil.keccakFromString, EthUtil.toBufferメソッドを用いてそれぞれ変数をハッシュ化しHASHED_MESSAGEHASHED_PRIVATE_KEYを得ます。

index.js
const SIGNED_ACOUNT_ADDRESS_ON_ETHERIUM = env.SIGNED_ACOUNT_ADDRESS_ON_ETHERIUM;
const PRIVATE_KEY = env.PRIVATE_KEY;

const timestamp = Date.now();
const MESSAGE = 'ようこそ!\n' + 'Address: ' + SIGNED_ACOUNT_ADDRESS_ON_ETHERIUM + '\n' + 'timestamp: ' + timestamp;
const HASHED_MESSAGE = EthUtil.keccakFromString(MESSAGE)
const HASHED_PRIVATE_KEY = EthUtil.toBuffer(PRIVATE_KEY)

単純なメッセージとバッファ化されたメッセージをconsole.logで比較します。

//単純なメッセージ
ようこそ!
Address: 0x89c24a88bad4abe0a4f5b2eb5a86db1fb323832c
timestamp: 1647030437067

//バッファ化されたメッセージ
<Buffer 14 fd a9 19 6b b7 9b d7 83 54 2c be 85 2a 2e 94 ac 3c 80 28 fd 81 46 2b 94 48 e8 e2 0b 26 b3 24>

最後に、EthUtil.ecsignメソッドの引数にHASHED_MESSAGEHASHED_PRIVATE_KEYをわたして署名を生成します(API叩くだけの質素な実装)。

index.js
//メッセージと秘密鍵から署名を作成する関数
function getSignature(hashedMessage, privateKey) {
  const createdSignature = EthUtil.ecsign(hashedMessage, privateKey);
  return createdSignature;
}

...

//署名を作成する
const CREATED_SIGNATURE = getSignature(HASHED_MESSAGE, HASHED_PRIVATE_KEY);

作成した署名をconsole.log(CREATED_SIGNATURE);で確認します。

{
  r: <Buffer 1f 69 ab 34 5f 4b 93 30 fc 4d 9c f4 4e 36 f8 b4 af 2c 0d c7 75 d9 99 f7 38 58 6e 39 95 56 68 de>,
  s: <Buffer 73 cb bc 6d 34 10 e3 c3 9d b0 cb 78 2f 67 53 ca b9 69 d3 f2 ff d9 99 d1 00 ae c8 52 79 93 71 6b>,
  v: 28
}

実装上注意する点

メッセージのハッシュ化を行う際には単純にバッファをかけるのではなく、用途に応じた関数を指定しなくてはいけません(当たり前ですが)。
例えばEthUtil.toBufferメソッドは0xプレフィックスの文字列しかサポートしていないメソッドなので、単純な文字列をバッファ化させるようにメソッドを選定する必要があります。

このようにEthUtil.toBufferメソッドでは文字列をバッファ化できません

index.js
const MESSAGE = 'honyohonyo'
const HASHED_MESSAGE = EthUtil.toBuffer(MESSAGE)
Error: This method only supports Buffer but input was: honyohonyo
at assertIsBuffer (/home/ikmz/dev/ECDSA/node_modules/ethereumjs-util/dist/helpers.js:23:15)
at Object.keccak (/home/ikmz/dev/ECDSA/node_modules/ethereumjs-util/dist/hash.js:15:34)
at sign (/home/ikmz/dev/ECDSA/index.js:27:33)
at Object. (/home/ikmz/dev/ECDSA/index.js:40:19)

モジュールは全てここに記載されているので適度にググったり翻訳して地道に探そう!

https://github.com/ethereumjs/ethereumjs-util/tree/master/docs

2. どのように署名を検証しているのか

先ほど入手した署名をもとにEthUtil.ecrecoverメソッドで公開鍵を導出します。

引数には、ハッシュ化された署名メッセージ、バッファ化された署名のr、バッファ化された署名のs、ブロックチェーンネットワークIDを用いる仕様です。

署名検証に用いるEthUtil.toChecksumAddressメソッドにて「警告:chainIdがある場合とない場合のチェックサムは異なります」とあるので、今回はEthUtil.ecrecoverメソッドのブロックチェーンネットワークIDを指定しません。

公開鍵をconsole.logで確認します

<Buffer ff f4 9b 58 b8 31 04 ff 16 87 54 52 85 24 66 a4 6c 71 69 ba 4e 36 8d 11 83 0c 91 70 62 4e 0a 95 09 08 0a 05 a3 8c 18 84 17 18 ea 4f c1 34 83 ac 46 7d ... 14 more bytes>

次に、EthUtil.pubToAddressメソッドで引数に渡した公開鍵のイーサリアムアドレスを返し、EthUtil.bufferToHexメソッドでバッファを0xプレフィックス付きの16進文字列に変換します。

最後に、EthUtil.ecrecoverメソッドで署名の検証を行います。

index.js
//メッセージと署名をもとに、署名者の Ethereum アドレスを導出し署名者が正しいかどうかを返すメソッド
function getVerifiedSigner(hashedMessage, createdSignature, signedAccountAddress) {
  //作成された署名から公開鍵を導出
  const publicKey = EthUtil.ecrecover(hashedMessage, createdSignature.v, EthUtil.toBuffer(createdSignature.r), EthUtil.toBuffer(createdSignature.s));
  //公開鍵から署名者のアドレスを導出
  const signerAccountAddress = EthUtil.bufferToHex(EthUtil.pubToAddress(publicKey));

  //導出したアドレスと署名時のアドレスを比較する
  if(EthUtil.toChecksumAddress(signerAccountAddress) == EthUtil.toChecksumAddress(signedAccountAddress)){
    return true;
  }else{
    return false;
  }
}

//署名を作成する
const CREATED_SIGNATURE = getSignature(HASHED_MESSAGE, HASHED_PRIVATE_KEY);

//署名を検証する
const isVerified = getVerifiedSigner(HASHED_MESSAGE, CREATED_SIGNATURE, SIGNED_ACOUNT_ADDRESS_ON_ETHERIUM)
console.log(isVerified)

実装上必要な知識

公開鍵から署名者のアドレスを導出する意味ですが、公開鍵暗号方式に関しては一般にこのような内容です。

  • 公開鍵:サーバ側が持ってるもの(鍵穴)
  • 秘密鍵:所有者が持つもの(鍵)

ですので、「鍵穴を見たら、鍵を持ってる人間の情報を導出できるってことかな」というような理解でOKだと思います。

もちろんハッシュの衝突性から秘密鍵の導出まではできません。

また、仮にウォレットアドレスの秘密鍵が流出した場合、秘密鍵所有者が所有しているNFTやメインネットの仮想通貨を操作できるようになりますので保管には十分気を付けてください。

動作確認

それではいよいよターミナルで署名の検証を行います。もし導出されたアドレスと署名時に用いたアドレスが等しければtrue、そうでなければfalseが返るはずです。

ikmz@ikmz:~/dev/ECDSA$ node index.js
true

OK!

おまけ

署名の際に表示されるメッセージが文字化けする事象が発生するらしい。

https://github.com/MetaMask/metamask-extension/issues/5473
https://github.com/MetaMask/metamask-extension/issues/3931

参考記事

https://qiita.com/hm0429/items/e14223de67b876dcb792

感想

業務でブロックチェーン処理を書くことはないですが、ブロックチェーン処理に関わる処理やデバッグ作業を行うことは多いので今回は署名アルゴリズムの一つであるECDSAを抜粋して書いてみました。まだまだブロックチェーン領域は未知な点が多いですが業務や先輩方のディベートで出会った用語やをなるべくとりこぼさず実装までこぎつけて理解を深めていければと思います。
最後までお読みいただきありがとうございました。

GitHubで編集を提案

Discussion

ログインするとコメントできます