どうも、DRIP エンジニアの小川です。
DRIP は Drecom Invention Project の略称で、ドリコムが発明を産み続けるためのプロジェクトです。
今年は AR とブロックチェーンをメイン領域として活動しています。

先日「日本初の大規模イーサリアム技術者会議」と銘打った Hi-Con が開催され、私は LT 枠として登壇し表題の話をしました!
資料は SlideShare にアップしてあります。
が、資料では文字が小さくなって主に実装部分がわかりづらくなってしまっているため、今回は当資料の補足をさせていただきます。
技術者会議にて発表した都合上、今回はエンジニア向けの内容となっておりますのでご留意ください。

  • Ethereum(イーサリアム) とは「分散アプリケーションのためのプラットフォーム」であり、ブロックチェーンをベースに構築されています。
  • DApps とは「ブロックチェーンを利用した非中央集権の分散型アプリケーション」の事で、本稿では「Ethereum のブロックチェーンを利用したアプリケーション」を指します。参考情報はこちら。
  • web3 とは主に Web フロントエンドで使用する Ethereum JavaScript API である web3.js の略称です。

web3.eth.personal.sign とは?

指定した文字列を、指定アドレスの元となる秘密鍵で ECDSA を用いて署名するメソッドです。
ECDSA による署名についての説明は長くなるため割愛させていただきますので、詳しくはこちらこちらなどをご参照ください。
メソッドを叩くと、このような画面がユーザーに表示されます。

署名すると (r,s,v) を意味する16進数バイトコード 65 bytes 分の文字列を取得できます。
以下、この文字列を署名データと呼びます。
また上記画像を見てもらうとわかる通り、署名に使うメッセージはユーザーに表示されるため、ここを UX の一環として設計するとフロントエンド側に余計な UI を挟まなくて済みます。

web3.eth.personal.sign(
  "Hello world", // UTF-8 文字列ならおそらく大丈夫
  "0x11f4d0A3c12e86B4b5F39B213F7E19D048276DAe",
  “test password!” // unlock されてるアカウントへの署名なら null でも動作します
)
.then(res => sig = res)

console.log(sig)
> "0x30755ed65396facf86c53e6217c52b4daebe72aa4941d89635409de4c9c7f9466d4e9aaec7977f05e923889b33c0d0dd27d7226b6e6f56ce737465c5cfd04be400"

r = "0x" + sig.slice(2, 66)
s = "0x" + sig.slice(66, 130)

v = parseInt(sig.slice(130, 132), 16)
if (v < 27) v += 27
v = "0x" + v.toString(16)

ecrecover とは?

Solidity で定義されている、署名データを検証するメソッドです。
署名データおよび署名に使った文字列を渡すと、署名したアドレスが返ってきます。

hash_message = "\x19Ethereum Signed Message:\n" + len(message) + message
※ len(message) is UTF-8 byte size.

// 本来 ecrecover は JSON-RPC で問い合わせるものですが、概念を伝えるため以下の形にしています。
revocer_address = ecrecover(keccak256(hash_message), v, r, s) 
console.log(revocer_address == "0x11f4d0A3c12e86B4b5F39B213F7E19D048276DAe")
> true

今回は一例として Solidity 側での検証を採用していますが、ECDSA 署名の検証は node.jsRuby などでもライブラリを使えば可能ですので、ぜひそちらも試していただければと思います。

  • Solidity とは Ethereum Smart Contract を開発可能な言語の1つで、自由度が高く情報も豊富なため多くの DApps 開発者に利用されています。

ユーザー認証設計

これで web3.eth.personal.sign による署名と ecrecover による検証が可能となりました。
というわけでこの仕組みを DApps のユーザー認証に使えるようにする設計を考えてみます。

ユーザーがフロントエンドで何かしらの文字列に署名し、

  • 署名データ
  • 署名に使った文字列
  • 署名に使ったアドレス

をバックエンドに渡します。
バックエンド側で署名データを検証して出てきたアドレスが「署名に使ったアドレス」と一致した場合、そのユーザーはそのアドレスの所有者だ!と判断して良いと考えられます。
所有者として判断できる理由は、ecrecover が「署名に使ったアドレス」を返せる署名データを作れるのは、そのアドレスの秘密鍵だけであるからです。
検証には秘密鍵は不要かつ署名データから秘密鍵を導出することはほぼ不可能なので、ユーザーは秘密鍵を安全なところに置いたまま、別のユーザーやマシンに対してアドレスの持ち主であることを証明できるのです。
そして、アドレスは Ethereum ネットワーク内で一意であるため、そのアドレスをユニークパラメータとして紐づけた User データをバックエンドが持つようにしておけば、ユーザーが指定の文字列から署名データを作ってバックエンドに投げることでユーザーの本人認証が実現できます。

懸念

しかし、このやり方には2つの懸念があります。

署名データの有効期限

先ほど挙げた

  • 署名データ
  • 署名に使った文字列
  • 署名に使ったアドレス

がもし第三者に漏れてしまった場合、その人がこれらをバックエンドに投げてしまえばユーザーのなりすましをする事が可能です。
よくある Web サービスではバックエンドと通信する際にやり取りするデータが万が一漏れたとしても救済する仕組みを持っています。
トークンに有効期限を付けたり、トークンの強制無効化だったり、email からのパスワード変更だったり、2段階認証だったり…

署名に使うメッセージがどこから要求されたものなのかわからない

署名に使用するメッセージにはウェブサイト特有の情報が暗黙で含まれないため注意が必要という指摘があり、例えば署名する直前にメッセージがすり替えられていたとしても、ユーザーはなかなか気づけないかもしれません。
署名前にメッセージが表示されるのが救いなのでメッセージ中に「このメッセージは◯◯で利用します」というような事を含められるが、要求元の証明としてはここまでが限界と言えます。
なので現状の web3.eth.personal.sign を使うのであれば、メッセージはある程度動的になっていて、かつ有効期限のようなものが含まれているのが望ましいと考えられます。

懸念への対応を考える

署名データに有効期限を入れる

署名する前にバックエンドから「ユーザー認証用の有効期限つきトークン」をもらい、それを署名メッセージに含める事で署名データそのものに有効期限を持たせるようにしてみます。
つまりユーザー認証時は

  • 署名データを検証して得たアドレスが「署名に使ったアドレス」と一致しているか
  • ユーザー認証用の有効期限つきトークンが期限内か

のダブルチェックを行います。

署名に使うメッセージの内容を再考する

「このメッセージは◯◯で利用します」などを含めるように作り直します。
例:「xxさん、お帰りなさい。このメッセージを署名してログインします。このメッセージは◯◯のユーザー認証でのみ利用します。[ユーザー認証用の有効期限つきトークン]」

ユーザー認証設計・改

というわけで、先ほどのシーケンスを以下のように変更しました。

署名前、バックエンドに「ユーザー認証用の有効期限つきトークン」を要求し、それを署名に利用。
署名データをバックエンドに投げる際に上記トークンも一緒に投げるようにします。
バックエンドでは署名データとトークン2つ検証をします。
これで、最初の設計よりも安全なユーザー認証ができるようになりました。

さいごに

ユーザー登録が必要な DApps では「ユーザーが指定したアドレスを本当にそのユーザーのものとして信用して良いのか?」という懸念がついて回りますが、この web3.eth.personal.sign を使えばより安心安全にアドレスの持ち主として証明でき、ユーザー認証に利用することができます。
引き続き DRIP ではより良い DApps の考案や DApps 開発の上での課題解決などに取り組み、ブロックチェーン領域での価値創造を目指します!
また DRIP では共に新規事業の種を見つけるための仲間を募集中です。興味ある方はぜひこちらからご連絡ください!