アルゴリズムとかオーダーとか

仕事で勉強したことなどをまとめてます

ERC-712 ヒューマンリーダブルな型付署名の提案

今回はEthereumに新しく機能を追加する提案であるERC-712を紹介します。
この提案はRPCにすでにあるeth_signをさらに拡張した新しい機能を追加する提案です。μRaidenでも疑似的に実装して利用しています。
github.com

簡単な要約

Ethereumキーを使用して機械で検査可能で人間が判読可能な型付きデータ署名の標準。

要旨

Ethereum クライアントはeth_sign RPC 呼出でUTF-8文字列に署名する機能を提供しますが、署名済みのデータが文字列でない場合、DAppの実際の項目を表示するための十分なメタデータはこの呼出しでは提供されません。将来的にはオフチェーン成分(例えば、状態チャンネル)を含むプロトコルを使用し、Ethereum上の値が伝えられることが期待されます。これはプロトコルでEthereumのキーを使用して複雑なデータ構造に署名することが含まれているため、人間が読み取り可能でPC検証が可能な署名フローがユーザーのセキュリティに重要です。

このEIPでは、任意の型指定されたデータの配列に署名するRPCメソッドが追加されているため、署名UIはこのデータを人間が読める形式で表示することが容易に、署名する内容を100%自信を持って表現でき得られた署名を機械で検証可能です。さらに、悪意のあるDAppが、署名者が署名する前にtypedDataをハッシュするために、署名者が代替データをユーザーに提供する余地はありません。

このメソッドは、署名前にユーザーの承認が必要な署名UI(MetaMask、MEW、Parity signer、Ledger、Trezor)を持つEthereumクライアントでのみ実装する必要があります。承認が既に与えられているクライアントでは実装する必要はありません(例:ロックされていないアカウントのGeth)。

動機

Ethereumには、より高水準のプロトコルが登場しています。状態チャンネル、0xプロトコル、ログインプログラムなど...スケーラビリティ上の理由から、これのプロットコールは、いくつののプロトコル固有のデータにオフフリンで署名したと考えています。次は、署名されたメッセージを使用して、重要な結果を伴ううえ、他のアクションをトリガーします。

  • ETHの転送
  • Tokenの転送
  • 所有権の移行
  • 等...

現在のeth_sign実装では、型や構造を指定せずに任意のデータに署名することができます。署名者の中には、データがUTF-8文字列であると仮定しているものがあります。一部の署名者はそれを16進数でエンコードされた文字列として表示します。これは、UTF-8文字列が提供されていない場合、署名しているメッセージを確認することができないためユーザーの混乱を招きます。

Metamaskでeth_signを呼び出すと、署名する文字列が表示されます。ユーザーが複数の重要な情報を含む複雑な構造をハッシングした結果に署名する場合、署名しているものを確認する唯一の方法は、独立したスクリプトで同じ構造を再ハッシュし、ハッシュが一致することを確認することです。

Metamaskのpersonal_signをハッシュの生のバイト(例えばASCII文字列ではない)で呼び出すと、ユーザは検証の難しい(文字化けした)データに署名しなければいけません。

主な問題は、signerがDAppの本意を効果的に表示するのに十分なメタデータを持っていないことです。これを行うには、dAppがユーザにハッシュして署名してもらいたいデータの平文入力が必要です。

仕様

このEIPでは、eth namespaceに新しいJSON RPCメソッドeth_signTypedDataを提案します。
指定された型と人間が読める名前とともに値の配列を受け取ります。 jsonスキーマドラフトを以下のように定義します。

Params JSON Schema:

{
  items: {
    properties: {
      name: {type: 'string'},
      type: {type: 'string'}, // Solidity type as described here: https://github.com/ethereum/solidity/blob/93b1cc97022aa01e7daa9816bcc23108bbe008b5/libsolidity/ast/Types.cpp#L182
      value: {
        oneOf: [
          {type: 'string'},
          {type: 'number'},
          {type: 'boolean'},
        ],
      },
    },
    type: 'object',
  },
  type: 'array',
}

Example params:

// For this state channel POC: https://medium.com/@matthewdif/ethereum-payment-channel-in-50-lines-of-code-a94fad2704bc

typedData = [
    {
      "name": "channel",
      "type": "address",
      "value": "0xb088a3Bc93F71b4DE97b9De773e9647645983688",
    },
    {
      "name": "value",
      "type": "uint",
      "value": 42,
    },
];

siner UIで同表示されるか

スキーマを署名の一部にすることが重要です(説明は「理論的根拠」のセクションにあります)。スキーマがユーザーと署名されたハッシュを生成するための値と組み合わされる方法を以下に示します。最初に、スキーマは、強固なイベントシグネチャと同様のメソッドを使用して文字列にエンコードされます。その後、データ配列のkeccak256ハッシュとともにハッシュされます。

疑似コードの例:

// Client-side code example
const typedData = [
  {
    'type': 'string',
    'name': 'message',
    'value': 'Hi, Alice!',
  },
  {
    'type': 'uint',
    'name': 'value',
    'value': 42,
  },
];
const signature = await web3.eth.signTypedData(typedData);
// Signed code JS example
import * as _ from 'lodash';
import * as ethAbi from 'ethereumjs-abi';

const data = _.map(typedData, 'value');
const types = _.map(typedData, 'type');
const schema = _.map(typedData, entry => `${entry.type} ${entry.name}`);
const hash = ethAbi.soliditySHA3(
  ['bytes32', 'bytes32'],
  [
    ethAbi.soliditySHA3(_.times(typedData.length, _.constant('string')), schema),
    ethAbi.soliditySHA3(types, data),
  ],
);
// Solidity example
string message = 'Hi, Alice!';
unit value = 42;
const hash = keccak256(
  keccak256('string message', 'uint value'), // Probably hardcoded
  keccak256(message, value),
);
address recoveredSignerAddress = ecrecover(hash, v, r, s);

署名はeth_signと同じ形式である、ecSignatureパラメータが連結された(r + s + v)の16進数でエンコードで返されます。

根拠

Ethereumクライアントでの署名サポートは、オフチェーントランザクションで最も役に立ちます。EthereumのOn-chainトランザクションは、署名者UIがトランザクションに関する有用な情報をユーザに示すことを可能にする明確な型付きスキーマ(gas uint、gasPrice uintなど)を持っています。オフ・チェーン取引でも同じことが必要です。

Solidity keccak256関数は、型付き引数の配列を受け取れ、このデザインの動機付けになります。

この変更の主な点と最も重要な点は、信頼されていないDAppを使用していても、Signer UIにユーザーに十分な情報を提供して署名対象を確認できる機会を与えることです。DAppは署名者に読みにくいハッシュを提供できなくなり、ユーザーが署名する正確な値(プレーンテキスト)を提供しなければいけないため、悪意のある行動(トランザクションなど)に署名する危険性は大幅に減ります。

スキーマ情報は、DAppによって供給される対応するタイプおよび名前のデータが示されたことを検証者が確信できるように、必ずデータと共に署名されます。

このアプローチは、小さな画面のハードウェアウォレット(Ledger Nano S、Trezor)で実装することさえ可能です。ユーザーは一度に1行ずつ表示されます。(現在の取引について)

下位互換性

このEIPは下位互換性を損なわない。

eth_signTypedDataがその機能のスーパーセットであっても、下位互換性の理由からeth_signおよびpersonal_signは削除されません。しかし、現行のオフ・チェーン・プロトコルでは、スマート・コントラクト内の検証コードを変更してスキーマを組み込む必要があります。

eth_signTypedDataを使用した単純な文字列の署名の例

const signature = await web3.eth.signTypedData([
  {
    'type': 'string',
    'name': 'message',
    'value': 'Hi, Alice!',
  },
]);

eth_signTypedDataの実装では、使用されている暗号プリミティブ(tightly-packed + keccak256 + secp256k1)についていくつかの前提がありますが、keccak256関数についても同様の前提があります。 Ethereumが今後より暗号非依存になり、他のタイプの署名が可能になる場合、このEIPは調整できます。

keccak256の選択は、sha3と比較してsmart contractで検証するのが2倍安いためです。

テストケース

まだ準備はできていません。
適切なテストベクタスイートは、次のファイナライズステップで追加されます。

実装

これは、Metamask 3.11.0以降の実験的な機能として実装されています。
web3.jsの一部ではないので、次のように使ってみてください:

const data = {
  'type': 'string',
  'name': 'message',
  'value': 'Hi, Alice!',
};
web3.currentProvider.sendAsync({method: 'eth_signTypedData', params: [data, signerAddress], jsonrpc: '2.0', id: 1}, callback);

まとめ

結構ボリューミーな提案ですが、内容は非常に分かりやすいです。この提案が実装されたら、例えばERC-223 TokenなどのtokenFallback関数のamountが正しい値になっているのかなどが保証できます。また、いくつか先行実装のあるtransaction発行の肩代わり(meta transaction的な?)についても、署名の作成方法を統一できたりといろいろと便利になりそうです。

あと、eth_signでは(Gethに限るのかもしれないけど)署名を生成するときにprefixとして"\x19Ethereum Signed Message:\n"を自動で付与するので、eth_signTypedDataでこのprefixを付与しないのであれば下位互換というより別物的な扱いになると思います。
この機能は署名作成と検証部分のみであれば現状でもprefixに気を付ければeth_signを使って疑似的に実装できます。