# [ERC7913] 鍵の種類を問わない署名の新しい仕組みを理解しよう！

> Ethereumでは、**Externally Owned Account（EOA）**は対応する秘密鍵でメッセージに署名できます。

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

## はじめに

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

https://amzn.asia/d/gxvJ0Pw

今回は、Ethereumアドレスを持たない鍵（メール署名、RSA、P-256など）でも、共通のVerifierコントラクトを使って安全に署名を検証できるようにする仕組みを提案している**ERC7913**についてまとめていきます！

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

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

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

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

## 概要

Ethereumでは、**Externally Owned Account（EOA）**は対応する秘密鍵でメッセージに署名できます。
また、**ERC1271**では、マルチシグ（複数署名）などのスマートアカウントが署名を検証するための方法が定義されています。どちらの場合も、署名者（signer）の「正体（identity）」はEthereumアドレスとして表現されます。

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

[\[ERC1271\] コントラクトで署名を検証する仕組みを理解しよう！](/articles/da237b1b8cc185491512)

**ERC7913**は、この「**署名者の記述方法**」と「**署名検証の仕組み**」を、Ethereumアドレスを持たない鍵にも拡張することを目指しています。
つまり、Ethereum上で自分のアドレスを持たないような鍵（非Ethereum型の鍵）でも署名者として扱えるようにするということです。

この新しい仕組みを使うと、以下のような多様な署名者をEthereumの世界に統合できます。

* **非Ethereum系の暗号曲線**（例：secp256r1）
* **ハードウェアデバイス**（例：スマートフォンや専用セキュリティチップ）
* **メールアドレスなどのWeb2由来の認証手段**

特に、「**ソーシャルリカバリー（social recovery）**」を行うスマートアカウントなどでは、Ethereumアドレスを持たない署名者の取り扱いが重要になります。

## 動機

#### 背景

Ethereumではこれまで、`secp256k1`という暗号曲線を使って署名を行うのが基本でした。
しかし、**アカウント抽象化（Account Abstraction）**の発展によって、「**署名の仕組みをEthereum以外にも拡張したい**」というニーズが高まっています。

例えば以下です。

* **secp256r1曲線**
    多くのスマートフォンやハードウェアデバイスが標準でサポートしている。
* **RSA鍵**
    企業や金融機関など、伝統的な組織で広く使われている。
* **ZK（ゼロ知識）技術による署名**
    メールアドレスやJWT（JSON Web Token）を使って署名する、新しいWeb2連携の仕組みも登場。

これらの署名方式には共通点があります。
それは「**Ethereum上で固有のアドレスを持たない**」という点です。

#### 現行モデルの課題

これまでの仕組みでは、署名者をEthereumアドレス（＝EOAまたはコントラクトアドレス）として認識していました。
そのため、もし他の暗号アルゴリズムの鍵を使いたい場合、それぞれの鍵ごとに**ERC1271**互換コントラクトをデプロイして対応する必要がありました。

しかし、このやり方には以下の問題があります。

* 各鍵に対してコントラクトをデプロイするのはコストが高く非効率。
* 鍵ごとにアドレスを割り当てると、ユーザーが誤って資産を送金するなどの混乱の原因になる。
* **アカウントアドレス（資産を保持する）**と**署名鍵（アカウントを制御する）**を明確に分離したいというアカウント抽象化の目的に反する。

#### 提案する解決策化

提案されている新しい仕組みでは、各鍵に専用のコントラクトを作る代わりに、共通の検証コントラクト（verifier contract）をいくつか用意し、そのコントラクトがさまざまな形式の署名を標準的な方法で処理できるようにします。

* **(verifier, key)** のペアによって署名者を表現。
* 各鍵にアドレスを割り当てる必要がなく、セットアップコストが不要。
* すでにデプロイ済みのverifierを使えば、どんな署名形式の鍵でも簡単に扱える。

この構造によって、Ethereumアドレスを持たない署名者でも柔軟に権限を与えることができます。

* ソーシャルリカバリーの回復者（recovery guardian）としてメールアドレスを登録する。
* スマートアカウントの操作を特定のハードウェアデバイスに限定する。
* 外部認証サービス（JWTやOAuthなど）を使ったアクセス制御を行う。

これらはすべて、署名者を「**アドレス**」ではなく「**バイト列（bytes object）**」として識別することで実現できます。

#### 従来モデルとの互換性

この新しい定義は、既存のEOAおよび**ERC1271**コントラクトとも互換性（backward compatible）を保っています。

| 署名者の種類            | Verifier      | Key        |
| ----------------- | ------------- | ---------- |
| EOA（通常のウォレット）     | 署名者自身のアドレス    | 空（空のbytes） |
| ERC-1271コントラクト    | コントラクトのアドレス   | 空（空のbytes） |
| 非Ethereum鍵（例：RSA） | 検証コントラクトのアドレス | 鍵のバイト列     |

このように、既存の仕組みを壊すことなく、新しい形式の署名者を扱うことができます。

## 仕様

### 命名法

仕様の中で登場する「**キー（key）**」と「**検証者（verifier）**」という用語は、以下のように定義されています。

#### キー（Key）

**Key** は任意の長さの **bytesオブジェクト（バイナリデータ）** で表現されます。
例としては、P-256曲線やRSAの公開鍵などが挙げられます。
Keyは署名の検証に必要な情報を持ちますが、Ethereumアドレスを持たないこともあります。
Keyは空にすることはできないです。
つまり、空の鍵を指定することは不正であり、署名の検証に使うことはできません。

#### 検証者（Verifier）

**Verifier** は、ある特定の種類の鍵に対して署名を検証する責任を持つスマートコントラクトです。
Verifier 自体はEthereum上に存在し、**Ethereumアドレス**を持ちます。
例えば、RSA鍵用、secp256r1鍵用、メール署名用など、異なる署名方式ごとに専用のVerifierコントラクトを用意できます。

このように、Verifierは「**署名検証の仕組みを提供する共通の窓口**」として機能します。

### 署名検証インターフェース

Verifierコントラクトは、以下のインターフェースを必ず実装する必要があります。

```js
interface IERC7913SignatureVerifier {
  /**
   * @dev Verifies `signature` as a valid signature of `hash` by `key`.
   *
   * MUST return the bytes4 magic value 0x024ad318 (IERC7913SignatureVerifier.verify.selector) if the signature is valid.
   * SHOULD return 0xffffffff or revert if the signature is not valid.
   * SHOULD return 0xffffffff or revert if the key is empty
   */
  function verify(bytes calldata key, bytes32 hash, bytes calldata signature) external view returns (bytes4);
}
```

| 項目          | 内容                                   |
| ----------- | ------------------------------------ |
| `key` | 署名者を識別するための鍵データ。任意の長さのbytes形式で渡されます。 |
| `hash`      | 署名対象のデータのハッシュ値（`bytes32`）。           |
| `signature` | 検証対象の署名データ。                          |
| 戻り値  | `bytes4`型で、検証結果を示す特定のマジック値を返します。     |

#### 戻り値

- 署名が有効な場合は以下の固定値を返す必要があります。

   ```
   0x024ad318
   ```

   この値は `IERC7913SignatureVerifier.verify.selector` を表すもので、「**署名が有効である**」という標準的な証明として扱われます。

- 署名が無効な場合、またはkeyが空の場合は、次のいずれかの動作を行うことが推奨されています。

   * `0xffffffff` を返す
   * または、トランザクションを `revert`（失敗）させる

これにより、呼び出し側は返り値を確認するだけで、署名の有効・無効を簡単に判断できるようになります。

### ステートを持たない設計

仕様では、Verifierコントラクトはステートレスであるべきとされています。

つまり、コントラクト内部で状態（state）を保持せず、すべての検証が入力パラメータのみに基づいて行われることが望ましい、という方針です。

これにより以下の利点があります。

* 同じ入力に対して常に同じ結果を返す（**決定性**の確保）
* コントラクトの呼び出しコスト（gas）を削減できる
* セキュリティ面での脆弱性（状態依存のバグや悪用リスク）を減らせる

このように、Verifierは「**署名の真偽を判定する純粋関数**」のような役割を果たすことを想定しています。

## 補足

**ERC7913**のコアは、「**キーごとにERC1271の“アイデンティティコントラクト”をデプロイしなくても、Verifier（検証コントラクト）を使えば同等の検証ができる**」という点です。
各キー専用のコントラクトを用意すると、デプロイのたびにガス代がかかり、運用も複雑になります。
Verifier方式なら、すでに用意された少数のコントラクトに署名検証ロジックを集約でき、新しいキーを使うたびのデプロイコストはゼロになります。

さらに、**ERC1271**が「**キーが不要なケース**」をすでにカバーしている点にも注意します。
例えば、署名者がコントラクト（マルチシグなど）で、そのコントラクト自身が署名検証を提供する場合、外部のキー情報は要りません。
ここで曖昧さを避けるために、Verifierコントラクト側では空のキー（empty key）をサポートしない／想定しないのが望ましい、という整理にしています。
長さ20バイトの署名者（＝アドレスのみ、キーは空）は、**ERC1271**またはEOAのecrecoverに委ねるのが正しい振る舞いです。

メッセージ形式については、既存の`ecrecover`や**ERC1271**との整合性から、入力は`bytes32`のハッシュにそろえます。
一般的には、**ERC191**や**ERC712**に従ってメッセージから`bytes32`ハッシュを作ります。

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

[\[ERC191\] 署名済みデータを安全に使用する仕組みを理解しよう！](/articles/ee4edd15ad6fe7f58f7e)

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

[\[EIP712\] 型付き構造化データのハッシュ化と署名の仕組みを理解しよう！](/articles/f54bc85cb66de5a54257)

もし他の暗号方式で別のハッシュ規則を使う場合でも、ここで受け取る`bytes32`を“メッセージ”として扱い、必要ならその標準に従って再ハッシュする、という立て付けです。
こうしておくと、呼び出し側の統一性（`bytes32`固定）と、Verifier側の柔軟性（内部での再ハッシュ可）を同時に満たせます。

## 互換性

この仕組みは、**ERC7913**スタイルの署名者**と、従来の**EOA**や**ERC1271**を並存させるための具体的な手順を定めています。
考え方はシンプルで、**署名者（signer）**を「**verifierアドレス || key**」というバイト列の連結として表現します。
ここでsignerの最小長は20バイト（= Ethereumアドレスの長さ）です。

#### 署名者（signer）のエンコード

| 項目 | 形式      | 説明          |
| ---------- | ---------------- | ------------ |
| `signer`   | `bytes` | 先頭20バイトが`verifier`アドレス、残りが`key`（空の場合あり） |
| `verifier` | `address`（20バイト） | 署名検証を行うコントラクト、またはEOA/コントラクトのアドレス        |
| `key`  | `bytes` | 非EOA鍵などを表すバイト列。EOA/ERC-1271のときは**空** |

##### 例

* **EOAやERC1271コントラクト**を表す署名者
  `signer = <verifier=0xAbCd...20bytes> || <key=空>`（計20バイト）
* **非Ethereum鍵（例：RSA、P-256）**を表す署名者
  `signer = <verifier=0xVerif...20bytes> || <key=公開鍵などのbytes>`（20バイト＋nバイト）

#### 検証アルゴリズム

与えられた `signer`、メッセージハッシュ `hash`（`bytes32`）、署名 `signature` を使って、以下の手順で検証を行います。

```py
if signer.length < 20:
    fail  // 署名者が20バイト未満は不正

(verifier, key) = (signer[0:20], signer[20:])  // 先頭20バイトがverifier、残りがkey

if key is empty:
    // verifierは“アイデンティティ”（EOAまたはERC-1271コントラクト）として扱う
    if verifierアドレスにコードがある:
        // コントラクト → ERC-1271の isValidSignature(hash, signature)
        // 期待されるmagic value（0x1626ba7e）が返れば成功、そうでなければ失敗
        return isValidSignature(verifier, hash, signature)
    else:
        // EOA → ecrecover(hash, signature) == verifier なら成功
        return (ecrecover(hash, signature) == verifier)
else:
    // keyが非空 → ERC-7913 Verifierに委譲
    // IERC7913SignatureVerifier(verifier).verify(key, hash, signature)
    // 0x024ad318 が返れば成功、それ以外は失敗
    return (IERC7913SignatureVerifier(verifier).verify(key, hash, signature) == 0x024ad318)
```
## 参考実装

```js
import {SignatureChecker} from '@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol';

/// @dev Extention of openzeppelin's SignatureChecker library
library SignatureCheckerExtended {
  function isValidSignatureNow(bytes calldata signer, bytes32 hash, bytes memory signature) internal view returns (bool) {
      if (signer.length < 20 ) {
        return false;
      } else if (signer.length == 20) {
        return SignatureChecker.isValidSignatureNow(address(bytes20(signer)), hash, signature);
      } else {
        try IERC7913SignatureVerifier(address(bytes20(signer[0:20]))).verify(signer[20:], hash, signature) returns (bytes4 magic) {
          return magic == IERC7913SignatureVerifier.verify.selector;
        } catch {
          return false;
        }
      }
  }
}
```

## セキュリティ

## ＃# 永続的な信頼性を確保するための原則

Signerが将来にわたって有効であり続けるためには、署名の検証を担うVerifierコントラクトが「**信頼不要（trustless）**」であることが重要です。
信頼不要とは、特定の管理者や権限者の判断に依存せず、誰でも検証結果を再現できる状態を指します。
そのため、仕様上では以下のように推奨されています。

| 要素   | 推奨事項   | 説明        |
| ----------------- | ------------ | -------- |
| **Verifierの設計方針** | **アップグレード不可能（non-upgradeable）であるべき** | 署名者が何年後に使われても、検証ロジックが途中で改変されないようにする。 |
| **長期的有効性の保証**     | コントラクトの挙動や依存関係が時間経過で変わらないようにする。 | 署名の検証基準が後から変わると、過去の署名が無効になるリスクがあるため。 |

特に、「**アップグレード可能コントラクト（upgradeable contract**）」は、管理者によってコードが後から書き換えられる可能性があるため、セキュリティの観点から非推奨とされています。

#### ステートに依存しない（Stateless）Verifierの推奨

Verifierは、デプロイ後に変化しうる情報や値に依存すべきではありません。
つまり、署名検証に関わるロジックは完全に静的（immutable）であるべきということです。

Solidityの観点で言えば、`verify`関数はpure関数であることが推奨されます。
pure関数とは、以下のような特徴を持つ関数です。

* コントラクトのステート変数（storage）を読み書きしない
* 外部環境（`block.timestamp`, `msg.sender` など）に依存しない
* 同じ入力を与えれば、常に同じ出力を返す（決定性）

この特性を持つことで、Verifierの動作は時間・環境・誰が呼び出しても同じ結果になるため、長期的な一貫性が保証されます。

#### 安全なパラメータ設計

Verifierが署名検証を行う時に使用するすべてのパラメータは、「**キー（key）**」または「**コントラクトの定数**」として固定されている必要があります。

| パラメータの扱い  | 説明    |
| -------------- | ------------------- |
| **key**            | 検証対象となる公開鍵や識別情報など。署名者によって提供され、入力として渡される。                        |
| **immutable code** | コントラクトに埋め込まれた変更不可能な定数やロジック。デプロイ後に変更されない。                        |
| **可変な外部状態**        | **使用すべきではない（SHOULD NOT）**。ブロックナンバー、オラクル値、アップグレード設定などに依存してはいけない。 |

Solidityで安全に設計するには、以下のように**`immutable`キーワード**を使うのが良いとされています。

```js
contract ExampleVerifier is IERC7913SignatureVerifier {
    address public immutable trustedKey;
    
    constructor(address _trustedKey) {
        trustedKey = _trustedKey;
    }

    function verify(bytes calldata key, bytes32 hash, bytes calldata signature)
        external
        pure
        returns (bytes4)
    {
        // 状態を読まず、入力パラメータのみに基づいて検証する
        if (_isValidSignature(key, hash, signature)) {
            return 0x024ad318;
        }
        return 0xffffffff;
    }

    function _isValidSignature(bytes calldata key, bytes32 hash, bytes calldata signature)
        private
        pure
        returns (bool)
    {
        // シンプルな検証ロジック
        // 状態や外部要素に依存しない
        return keccak256(key) == hash; // 仮のロジック例
    }
}
```

このように`immutable`や`pure`を適切に活用することで、Verifierは信頼不要で安全な仕組みを維持できます。

#### ERC7562との整合性

Verifierのステートレス（stateless）設計は、**ERC7562**のスコープルール（scope rules）にも適合します。
**ERC7562**では、スマートアカウントの動作範囲や署名検証に関する安全性の要件が定められており、ステートレスなVerifierはその要件を満たすとされています。

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

[\[ERC7562\] Account Abstractionのトランザクション検証ルールについて理解しよう！](/articles/213a17c8dd12a29f8f2c)

つまり、Verifierがステートレスであることは、単にガス効率の面だけでなく、Ethereumエコシステム全体との整合性を保ち、長期的に安全なアカウント抽象化を実現するための鍵でもあります。

## 引用

Hadrien Croubois (@Amxx), Ernesto García (@ernestognw), Francisco Giordano (@frangio), Aryeh Greenberg (@arr00), "ERC-7913: Signature Verifiers [DRAFT]," Ethereum Improvement Proposals, no. 7913, March 2025. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7913.

## 最後に

今回は「Ethereumアドレスを持たない鍵（メール署名、RSA、P-256など）でも、共通のVerifierコントラクトを使って安全に署名を検証できるようにする仕組みを提案している**ERC7913**」についてまとめてきました！
いかがだったでしょうか？

質問などがある方は以下の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
