見出し画像

ERC-6327 弾性署名

パスワードを使ってデータを秘密鍵として署名する

目次

  概要
  動機
  使用例
  仕様
  IElasticSignature インターフェース
  理由
  後方互換性
  リファレンス実装
  セキュリティ
 

概要

エラスティック署名(ES)は、人間に優しい秘密でデータに署名することを目的としている。秘密は完全にオンチェーンで検証され、どこにも保存されない。ユーザーは必要であれば何度でも秘密を変更することができる。秘密は固定された長さを持たない。秘密はパスワードのようなもので、秘密鍵よりも理解しやすい概念である。これは特に非技術的なユーザーに当てはまる。このEIPは、ESによる操作の検証と認可を行うスマート・コントラクトのインターフェースを定義している。

動機

変更可能な「秘密鍵」によって何が可能になるのか?何年もの間、私たちはユーザー、特に技術的な経験の浅いユーザーにとっての導入障壁を下げる方法を模索してきた。秘密鍵のカストディ・ソリューションは、ユーザーフレンドリーなオンボーディング・エクスペリエンスを提供しているように見えますが、ベンダーに依存し、分散化されていません。ESは、ゼロ知識技術で画期的な進歩を遂げた。ユーザーは秘密を知っているという証明を生成し、スマート・コントラクトがその証明を検証する。

使用例

ESは代替署名アルゴリズムである。秘密鍵のどちらか一方の解決策ではない。ESは、秘密鍵署名の上に追加的な署名メカニズムとして機能するように設計されている。

  DeFiアプリはESを資金移動プロセスに利用できる。利用者は、取引を完了するためにパスワードを入力する必要がある。これにより、秘密鍵が漏洩した場合でも保護が強化される。
   ESはまた、Account Abstraction ERC-4337のようなスマートコントラクトウォレットのプラグインとして使用することもできる。秘密鍵の代わりに分散型パスワードが選ばれる。これは新しいイーサリアム・ダップ・ユーザーのスムーズなオンボーディング・エクスペリエンスにつながる可能性がある。

仕様

そうしよう:
pwdhash  は秘密鍵(パスワード)のハッシュを表す。
datahash   は意図したトランザクションデータのハッシュを表す。
fullhash   は datahash とすべてのよく知られた変数のハッシュを表す。
expiration   は、意図したトランザクションが期限切れになるタイムスタンプである。
allhashfullhashpwdhash のハッシュを表す。

検証者(Verifier)、要求者(Requester)、証明者(Prover)の3者が関与する。

 •ベリファイアは
   • ベリファイアは、リクエスタが提供するdatahashからfullhashを計算するべきである[SHOULD]。
    •与えられたアドレスに対して pwdhash を導出すべきである(SHOULD)。このアドレスはEOAまたはスマートコントラクトのウォレットである。
    •導出された pwdhash、計算されたフルハッシュ、および要求元から提出された allhash を用いて証明を検証するべきである(SHOULD)。

•リクエスタ
datahashを生成し、expirationを決定するべきである[SHOULD]。
•検証者に検証を要求しなければならない[SHALL]、
         •proof and allhashで検証者に検証を要求するものとする;
         •datahash;
         
expiration.

•ことわざ
•からproofallhashを生成すべきである。
datahashexpiration依頼者と合意したもの

       また、いくつかの条件もある。

  •よく知られた変数は、すべての関係者が利用可能であるべきである(SHOULD)。

          nonceを含むべきである(SHOULD)。
          chainid を含むべきである(SHOULD)。
          検証者固有の変数を含めてもよい。

パブリック・ステートメントには以下を含めるべきである(SHOULD)。
pwdhashを反映したもの;
Fullhash を反映したもの;
allhashを反映したもの。

fullhash の計算は、検証者と証明者の両方が合意すべきである[SHOULD]。•datahashの計算

IElasticSignature インターフェース

•これはベリファイアのインターフェイス。

pragma solidity ^0.8.0;

interface IElasticSignature {
    /**
     * Event emitted after user set/reset their password
     * @param user - an user's address, for whom the password hash is set. It could be a smart contract wallet address
     *  or an EOA wallet address.
     * @param pwdhash - a password hash
     */
    event SetPassword(address indexed user, uint indexed pwdhash);

    /**
     * Event emitted after a successful verification performed for an user
     * @param user - an user's address, for whom the submitted `proof` is verified. It could be a smart contract wallet
     *  address or an EOA wallet address.
     * @param nonce - a new nonce, which is newly generated to replace the last used nonce. 
     */
    event Verified(address indexed user, uint indexed nonce);

    /**
     * Get `pwdhash` for a user
     * @param user - a user's address 
     * @return - the `pwdhash` for the given address
     */
    function pwdhashOf(address user) external view returns (uint);

    /**
     * Update an user's `pwdhash`
     * @param proof1 - proof generated by the old password
     * @param expiration1 - old password signing expiry seconds
     * @param allhash1 - allhash generated with the old password
     * @param proof2 - proof generated by the new password
     * @param pwdhash2 - hash of the new password
     * @param expiration2 - new password signing expiry seconds
     * @param allhash2 - allhash generated with the new password
     */
    function resetPassword(
        uint[8] memory proof1,
        uint expiration1,
        uint allhash1,
        uint[8] memory proof2,
        uint pwdhash2,
        uint expiration2,
        uint allhash2
    ) external;

    /**
     * Verify a proof for a given user
     * It should be invoked by other contracts. The other contracts provide the `datahash`. The `proof` is generated by
     *  the user. 
     * @param user -  a user's address, for whom the verification will be carried out.
     * @param proof - a proof generated by the password
     * @param datahash - the data what user signing, this is the hash of the data
     * @param expiration - number of seconds from now, after which the proof is expired 
     * @param allhash - public statement, generated along with the `proof`
     */
    function verify(
        address user,
        uint[8] memory proof,
        uint datahash,
        uint expiration,
        uint allhash
    ) external;
}

verify関数は他のコントラクトから呼び出されるべきである(SHOULD)。他のコントラクトは、これを呼び出すためにdatahashを生成すべきである(SHOULD)。この関数は、allhashが正しく計算され、パスワードと正直に一致するかどうかを検証すべきです(SHOULD)。

理由

契約書には全員のpwdhashが保存される。


下図はZKの回路ロジックを示している。


署名を検証するには、proofallhashpwdhashfullhashが必要である。


証明者は公開出力とともにProofを生成する。証明者はそれらすべてをサードパーティのリクエスタ契約に送る。リクエスタはデータハッシュを生成する。リクエスタはdatahashproofallhashexpiration、およびプ ロバーのアドレスをベリファイアコントラクトに送信する。このコントラクトは、datahashがプ ロバーのものであること、つまり引き出し操作がプロバーのパスワードによって 署名されていることを検証する。

後方互換性

このEIPは、EOA署名ではなくパスワード・ベースの署名に特化した方法であるため、署名検証に関するこれまでの研究と後方互換性がある。

リファレンス実装

署名契約の実施例:

pragma solidity ^0.8.0;

import "../interfaces/IElasticSignature.sol";
import "./verifier.sol";

contract ZKPass is IElasticSignature {
    Verifier verifier = new Verifier();

    mapping(address => uint) public pwdhashOf;

    mapping(address => uint) public nonceOf;

    constructor() {
    }

    function resetPassword(
        uint[8] memory proof1,
        uint expiration1,
        uint allhash1,
        uint[8] memory proof2,
        uint pwdhash2,
        uint expiration2,
        uint allhash2
    ) public override {
        uint nonce = nonceOf[msg.sender];

        if (nonce == 0) {
            //init password

            pwdhashOf[msg.sender] = pwdhash2;
            nonceOf[msg.sender] = 1;
            verify(msg.sender, proof2, 0, expiration2, allhash2);
        } else {
            //reset password

            // check old pwdhash
            verify(msg.sender, proof1, 0, expiration1, allhash1);

            // check new pwdhash
            pwdhashOf[msg.sender] = pwdhash2;
            verify(msg.sender, proof2, 0, expiration2, allhash2);
        }

        emit SetPassword(msg.sender, pwdhash2);
    }

    function verify(
        address user,
        uint[8] memory proof,
        uint datahash,
        uint expiration,
        uint allhash
    ) public override {
        require(
            block.timestamp < expiration,
            "ZKPass::verify: expired"
        );

        uint pwdhash = pwdhashOf[user];
        require(
            pwdhash != 0,
            "ZKPass::verify: user not exist"
        );

        uint nonce = nonceOf[user];
        uint fullhash = uint(keccak256(abi.encodePacked(expiration, block.chainid, nonce, datahash))) / 8; // 256b->254b
        require(
            verifyProof(proof, pwdhash, fullhash, allhash),
            "ZKPass::verify: verify proof fail"
        );

        nonceOf[user] = nonce + 1;

        emit Verified(user, nonce);
    }

    /////////// util ////////////

    function verifyProof(
        uint[8] memory proof,
        uint pwdhash,
        uint fullhash, //254b
        uint allhash
    ) internal view returns (bool) {
        return
            verifier.verifyProof(
                [proof[0], proof[1]],
                [[proof[2], proof[3]], [proof[4], proof[5]]],
                [proof[6], proof[7]],
                [pwdhash, fullhash, allhash]
            );
    }
}

verifier.solはsnarkjsによって自動生成され、circuit.circomのSource codeは以下の通りです。

pragma circom 2.0.0;

include "../../node_modules/circomlib/circuits/poseidon.circom";

template Main() {
    signal input in[3];
    signal output out[3];

    component poseidon1 = Poseidon(2);
    component poseidon2 = Poseidon(2);

    poseidon1.inputs[0] <== in[0];  //pwd
    poseidon1.inputs[1] <== in[1];  //address
    out[0] <== poseidon1.out; //pwdhash

    poseidon2.inputs[0] <== poseidon1.out;
    poseidon2.inputs[1] <== in[2]; //fullhash
    out[1] <== in[2]; //fullhash
    out[2] <== poseidon2.out; //allhash
}

component main = Main();


セキュリティ

pwdhashは公開されているので、パスワードをクラックすることが可能です。RTX3090のPoseidonハッシュレートは100Mhash/sと推定される:
8文字(数字): 1秒

8文字(数字+英語) : 25日

8文字(数字+英語+記号) : 594日

12文字(数字): 10000秒

12文字(数字+英語): 1023042年

12文字(数字+英語+記号) : 116586246年

秘密鍵のクラック難易度は2^256、40文字(数字+英字+記号)のクラック難易度は92^40で、92^40>2^256なので、パスワードが40文字の場合、秘密鍵よりもクラックが難しい。



この記事が気に入ったらサポートをしてみませんか?