# [ERC6327] パスワードで安全にデータ署名する仕組みを理解しよう！

> Elastic Signature（以下、ES）は、人が覚えやすい秘密情報を使ってデータへ署名できる仕組みです。

- URL: https://cardene.dev/articles/b2b6b86dbd2c4e550046
- 公開日: 2025-11-17
- 更新日: 2025-11-17
- トピック: Blockchain
- タグ: ethereum, solidity, smartcontract, ブロックチェーン

## はじめに

『DApps開発入門』という本や色々記事を書いているかるでねです。

https://amzn.asia/d/gxvJ0Pw

今回は、パスワードを使って秘密鍵のように取引データへ安全な署名を行い、より直感的で管理しやすい認証体験を実現しようとする仕組みを提案している**ERC6327**についてまとめていきます！

:::message
以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。
:::

https://eips.ethereum.org/EIPS/eip-6327

他にも様々なEIP・BIP・SLIP・CAIP・ENSIP・RFC・ACPについてまとめています。

https://cardene.notion.site/ERC-EIP-2a03fa3ea33d43baa9ed82288f98d4a9?pvs=4

## 概要

Elastic Signature（以下、ES）は、人が覚えやすい秘密情報を使ってデータへ署名できる仕組みです。
ここでいう「**秘密情報**」は、一般的な暗号資約で使われる固定長の秘密鍵ではなく、ユーザーが自由に決められるパスワードのようなものです。
ESの大きな特徴は、秘密情報そのものを保存せず、ゼロ知識証明（秘密を明かさずに知っていることだけを証明する仕組み）を使って完全にオンチェーンで検証できる点です。

ユーザーは必要に応じて好きなタイミングで秘密情報を変更でき、秘密情報の長さも制限されません。
つまり、従来の秘密鍵のように「**一度流出したら終わり**」という状況を避けることができます。
特に、ブロックチェーンに慣れていないユーザーにとって、秘密鍵よりもパスワードの方が理解しやすいという点が大きなメリットになります。

ESは、この仕組みをスマートコントラクトで扱えるようにするための標準インターフェースを定義するものです。
これにより、ESを利用した署名検証や操作の許可が、あらゆるDappやウォレットから安全に利用できるようになります。

## 動機

ESが実現しようとしているのは、「**変更可能な秘密鍵**」という新しい概念です。
ブロックチェーンを利用する時、多くの人がつまずくポイントは秘密鍵の扱いです。
秘密鍵は非常に強力ですが、紛失・流出すると取り返しがつかず、初心者にとっては心理的なハードルが高いものです。

これまでの対策としては、秘密鍵を預かるサービス（カストディアル型ウォレット）がありましたが、これらはサービス提供者に依存してしまいます。
つまり、 decentralization（完全な分散性）とは相性がよくありません。

ESは、ゼロ知識証明を使うことで「**ユーザー自身が管理でき、しかも秘密鍵のように扱う必要のない署名方式**」を実現します。
ユーザーは秘密情報（パスワード）をただ覚えておくだけでよく、スマートコントラクト側はユーザーがその秘密情報を「**知っている**」という事実だけを安全に検証できます。

この仕組みによって、オンボーディング（初めてのDapp利用時の手順）が簡単になり、非技術ユーザーでも分散的な環境を直接扱えるようになります。
さらに、秘密情報をいつでも変更できるため、安全性に対する不安も軽減できます。

#### 利用例

ESは、従来の秘密鍵の代わりになるものではなく、秘密鍵に追加して使える署名方式です。
つまり「**どちらか一方**」という選択ではなく、両方を組み合わせて安全性を高めることができます。

#### DeFiアプリでの利用

DeFiアプリにESを組み込むと、送金など重要な操作を行う時に、ユーザーへ追加のパスワード入力を求めることができます。
もし秘密鍵が漏洩したとしても、ESで設定したパスワードを知らない限り不正操作はできません。
二段階認証のような役割を果たし、セキュリティを大幅に強化できます。

#### スマートコントラクトウォレットでの利用

Account Abstraction（**ERC4337**）のようなスマートコントラクトウォレットでは、柔軟に署名方式を追加できます。

:::message
**ERC4337**については以下の記事を参考にしてください。
:::

[\[ERC4337\] プロトコルの変更をせずにAAを実装する仕組みを理解しよう！](/articles/8c97ca7c93c4557ebfc2)

そこにESを組み込むことで、以下のような使い方が可能になり、ブロックチェーン初心者でもストレスなくDappを使えるようになります。

* 初回利用時にパスワードを設定するだけでよい
* 秘密鍵を保管する必要がない
* 秘密情報はいつでも変更可能

特に、「**パスワードで使える分散ウォレット**」という形は、既存のWebサービスに近い感覚で利用できるため、ユーザーの心理的な抵抗を大きく下げる効果があります。

## 仕様

ES（Elastic Signature）は、パスワードのような任意長の秘密情報を使い、その秘密を公開せずにゼロ知識証明で「**知っていること**」だけを示す仕組みです。
この署名方式では、パスワードは保存されず、オンチェーンで完全に検証できます。
パスワードは任意のタイミングで変更でき、従来の秘密鍵と併せて利用することでセキュリティを向上できます。

ESの検証には Verifier（検証者）、Requester（検証要求者）、Prover（証明生成者）の三者が関与します。

全体の流れは次のハッシュ計算を土台としています。

| 名称         | 説明                                              |
| ---------- | ----------------------------------------------- |
| `pwdhash`    | パスワードのハッシュ。     |
| `datahash`   | トランザクション対象データのハッシュ。     |
| `fullhash`   | `datahash` と既知の変数（`nonce` や `chainid` など）から計算されるハッシュ。 |
| `expiration` | 証明が有効な期限（タイムスタンプ）。    |
| `allhash`    | `fullhash` と `pwdhash` から計算されるハッシュ。 |

各者の役割は以下のとおりです。

#### Verifier（検証者）

* **Requester**が提供した `datahash` をもとに `fullhash` を計算する。
* 対象ユーザーのアドレス（EOA またはスマートコントラクトウォレット）から `pwdhash` を取得する。
* **Prover**が生成し、**Requester**が提出した `allhash` が正しいか検証する。

#### Requester（要求者）

* `datahash` を生成し、証明の期限 `expiration` を決める。
* **Prover** から受け取った `proof` および `allhash` を**Verifier**に提出する。

#### Prover（証明生成者）

* `datahash`、`expiration`、`nonce`、`chainid` などの既知の変数を使って `proof` と `allhash` を生成する。

#### 必要条件

* 既知の変数は全参加者に共有されている必要がある。
* `nonce` を含める必要がある。
* `chainid` を含める必要がある。
* **Verifier** 固有の変数を含めてもよい。
* 公開ステートメントには `pwdhash`・`fullhash`・`allhash` をそれぞれ反映する必要がある。
* `fullhash` の計算方法は**Verifier**と**Prover**が合意しておく必要がある。
* `datahash` の計算方法も明確に定義される必要がある。

### IElasticSignature インターフェース

```javascript
pragma solidity ^0.8.0;

interface IElasticSignature {
    event SetPassword(address indexed user, uint indexed pwdhash);

    event Verified(address indexed user, uint indexed nonce);

    function pwdhashOf(address user) external view returns (uint);

    function resetPassword(
        uint[8] memory proof1,
        uint expiration1,
        uint allhash1,
        uint[8] memory proof2,
        uint pwdhash2,
        uint expiration2,
        uint allhash2
    ) external;

    function verify(
        address user,
        uint[8] memory proof,
        uint datahash,
        uint expiration,
        uint allhash
    ) external;
}
```

#### イベント

##### `SetPassword`

```javascript
event SetPassword(address indexed user, uint indexed pwdhash);
```

ユーザーが自身のパスワードハッシュを設定または再設定した時に発行されるイベント。
ユーザーのアドレスに紐づけて、パスワードを変更したことを通知します。
EOAだけでなく、スマートコントラクトウォレットでも利用できます。

**パラメータ**

* `user`
  * パスワードハッシュを設定したユーザーアドレス。

* `pwdhash`
  * 設定されたパスワードのハッシュ。

##### `Verified`

```javascript
event Verified(address indexed user, uint indexed nonce);
```

ユーザーが提出した proof の検証に成功した時に発行されるイベント。
パスワードによる認証が成功した際に、新しい `nonce` を発行し、再利用されないように管理します。
`nonce` はゼロ知識証明における再利用防止のための重要な要素です。

**パラメータ**

* `user`
  * 検証の対象となったユーザーアドレス。

* `nonce`
  * 新しく発行された `nonce`。

#### 関数

##### `pwdhashOf`

```javascript
function pwdhashOf(address user) external view returns (uint);
```

指定したユーザーアドレスに登録されている `pwdhash` を取得する関数で
Verifier が proofの検証を行う時に、対象ユーザーに対応するパスワードハッシュを取り出すために使われます。

**引数**

* `user`
  * パスワードハッシュを取得したいユーザーのアドレス。

**戻り値**

* `pwdhash`
  * 指定アドレスのパスワードハッシュ。

##### `resetPassword`

```javascript
function resetPassword(
    uint[8] memory proof1,
    uint expiration1,
    uint allhash1,
    uint[8] memory proof2,
    uint pwdhash2,
    uint expiration2,
    uint allhash2
) external;
```

古いパスワードから新しいパスワードへ安全に更新するための関数。
1つ目の proof（`proof1`）で旧パスワードを知っていることを証明し、2つ目の proof（`proof2`）で新しいパスワードに対応する正しい証明を提示します。
この2段階の検証により、パスワード変更が正当なユーザーによって行われていることを保証します。

**引数**

* `proof1`
  * 旧パスワードで生成した `proof`。

* `expiration1`
  * 旧パスワードの証明が有効である期限。

* `allhash1`
  * 旧パスワードから生成された `allhash`。

* `proof2`
  * 新パスワードで生成した `proof`。

* `pwdhash2`
  * 新しいパスワードのハッシュ。

* `expiration2`
  * 新パスワードの証明が有効である期限。

* `allhash2`
  * 新パスワードで生成した `allhash`。

##### `verify`

```javascript
function verify(
    address user,
    uint[8] memory proof,
    uint datahash,
    uint expiration,
    uint allhash
) external;
```

他のコントラクトから呼び出され、ユーザーが生成した `proof` を検証する関数。
トランザクションを発行する側（例：DeFi アプリ）が `datahash` を計算し、ユーザーが提示した `proof` と `allhash` をこの関数へ渡します。
**Verifier**は内部で `fullhash` を再計算し、`allhash` が正しいかをチェックします。
期限切れ（`expiration` 超過）や不正な `allhash` であれば検証は失敗します。
成功時には `Verified` イベントが発行され、新しい `nonce` を更新します。

**引数**

* `user`
  * 検証対象のユーザーアドレス。

* `proof`
  * ユーザーがパスワードをもとに生成した証明。

* `datahash`
  * 操作対象データのハッシュ。

* `expiration`
  * 証明が有効である期限。

* `allhash`
  * `proof` とともに生成される公開ステートメント。

## 補足

#### `pwdhash` をコントラクトに保存する理由

ESでは、各ユーザーのパスワードそのものはどこにも保存しませんが、パスワードをハッシュ化した値である `pwdhash` はオンチェーンに保存します。
コントラクト側は「**このアドレスのユーザーは、こういう `pwdhash` を持っている**」という情報だけを持ち、それをもとにゼロ知識証明の検証を行います。

こうしておくことで、**Verifier**となるESコントラクトは、**Prover**が提出した `proof`（証明）や `allhash`（まとめたハッシュ）が、登録済みの `pwdhash` と整合しているかどうかを判断できます。
パスワードそのものは一切開示されず、ハッシュと証明だけを頼りに「**本物のユーザーが操作した**」と確認できるようになっています。

#### zkサーキットとハッシュの全体像

![image.png](/images/qiita-ace7ce2d-ed76-48cf-ad8b-284397a42719.webp)
https://eips.ethereum.org/EIPS/eip-6327

この図は、ユーザーの操作から zkサーキット、そして**Verifier**による検証までのデータの流れを表しています。

1. 左側にユーザーがいて、「`input`」と書かれたブロックに操作内容が入ります。
    例えば、トークンの種類（`token`）、送金・出金量（`amount`）などです。
2. これらの入力データから `datahash` が計算されます。
    図では `token`・`amount` などがまとまって `datahash` になっています。
3. さらに `expiration`（有効期限）、`chainId`（チェーン ID）、`nonce`（使い捨てカウンタ）などの値を組み合わせて `fullhash` が作られます。
    これは、「**どのチェーンで**」、「**いつまで有効な**」、「**どの操作か**」を一意に表すハッシュです。
4. 一方で、ユーザーのウォレットアドレスとパスワード（`pwd`）がzkサーキットに入力されます。
    図の上部には「`wallet address (…) → pwdhash`」と書かれており、このアドレスに対応する `pwdhash` がコントラクトに保存されていることを表しています。
5. zk サーキットは、
   * `address`
   * `pwd`
   * `fullhash`
     を入力として、
   * `proof`（ゼロ知識証明）
   * `pwdhash`（パスワードハッシュ）
   * `allhash`（`fullhash` と `pwdhash` をまとめたハッシュ）
     を出力します。
6. 右側では、**Verifier**コントラクトがすでに保存している `pwdhash` と、**Prover** が渡してきた `proof`、`allhash`、`fullhash` を使って `verify` を行います。
    ここで、「**このユーザーは登録されている `pwdhash` に対応するパスワードを知っていて、そのパスワードでこの `fullhash` を署名した**」という事実を検証します。

この図が示しているポイントは、「**ユーザーの入力はトランザクション内容とパスワードに分かれ、それらが zkサーキットの中で結びつき、最終的に `fullhash`・`pwdhash`・`allhash` と `proof` が生成される**」という流れです。

#### Poseidonを使ったzkサーキット内部の計算

![image.png](/images/qiita-1a31e324-2783-4424-95aa-6a58f7819dd5.webp)
https://eips.ethereum.org/EIPS/eip-6327

この図は、zkサーキットの内部ロジックを、ハッシュ関数 `Poseidon` を使ってどのように構成しているかを示しています。

`Poseidon` は、ゼロ知識証明に向いたハッシュ関数で、回路内で計算しやすいように設計されています。
この図では「**Poseidon 1**」、「**Poseidon 2**」として2段階のハッシュ処理になっています。

図の流れを整理すると以下のようになります。

1. 入力（`input`）として、以下のデータがzkサーキットに入ります。
    * `password`
    * `address`
    * `fullhash`
2. **Poseidon 1**では、`password` と `address` から `pwdhash` を計算します。
    図の破線は、`password と `address` が**Poseidon 1**に流れ込み、その出力が `pwdhash` として右側に出ていることを示しています。
3. **Poseidon 2**では、**Poseidon 1**の出力 `pwdhash` と `fullhash` を組み合わせて `allhash` を計算します。
    図中の緑色の矢印が、`fullhash` と `pwdhash` が Poseidon 2 に入力され、右側に `all hash` が出力される流れを表しています。
4. zkサーキット全体としては、以下を出力します。

   * `proof`（ゼロ知識証明）
   * `all hash`
   * `pwdhash`
   * `fullhash`

    `fullhash` は入力の一部でもありますが、出力側にも含めることで、**Verifier** が「**どのトランザクションに対する証明なのか**」を明確に扱えるようにしています。

この図が強調しているのは、`pwdhash` と `allhash` がサーキット内部のハッシュ計算から一貫して導かれており、**Verifier** は「**`Poseidon` でこう計算されているはずだ**」という前提で検証する、という点です。
つまり、**Prover** が勝手な `allhash` を作ることはできず、パスワードとアドレス、`fullhash` の組み合わせに従った値しか出せない構造になっています。

#### ワークフロー（User → Requester → ES コントラクト）

![image.png](/images/qiita-e80d3800-7ea5-43ba-bb42-2eb44fd66779.webp)

この図は、実際のフローを「**誰が何をどこへ渡すか**」という視点で描いたものです。
User（prover）、Third-party contract（requester）、ES contract（verifier）の三者が登場します。

1. 左側のUser（prover）は、例えば「**`withdrawal`（出金）**」操作をしようとします。
    ここでは `token` の種類と `amount`（数量）が入力として扱われています。
2. Userは、自分のパスワードと出金内容を使ってzkサーキットを実行していかを生成します。
   * `proof`
   * `allhash`

    図ではZKブロックから `proof` と `allhash` が出力されています。
3. Userは、`withdrawal` の内容とともに、`proof` と `allhash` を**Third-party contract（requester）**に送ります。
    **Third-party contract**は、例えばDeFiアプリのコントラクトのようなイメージです。
4. **Requester**コントラクトは、受け取った `token`・`amount` などから `datahash` を計算し、さらにユーザーアドレスを組み合わせて「**どのユーザーがどの操作をしたのか**」を表すハッシュを作ります。
   図の中央部分では、以下を右側の**ES contract**に渡しています。

   * `datahash`
   * `user address`
     と、User から来た
   * `proof`
   * `allhash`
5. ES contract（verifier）は、あらかじめ保存しているユーザーの `pwdhash` と、Requester から渡された `datahash` から `fullhash` を計算します。
    そして、以下を使って検証を行います。

    * `fullhash`
    * `pwdhash`
    * `proof`
    * `allhash`

    図の右端では、ES contract に `fullhash` と `pwdhash`、そこに `proof` と `allhash` が入力されている形で表現されています。
6. 検証に成功すれば、「**この出金操作は、登録済みパスワードを知っているユーザーによって署名された**」とみなすことができます。
    これにより、秘密鍵が知られているだけでは不十分で、パスワードによるES署名も満たしている場合にのみ資金移動が行われるようにできます。

## 互換性

ESによる署名検証は、すでに存在している従来の署名検証の仕組みと衝突しないように設計されています。

ここで強調されているのは、ESが扱うのは「**パスワードベースの署名**」だけであり、Externally Owned Account（EOA）の秘密鍵を使った従来の署名方式とは別レイヤーの仕組みだという点です。
そのため、既存のEOA署名検証ロジックを変更したり置き換えたりする必要はなく、追加の署名方式として並行して使うことができます。

言い換えると、既存のコントラクトやウォレットがEOA署名による検証を続けながら、必要に応じてESを組み込んでいくことができる設計になっています。

## 参考実装

```js
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` は以下です。

```c++
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` が公開される**」という前提を置いたうえで、総当たり攻撃（パスワードのハッシュから元のパスワードを推測する攻撃）にどれくらい耐えられるかを具体的な数字で示しています。

#### `pwdhash` が公開されることによるリスク

ESではパスワードそのものは保存されませんが、そのハッシュ値である `pwdhash` はオンチェーンに公開されます。
ハッシュ関数として `Poseidon` を使うことを前提に、例えばGPU（RTX 3090）で1 秒あたり1億回（100Mhash/s）のハッシュ計算ができると仮定し、「**どの程度の長さ・文字種のパスワードなら現実的に安全か**」を見積もっています。

#### 想定されるクラック時間の目安

提示されている条件と推定クラック時間を表にすると次のようになります。

| パスワード条件            | 推定クラック時間      |
| ------------------ | ------------- |
| 8文字（数字のみ）          | 1 秒           |
| 8文字（数字 + 英字）       | 25 日          |
| 8文字（数字 + 英字 + 記号）  | 594 日         |
| 12文字（数字のみ）         | 10,000 秒      |
| 12文字（数字 + 英字）      | 1,023,042 年   |
| 12文字（数字 + 英字 + 記号） | 116,586,246 年 |

ここで示されているのは、あくまでハッシュレート 100Mhash/s 程度の環境で総当たり攻撃を行った場合の「**目安**」です。
文字種が増えるほど、同じ文字数でも総当たりに必要な組み合わせ数が急激に増えることがわかります。

#### 秘密鍵との比較

次に、パスワードの強度と秘密鍵の強度の比較が示されています。

* 秘密鍵のクラック難易度は 2^256。
* 40文字のパスワード（数字 + 英字 + 記号）の総当たり難易度は 92^40。

ここで「`92`」というのは、「**数字 + 英字 + 記号**」を合わせた時に想定している文字集合のサイズです。
そして、

* `92^40 > 2^256`

となるため、40文字のパスワード（数字 + 英字 + 記号）であれば、理論上は一般的な 256bit 秘密鍵よりも総当たり攻撃に強い、という結論を示しています。

つまり、`pwdhash` が公開されている前提でも、十分に長く、かつ多様な文字種を使ったパスワードを選べば、秘密鍵と同等以上の総当たり耐性を持たせることができる、という考え方になっています。

## 引用

George (@JXRow), "ERC-6327: Elastic Signature [DRAFT]," Ethereum Improvement Proposals, no. 6327, January 2023. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-6327.

## 最後に

今回は「パスワードを使って秘密鍵のように取引データへ安全な署名を行い、より直感的で管理しやすい認証体験を実現しようとする仕組みを提案している**ERC6327**」についてまとめてきました！
いかがだったでしょうか？

質問などがある方は以下のTwitterのDMなどからお気軽に質問してください！

[Twitter @cardene777](https://twitter.com/cardene777)

他の媒体でも情報発信しているのでぜひ他も見ていってください！

https://chaldene.net/

https://zenn.dev/heku

https://cardene.substack.com/

https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58

https://cardene.notion.site/ERC-EIP-2a03fa3ea33d43baa9ed82288f98d4a9?pvs=4
