# [ERC8128] ウォレットの署名だけでAPI認証を完結させる仕組みを理解しよう！

> ERC8128は、Ethereumアカウントを使ってHTTPリクエストに暗号署名を付与し、リクエストの認証と整合性を保証する仕組みです。

- URL: https://cardene.dev/articles/2ea3b9cb21612b1b8b3e
- 公開日: 2026-02-13
- 更新日: 2026-02-13
- トピック: Blockchain
- タグ: ethereum, solidity, smartcontract, ブロックチェーン

## はじめに

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

https://amzn.asia/d/gxvJ0Pw

今回は、Ethereumアカウントの暗号署名をHTTPリクエストに直接付与し、APIキーやセッションを使わずにリクエスト単位で認証を完結させる仕組みを提案している**ERC8128**についてまとめていきます！

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

https://eip.tools/eip/8128

https://ethereum-magicians.org/t/erc-8128-signed-http-requests-with-ethereum/27515

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

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

## 概要

**ERC8128**は、Ethereumアカウントを使ってHTTPリクエストに暗号署名を付与し、リクエストの認証と整合性を保証する仕組みです。
RFC 9421（HTTP Message Signatures）をベースに、Ethereum固有の署名・検証方式を定義しています。

従来のHTTP認証では、Cookie、APIキー、JWTなどの「**bearer credential**」（持っているだけで認証が通る資格情報）を使うのが一般的でした。
しかし、これらは盗まれると第三者がそのまま再利用でき、リクエストの内容自体の整合性は保証されません。

**ERC8128**では、署名がリクエストの内容（メソッド、パス、クエリ、ボディ）に暗号的に紐づくため、改ざんを検知できます。
さらに、サーバーがbearer credentialを発行・管理する必要がなく、Ethereumアカウントのオンチェーンで検証可能なIDをそのまま利用できます。

#### RFC 9421とは

**RFC 9421**（HTTP Message Signatures）は、IETFが策定したHTTPメッセージに暗号署名を付与するための標準規格です。

従来のHTTP認証ではリクエストの送信者を認証するだけでしたが、RFC 9421ではリクエストの内容自体に暗号的な証明を紐づけられます。
具体的には、署名者がHTTPリクエストの構成要素（メソッド、パス、ヘッダーなど）の中から署名対象を選択し、それらを一定の形式（正規化）に変換した上で暗号署名を生成します。
検証者は同じ手順で署名対象を再構築し、署名を検証することで、リクエストが改ざんされていないことを確認できます。

RFC 9421が定義しているのは以下の要素です。

| 要素 | 説明 |
|------|------|
| `Signature-Input`ヘッダー | 署名対象のコンポーネント一覧と署名パラメータを格納する |
| `Signature`ヘッダー | 署名バイト列を格納する |
| Derived Component | `@method`、`@authority`、`@path`、`@query`など、HTTPリクエストから導出される値 |
| 署名ベース（Signature Base） | 署名対象の値を正規化して1つの文字列にまとめたもの。実際に署名する対象データ |
| 署名パラメータ | `created`（作成時刻）、`expires`（有効期限）、`nonce`（リプレイ防止）、`keyid`（鍵識別子）など |

RFC 9421自体は暗号アルゴリズムに依存しない汎用的な枠組みです。
RSA、ECDSA、Ed25519など、どの署名アルゴリズムでも使用できるように設計されています。

**ERC8128**は、このRFC 9421の枠組みに**ERC191**（Ethereumのメッセージ署名標準）と**ERC1271**（スマートコントラクトの署名検証）を組み合わせ、Ethereumアカウントによる署名と検証の仕組みを追加したものです。

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

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

## 動機

この提案が生まれた背景には以下のような課題がありました。

#### 既存のHTTP認証の限界

現在のHTTP APIは、Cookie、APIキー、JWTなどのbearer credentialで認証を行うのが一般的です。
しかし、これらの仕組みはリクエストの送信者を認証するだけで、リクエストの内容自体の整合性は保証しません。
また、サーバー側で秘密情報を発行・保護する必要があり、管理コストが発生します。

HTTPメッセージ署名は、リクエスト自体に暗号的な証明を紐づけることでこれを改善します。
クライアントがリクエストの構成要素を選択して署名し、サーバーがそれを検証することで、bearer credentialの発行や保存なしに認証とリクエスト整合性を実現できます。

#### SIWEとの違い

**EIP4361**（Sign-In with Ethereum / SIWE）は、Webフロントエンドでのインタラクティブなログインに広く使われています。

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

[\[ERC4361\] Ethereumアドレスを使用してオフチェーンサービスにログインする仕組みを理解しよう！](/articles/b4d04cc84b4f1f8a3be1)

しかし、SIWEはユーザーがステートメントに署名してセッションを確立する仕組みであり、個々のHTTPリクエストを認証する仕組みではありません。
以下のようなケースには対応していません。

- APIクライアントやサーバー間のマシン間通信
- AIエージェントによる自動的なAPI呼び出し
- セッションやbearer credentialなしでの個別リクエスト認証

#### Ethereumアカウントの活用

Ethereumアカウントは、状態を持ちグローバルに検証可能なIDを提供します。
このアカウントをHTTPメッセージ署名の署名権限として使用することで、アドレスの制御を証明（EOAの場合）したり、コントラクトで定義されたポリシーを満たす（SCAの場合、**ERC1271**経由）ことでリクエストを認可できます。

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

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

これにより、委任、セッションモジュール、スコープ付きポリシー、オンチェーンのコンポーザビリティなど、より豊かな認可モデルが可能になります。

:::message
この提案では、セキュリティのコントロールをサーバー側からクライアント（署名者）側に移しています。
従来のシステムではサーバーが資格情報を発行し、スコープを決め、リスク管理を行っていました。
**ERC8128**では、署名者がリクエストごとに「**どのコンポーネントを署名に含めるか**」「**リプレイ保護を有効にするか**」を選択し、検証者はその署名が自身の受け入れルールを満たすかどうかを判定します。
:::

## 仕様

### 用語定義

| 用語 | 説明 |
|------|------|
| EOA | 秘密鍵で制御されるEthereumアカウント（Externally Owned Account） |
| SCA | オンチェーンのロジックで署名を検証するスマートコントラクトアカウント（Smart Contract Account） |
| Signer | リクエストに署名を生成するHTTPクライアント |
| Verifier | 受信したリクエストの署名を検証するHTTPサーバーまたはGateway |
| Request-Bound | 特定の1つのHTTPリクエストに署名を紐づけるモデル |
| Class-Bound | リクエストの「種類」に署名を紐づけるモデル |
| Non-Replayable | 署名を再利用できない性質 |
| Replayable | 有効期間内であれば署名を再利用できる性質 |

### ヘッダー

署名はRFC 9421に準拠した以下の2つのHTTPヘッダーに格納されます。

- `Signature-Input`
    - 署名対象のコンポーネント一覧と署名パラメータを格納する。
- `Signature`
    - 対応するラベルの署名バイト列を格納する。

ラベルには `eth` の使用が推奨されます。
ただし、RFC 9421で有効な任意のラベルを使用することもできます。

以下は署名付きHTTPリクエストのヘッダー例です。

```text
Signature-Input: eth=("@method" "@authority" "@path" "@query" "content-digest");created=1736940000;expires=1736940060;nonce="b64url_r4Nd0mN0nCEK0YQY4d4r7A";keyid="eip8128:1:0x2a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d"
Signature: eth=:base64_encoded_signature_bytes:
```

`Signature-Input`にはラベル`eth`に対する署名対象コンポーネント（`@method`、`@authority`など）とパラメータ（`created`、`expires`、`nonce`、`keyid`）が含まれます。
`Signature`には対応する署名バイト列がBase64エンコードされて格納されます。

### セキュリティモデル

**ERC8128**のセキュリティは、「**Request Binding**」（リクエスト紐づけ）と「**Replay Protection**」（リプレイ保護）の2つの独立した軸で表現されます。
事前のネゴシエーションや機能発見は不要で、署名者がコンポーネントとパラメータを選択し、検証者がそれを評価します。

|  | Non-Replayable（再利用不可） | Replayable（再利用可） |
|--|------------------------------|----------------------|
| Request-Bound（特定リクエストに紐づく） | 最も安全 − 準拠の必須要件 | 安全だが有効期間内は再利用可能 |
| Class-Bound（リクエストの種類に紐づく） | リクエストの種類単位で認可 | 最も柔軟だがセキュリティは最も弱い |

#### Request Binding（リクエスト紐づけ）

**Request-Bound**署名は、特定の1つのHTTPリクエストに対する認可を意味します。
以下のコンポーネントを順番に署名に含める必要があります。

| コンポーネント | 内容 | 条件 |
|--------------|------|------|
| `@authority` | ホスト | 常に必須 |
| `@method` | HTTPメソッド | 常に必須 |
| `@path` | パス | 常に必須 |
| `@query` | クエリ文字列 | クエリが存在する場合 |
| `content-digest` | リクエストボディのダイジェスト | ボディが存在する場合 |

メソッド、パス、クエリ、ボディのいずれかが変更されると署名の検証が失敗するため、改ざんを検知できます。

**Class-Bound**署名は、上記のコンポーネントの一部を省略したものです。
元の順序を維持しつつ一部のコンポーネントを除外することで、特定のリクエストではなく、署名に含まれたコンポーネントが一致するリクエストの「**種類**」に対する認可を意味します。

:::message
例えば、`@authority`と`@method`だけを含むClass-Bound署名は、「**api.example.comへのPOSTリクエスト全般**」を認可することになります。
パスやボディの違いに関係なく署名が有効になるため、柔軟ですがセキュリティは弱くなります。
冪等な読み取り操作や、事前に定義されたリクエストクラスなど、低リスクなケースに限定して使用します。
:::

#### Replay Protection（リプレイ保護）

**Non-Replayable**署名は再利用できない署名です。

- `nonce`パラメータを含める必要がある
- 検証者は(`keyid`, `nonce`)の組み合わせの一意性をアプリケーション定義のnonce保持期間内で保証する必要がある
- `expires - created`がnonce保持期間を超えるNon-Replayable署名は拒否する必要がある
- 一度受け入れたnonceは、保持期間内に再度受け入れてはならない

`nonce`を省略した署名は**Replayable**となり、有効期間内であれば検証者のポリシーに従って再利用が可能です。

### 準拠の基準

この提案に準拠するためには、以下の両方の条件を満たす署名を受け入れる必要があります。

- **Request-Bound**であること
- **Non-Replayable**であること

これにより、最も安全な設定（Request-Bound + Non-Replayable）を選択した署名者は、準拠するすべての検証者と相互運用できることが保証されます。
どちらかの条件を満たさない署名しか受け入れない実装は、この提案に準拠していません。

検証者はオプションとして、Replayable署名やClass-Bound署名を受け入れることもできます。
ただし、Replayable署名を受け入れる場合は、署名の早期無効化の仕組み（セクション「署名の無効化」参照）を実装する必要があります。

### 署名パラメータ

#### `keyid`

Ethereumアカウントを明示的なチェーンコンテキスト付きで識別するパラメータです。
以下の形式で指定します。

```text
keyid="eip8128:<chain-id>:<address>"
```

- `<chain-id>` − **EIP155**で定義された10進整数
- `<address>` − `0x` + 40文字の16進数で表現されたEthereumアドレス

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

[\[EIP155\] Ethereumネットワークでリプレイ攻撃を防ぐ仕組みを理解しよう！](/articles/2526432899353e4a0225)

署名者はアドレスを小文字の16進数でシリアライズすることが推奨されます。
検証者はアドレスの大文字・小文字を区別せずに扱う必要があります。

`<chain-id>`は、アドレスがSCAかどうかの判定や**ERC1271**での検証に使用されます。

:::message
`eip8128`という名前空間は、CAIP-10のアカウント識別子構文（`eip155:<chain-id>:<address>`）と同じ形式ですが、署名をこの提案に明示的に紐づけるために異なる名前空間プレフィックスを使用しています。
:::

#### `created`と`expires`

- `created` − 署名が有効になるUnixタイムスタンプ（秒）
- `expires` − 署名が無効になるUnixタイムスタンプ（秒）で、`created`より大きい値である必要がある

検証者は以下の場合に署名を拒否します。

- `created`または`expires`が整数のUnixタイムスタンプでない場合
- 現在時刻が`created`より前、または`expires`より後の場合

また、検証者は`created`に対するクロックスキュー（時計のずれ）の許容範囲と、`expires`に対する最大有効期間を設定することが推奨されます。

#### `nonce`

リプレイ防止に使用されるパラメータです。
Non-Replayable署名では必須で、Replayable署名では省略されます。

### 署名の構築フロー

以下は、署名者（クライアント）がHTTPリクエストに署名を付与する全体の流れです。

```mermaid
sequenceDiagram
    participant Client as クライアント（署名者）
    participant Wallet as ウォレット
    participant Server as サーバー（検証者）

    Note over Client: リクエストを準備
    Client->>Client: ボディがあればcontent-digestを計算
    Client->>Client: 署名対象コンポーネントを選択
    Client->>Client: Signature-Inputを構築
    Client->>Client: RFC 9421に従って署名ベースMを構築
    Client->>Wallet: ERC-191形式でMに署名を要求
    Wallet-->>Client: 署名バイト列を返却
    Client->>Client: Signature-InputとSignatureヘッダーを設定
    Client->>Server: 署名付きHTTPリクエストを送信
```

署名の手順は以下の通りです。

1. リクエストにボディがある場合、`content-digest`を計算してヘッダーに付与する
2. ラベル`eth`で`Signature-Input`を構築する（署名対象コンポーネント + パラメータ `created`、`expires`、`nonce`、`keyid`）
3. RFC 9421に従って、署名ベース`M`を構築する
4. **ERC191**の`personal_sign`エンコーディングを使用してハッシュを計算し、ウォレットで署名する
5. `Signature-Input`と`Signature`ヘッダーをリクエストに設定する

署名ベース`M`からハッシュ`H`を計算する式は以下の通りです。

```text
H = keccak256("\x19Ethereum Signed Message:\n" || decimal(len(M)) || M)
```

このハッシュ`H`に対してECDSA署名を行い、結果を`Signature`ヘッダーにBase64エンコードして格納します。

#### 署名ベースの例

以下は、Request-Bound + Non-Replayableの署名ベースの例です。

```text
"@method": POST
"@authority": api.example.com
"@path": /foo
"@query": ?a=1&b=two
"content-digest": sha-256=:ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad:
"@signature-params": ("@method" "@authority" "@path" "@query" \
  "content-digest");created=1736940000;expires=1736940060;\
  nonce="b64url_r4Nd0mN0nCEK0YQY4d4r7A";keyid="eip8128:1:0x2a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d"
```

この例では、`POST`メソッド、ホスト`api.example.com`、パス`/foo`、クエリ`?a=1&b=two`、ボディのダイジェストがすべて署名に含まれています。
有効期間は60秒（`created=1736940000`から`expires=1736940060`）で、nonceによるリプレイ保護も有効です。

### 検証フロー

以下は、検証者（サーバー）が署名付きリクエストを検証する全体の流れです。

```mermaid
sequenceDiagram
    participant Server as サーバー（検証者）
    participant Chain as ブロックチェーン

    Note over Server: 署名付きリクエストを受信
    Server->>Server: Signature-InputとSignatureを解析
    Server->>Server: created / expiresのタイムスタンプを検証
    Server->>Server: nonceの一意性を確認
    Server->>Server: content-digestの再計算と照合
    Server->>Server: RFC 9421に従って署名ベースMを再構築
    Server->>Server: ERC-191形式でHを計算
    alt EOAの場合
        Server->>Server: ecrecoverでアドレスを復元しkeyidのアドレスと照合
    else SCAの場合
        Server->>Chain: ERC-1271のisValidSignatureを呼び出し
        Chain-->>Server: 検証結果を返却
    end
    Server->>Server: nonceを使用済みとして記録
    Note over Server: 認証成功
```

検証の手順は以下の通りです。

1. `Signature-Input`と`Signature`をラベル`eth`（または対応するラベル）で解析する
2. `created`、`expires`のタイムスタンプを検証し、`nonce`が存在する場合はその一意性を確認する
3. `content-digest`が署名に含まれている場合、リクエストボディから再計算して照合する
4. RFC 9421に従って、受信したリクエストと`Signature-Input`から署名ベース`M`を再構築する
5. `M`から**ERC191**形式でハッシュ`H`を計算する
6. `keyid`からチェーンIDとアドレスを抽出し、署名を検証する
7. `nonce`を使用済みとして記録する（少なくとも`expires`まで保持する）

#### EOAの検証

EOAの場合、**ERC191**で指定された方式（`ecrecover`）を使用して署名からアドレスを復元し、`keyid`に含まれるアドレスと照合します。

#### SCAの検証

SCAの場合、**ERC1271**の`isValidSignature`を使用して検証を行います。
検証に使用するコントラクトは、`keyid`に指定されたチェーン上で解決される必要があります。

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

[\[ERC6492\] デプロイされていないコントラクトアカウントで署名検証する仕組みを理解しよう！](/articles/019072f96b73939a047d)

参考実装では、デプロイ済みのSCA（**ERC1271**）に加えて、まだデプロイされていないSCA（**ERC6492**経由）や事前委任のSCAの検証もサポートしています。

### 署名の無効化

#### Non-Replayable署名の無効化

nonceの消費によって無効化されます。

- nonceは追跡され、初回使用後に消費済みとして扱われる
- 検証者は(`keyid`, `nonce`)の状態を、受け入れたNon-Replayable署名の有効期間（`expires - created`）以上保持する必要がある

#### Replayable署名の無効化

有効期限の到来によって主に無効化されます。
暗号的なリプレイ保護がないため、Replayable署名を受け入れる検証者は、署名者がトリガーできる早期無効化の仕組みを実装する必要があります。

以下のいずれかの仕組みを含める必要があります。

- `keyid`ごとのnot-beforeタイムスタンプ
- 署名ごとの無効化（署名や署名ベースのフィンガープリントなど）

早期無効化をトリガーするリクエストは、Request-Bound署名で認証される必要があります。
Class-Bound署名での無効化は許可されません。

:::message
早期無効化がRequest-Bound署名を要求する理由は、無効化が検証者の認可状態を変更するコントロールプレーン操作であるためです。
Class-Bound署名でこの操作を許可すると、1つの署名で複数の認可を無効化できてしまい、過剰な取り消しや権限の増幅が起こる可能性があります。
:::

#### 鍵の漏洩とローテーション

EOAの秘密鍵が漏洩した場合、検証者はポリシーベースの無効化に依存します。
SCAの場合、コントラクトの状態変更（署名者の変更など）によって、**ERC1271**の実装に応じて過去に発行された署名が無効になる場合があります。

## 処理フローの具体例

ここでは、具体的なサンプルデータを使って署名から検証までの全フローを追跡します。

#### シナリオ設定

以下の状況を考えます。

AliceはEOAを使って、NFTマーケットプレイスのAPIに購入注文のリクエストを送信します。
サーバーはAliceのEthereumアドレスで認証を行い、APIキーやセッションは使用しません。

| 項目 | 値 |
|------|-----|
| 送信者 | Alice（EOA） |
| Ethereumアドレス | `0x70997970c51812dc3a010c7d01b50e0d17dc79c8` |
| チェーンID | 1（Ethereum Mainnet） |
| 送信先API | `api.example.com` |
| 操作 | NFTの購入注文 |

#### 署名前のHTTPリクエスト

Aliceが送信したい元のHTTPリクエストは以下の通りです。

```text
POST /api/v1/orders?currency=eth HTTP/1.1
Host: api.example.com
Content-Type: application/json

{"item":"nft_001","price":"0.5"}
```

このHTTPリクエストの各部分を説明します。

| 部分 | 値 | 意味 |
|------|-----|------|
| メソッド | `POST` | サーバーにデータを送信して新しいリソース（注文）を作成する |
| パス | `/api/v1/orders` | 注文APIのエンドポイント |
| クエリ | `?currency=eth` | 支払い通貨をETHに指定するパラメータ |
| ホスト | `api.example.com` | リクエスト先のサーバー名 |
| ボディ | `{"item":"nft_001","price":"0.5"}` | 購入するNFTのIDと価格（0.5 ETH） |

この時点ではまだ認証情報は含まれていません。
APIサーバーはこのリクエストが本当にAliceから送られたものかどうか判断できません。

#### 署名の全体フロー

以下のシーケンス図は、署名から検証までの全体の流れを実際のデータとともに示しています。

```mermaid
sequenceDiagram
    participant Alice as Alice（EOA）
    participant Server as APIサーバー
    participant Store as Nonceストア

    Note over Alice: POST /api/v1/orders?currency=eth<br>body: {"item":"nft_001","price":"0.5"}

    Alice->>Alice: 1. content-digestを計算
    Alice->>Alice: 2. 署名対象コンポーネント選択<br>[@method, @authority, @path,<br>@query, content-digest]
    Alice->>Alice: 3. パラメータ設定<br>[keyid, created, expires, nonce]
    Alice->>Alice: 4. Signature-Inputを構築
    Alice->>Alice: 5. 署名ベースMを構築（RFC 9421）
    Alice->>Alice: 6. ERC-191ハッシュH = keccak256(...)
    Alice->>Alice: 7. signature = sign(H, 秘密鍵)

    Alice->>Server: 署名付きHTTPリクエスト送信

    Server->>Server: 1. Signature-Input / Signature解析
    Server->>Server: 2. タイムスタンプ検証
    Server->>Store: 3. nonce確認
    Store-->>Server: 未使用
    Server->>Server: 4. content-digest再計算と照合
    Server->>Server: 5. 署名ベースM再構築
    Server->>Server: 6. ecrecover(H, sig) → アドレス復元
    Server->>Server: 7. アドレス照合
    Server->>Store: 8. nonce記録（TTL 60秒）

    Server-->>Alice: 200 OK（注文受付完了）
```

各ステップを順に追っていきます。

#### 署名の各ステップ

##### ステップ1 − `content-digest`の計算

`content-digest`は、HTTPリクエストのボディ（送信するデータ本体）のハッシュ値です。
ハッシュとは、任意の長さのデータを固定長の短い値に変換する一方向関数で、元のデータが1バイトでも変わると出力が大きく変化します。
ここではSHA-256（256ビットの出力を持つ広く使われているハッシュ関数）を使用します。

リクエストにボディが存在するため、まずボディのSHA-256ハッシュを計算して`Content-Digest`ヘッダーに付与します。

```text
入力（ボディ）:
  {"item":"nft_001","price":"0.5"}

        ↓ SHA-256でハッシュ化 → Base64エンコード

出力（Content-Digestヘッダー値）:
  sha-256=:RK/0qy18MlBSVnWgjwz6lZEWjP/lF5HF9bvEF8FabDg=:
```

このダイジェスト値がボディの「**指紋**」となります。
ボディが1バイトでも変更されると異なるハッシュ値になるため、改ざんの検知に使用できます。
サーバー側で同じボディに対してSHA-256を再計算し、この値と照合することで、通信途中でボディが書き換えられていないかを確認できます。

##### ステップ2 − 署名対象コンポーネントの選択

Request-Bound署名を作成するため、リクエストの構成要素から署名対象のコンポーネントを選択します。
「**コンポーネント**」とは、HTTPリクエストを構成する個々の要素のことです。
RFC 9421では、`@`から始まる「**Derived Component**」（HTTPリクエストから導出される値）と、通常のHTTPヘッダーの2種類があります。

以下の表は、各コンポーネントの意味とサンプルリクエストでの実際の値です。

| コンポーネント | 意味 | サンプルでの値 |
|--------------|------|--------------|
| `@method` | HTTPメソッド。リクエストの種類（GET、POST、DELETEなど）を表す | `POST` |
| `@authority` | リクエスト先のホスト名（とポート番号）。どのサーバーに送るかを表す | `api.example.com` |
| `@path` | リクエスト先のパス。サーバー上のどのエンドポイントにアクセスするかを表す | `/api/v1/orders` |
| `@query` | クエリ文字列。URLの`?`以降のパラメータを表す | `?currency=eth` |
| `content-digest` | ボディのハッシュ値。ステップ1で計算した`Content-Digest`ヘッダーの値 | `sha-256=:RK/0qy...=:` |

```text
リクエストの構成要素            署名対象コンポーネント
─────────────────────         ──────────────────────
POST                      →  @method          （常に必須）
api.example.com           →  @authority       （常に必須）
/api/v1/orders            →  @path            （常に必須）
?currency=eth             →  @query           （クエリが存在するため追加）
{"item":"nft_001",...}    →  content-digest   （ボディが存在するため追加）
```

`@method`、`@authority`、`@path`は常に必須で、`@query`と`content-digest`はリクエストの内容に応じて追加されます。
これでリクエストのすべての重要な要素が署名に含まれ、どの部分が変更されても署名検証が失敗するようになります。

##### ステップ3 − 署名パラメータの設定

署名に付与する4つのパラメータを設定します。
これらのパラメータは、署名対象のコンポーネントとは別に、「**誰が**」「**いつ**」「**どの条件で**」署名したかを記述するメタ情報です。

| パラメータ | 役割 |
|-----------|------|
| `keyid` | 署名者のEthereumアカウントを識別する。チェーンIDとアドレスを含む |
| `created` | 署名が有効になる開始時刻。Unixタイムスタンプ（1970年1月1日からの経過秒数）で指定する |
| `expires` | 署名が無効になる時刻。この時刻を過ぎたリクエストはサーバーに拒否される |
| `nonce` | 一度だけ使用される使い捨ての値。リプレイ攻撃（署名済みリクエストの再送信）を防ぐために使用する |

```text
keyid   = "eip8128:1:0x70997970c51812dc3a010c7d01b50e0d17dc79c8"
           ├─ eip8128  → この提案の名前空間
           ├─ 1        → チェーンID（Ethereum Mainnet）
           └─ 0x7099.. → Aliceのアドレス

created = 1736940000    → 署名の有効開始時刻（2025-01-15 12:00:00 UTC）
expires = 1736940060    → 署名の有効期限  （2025-01-15 12:01:00 UTC）
nonce   = "k8Jx9mPq2nR5tV7wY0zA"  → ランダム生成された一意な値
```

有効期間は60秒に設定されています。
`nonce`を含めることでこの署名はNon-Replayable（再利用不可）となり、同じ署名が再度使用されても拒否されます。
`nonce`はクライアント側でランダムに生成する方式が一般的で、暗号的に安全な乱数生成器（`crypto.randomUUID()`など）を使用します。

##### ステップ4 − `Signature-Input`の構築

`Signature-Input`は、「**何に対して署名したか**」を記述するHTTPヘッダーです。
署名対象のコンポーネント一覧とパラメータをまとめた情報で、サーバーはこのヘッダーを見ることで署名の検証方法を判断できます。

ステップ2のコンポーネントとステップ3のパラメータを組み合わせて`Signature-Input`を構築します。

```text
eth=("@method" "@authority" "@path" "@query" "content-digest");created=1736940000;expires=1736940060;nonce="k8Jx9mPq2nR5tV7wY0zA";keyid="eip8128:1:0x70997970c51812dc3a010c7d01b50e0d17dc79c8"
```

この文字列の構造は以下の通りです。

```text
eth=                          → ラベル（この署名の識別子）
("@method" "@authority" ...)  → 署名対象コンポーネントのリスト
;created=1736940000           → パラメータ（セミコロン区切り）
;expires=1736940060
;nonce="k8Jx9mPq2nR5tV7wY0zA"
;keyid="eip8128:1:0x7099..."
```

RFC 9421のStructured Fieldsの形式に従ってシリアライズされています。

##### ステップ5 − 署名ベース`M`の構築

「**署名ベース**」（Signature Base）`M`とは、実際にウォレットで署名する対象となる文字列です。
RFC 9421の規則に従って、ステップ2で選択したコンポーネントの実際の値を正規化（一定の形式に変換）し、1つの文字列にまとめます。

```text
"@method": POST
"@authority": api.example.com
"@path": /api/v1/orders
"@query": ?currency=eth
"content-digest": sha-256=:RK/0qy18MlBSVnWgjwz6lZEWjP/lF5HF9bvEF8FabDg=:
"@signature-params": ("@method" "@authority" "@path" "@query" "content-digest");created=1736940000;expires=1736940060;nonce="k8Jx9mPq2nR5tV7wY0zA";keyid="eip8128:1:0x70997970c51812dc3a010c7d01b50e0d17dc79c8"
```

各行は `"コンポーネント名": 値` の形式で、リクエストから取得した実際の値が入ります。
最終行の`@signature-params`にはステップ4で構築した`Signature-Input`の内容が入ります。
この文字列全体が署名対象のメッセージ`M`です。

:::message
署名ベース`M`はリクエストの内容そのものから決定的に構築されます。
つまり、署名者とサーバーが同じリクエストに対してこの手順を実行すれば、まったく同じ`M`が得られます。
この性質が検証を可能にしています。
:::

##### ステップ6 − ERC-191ハッシュの計算と署名

このステップでは、署名ベース`M`に対して暗号署名を行います。
ここで登場する主要な用語を説明します。

| 用語 | 説明 |
|------|------|
| **ERC191** | Ethereumにおけるメッセージ署名の標準フォーマット。署名対象データの先頭に`\x19Ethereum Signed Message:\n`というプレフィックスを付けることで、通常のトランザクション署名と区別する |
| `keccak256` | Ethereumで標準的に使われるハッシュ関数。任意の長さの入力から256ビット（32バイト）の固定長ハッシュ値を生成する |
| ECDSA | 楕円曲線デジタル署名アルゴリズム。秘密鍵で署名を生成し、対応する公開鍵（アドレス）で検証できる |
| `ecrecover` | ECDSA署名とハッシュ値から、署名に使用された秘密鍵に対応するアドレスを数学的に復元する関数 |

署名ベース`M`に対して**ERC191**形式でプレフィックスを付与し、`keccak256`でハッシュを計算します。
その後、Aliceの秘密鍵を使ってECDSA署名を行います。

```text
M（署名ベース文字列）
  │
  ↓ ERC-191プレフィックス付与
"\x19Ethereum Signed Message:\n" + len(M) + M
  │
  ↓ keccak256ハッシュ
H = 0x7a8b9c...（32バイトのハッシュ値）
  │
  ↓ ECDSA署名（Aliceの秘密鍵を使用）
signature = 0x3f4a5b...（65バイト = r[32] + s[32] + v[1]）
  │
  ↓ Base64エンコード
"P0pbNmQ4eXFyM3Q1djd3OXpBYkNkRWZHaElqS2xNbk9w..."
```

**ERC191**のプレフィックス（`\x19Ethereum Signed Message:\n`）は、署名対象データがメッセージであることをマークする固定文字列です。
このプレフィックスがあることで、署名したメッセージが通常のEthereumトランザクションとして解釈されることを防ぎます。

ウォレットの`signMessage`メソッド（`personal_sign`）は内部的にこのプレフィックス付与とハッシュ計算を行うため、署名者はウォレットに`M`を渡すだけで署名が完了します。
最終的に得られる署名は65バイト（`r`値32バイト + `s`値32バイト + リカバリ用の`v`値1バイト）のバイナリデータで、これをBase64エンコードして`Signature`ヘッダーに格納します。

#### 署名後のHTTPリクエスト

すべてのステップを経て、最終的に以下のHTTPリクエストがAPIサーバーに送信されます。
元のリクエストのメソッド、パス、ボディはそのままで、認証に必要な3つのヘッダーが追加されています。

```text
POST /api/v1/orders?currency=eth HTTP/1.1
Host: api.example.com
Content-Type: application/json
Content-Digest: sha-256=:RK/0qy18MlBSVnWgjwz6lZEWjP/lF5HF9bvEF8FabDg=:
Signature-Input: eth=("@method" "@authority" "@path" "@query" "content-digest");created=1736940000;expires=1736940060;nonce="k8Jx9mPq2nR5tV7wY0zA";keyid="eip8128:1:0x70997970c51812dc3a010c7d01b50e0d17dc79c8"
Signature: eth=:P0pbNmQ4eXFyM3Q1djd3OXpBYkNkRWZHaElqS2xNbk9w...:

{"item":"nft_001","price":"0.5"}
```

署名前と比較すると、3つのヘッダーが追加されています。

| 追加ヘッダー | 役割 | 内容 |
|-------------|------|------|
| `Content-Digest` | ボディの「指紋」 | ボディのSHA-256ハッシュ値。サーバーが再計算して照合することでボディの改ざんを検知する |
| `Signature-Input` | 署名の設計図 | 何に署名したか（対象コンポーネント一覧）と署名条件（誰が、いつ、nonceは何か）を記述する |
| `Signature` | 署名本体 | ウォレットが生成したECDSA署名のBase64エンコード値。この値からサーバーが署名者のアドレスを復元する |

ボディやその他の元のリクエスト内容はそのままで、ヘッダーが追加されるだけです。
サーバーはこの3つのヘッダーを読み取り、リクエストの真正性を検証します。

#### 検証の各ステップ

APIサーバー（検証者）がこの署名付きリクエストを受信した後の検証手順を、実際のデータとともに追跡します。

##### ステップ1 − ヘッダーの解析

サーバーは`Signature-Input`と`Signature`ヘッダーからラベル`eth`の署名情報を抽出します。

```text
Signature-Input から抽出した情報:
  ├─ 署名対象コンポーネント
  │    @method, @authority, @path, @query, content-digest
  │
  ├─ パラメータ
  │    created = 1736940000
  │    expires = 1736940060
  │    nonce   = "k8Jx9mPq2nR5tV7wY0zA"
  │    keyid   = "eip8128:1:0x70997970c51812dc3a010c7d01b50e0d17dc79c8"
  │
  └─ keyid の分解
       名前空間  = eip8128
       チェーンID = 1
       アドレス   = 0x70997970c51812dc3a010c7d01b50e0d17dc79c8

Signature から抽出:
  └─ 署名バイト列 = P0pbNmQ4eXFyM3Q1djd3OXpBYkNkRWZHaElqS2xNbk9w...
```

ラベル`eth`を優先的に探しますが、他のラベルでもERC8128形式の`keyid`（`eip8128:`から始まる）を持つ署名であれば処理対象とします。

##### ステップ2 − タイムスタンプの検証

`created`と`expires`が有効な時間範囲内であるかを確認します。

```text
created  = 1736940000  （2025-01-15 12:00:00 UTC）
expires  = 1736940060  （2025-01-15 12:01:00 UTC）
現在時刻 = 1736940030  （2025-01-15 12:00:30 UTC）

検証:
  expires > created           → 1736940060 > 1736940000  ✅
  現在時刻 ≥ created          → 1736940030 ≥ 1736940000  ✅  署名は開始済み
  現在時刻 ≤ expires          → 1736940030 ≤ 1736940060  ✅  まだ期限内
  expires - created ≤ 最大有効期間 → 60秒 ≤ 300秒       ✅  有効期間が適切

結果: ✅ タイムスタンプ検証OK
```

もし現在時刻が`expires`を過ぎていた場合（`1736940120` = 12:02:00など）、この時点でリクエストは拒否されます。

##### ステップ3 − nonceの一意性確認

`nonce`（Number used ONCE）は、文字通り「**一度だけ使われる数**」です。
nonceストアとは、過去に受け入れたnonceを保存しておくデータベースやキャッシュのことで、同じnonceが再び使用されていないかを確認するために使います。

同じ(`keyid`, `nonce`)の組み合わせが過去に使用されていないかをnonceストアで確認します。

```text
検索キー:
  keyid = "eip8128:1:0x70997970c51812dc3a010c7d01b50e0d17dc79c8"
  nonce = "k8Jx9mPq2nR5tV7wY0zA"

nonceストアを検索:
  → このキーは存在しない（初回使用）

結果: ✅ nonce一意性OK
```

もし攻撃者が傍受した同じリクエストを再送信した場合、nonceストアに既に記録されているため、この時点で拒否されます。

##### ステップ4 − `content-digest`の検証

受信したリクエストのボディからダイジェストを再計算し、ヘッダーの値と照合します。

```text
受信したボディ:
  {"item":"nft_001","price":"0.5"}

        ↓ SHA-256で再計算

再計算したダイジェスト:
  sha-256=:RK/0qy18MlBSVnWgjwz6lZEWjP/lF5HF9bvEF8FabDg=:

Content-Digestヘッダーの値:
  sha-256=:RK/0qy18MlBSVnWgjwz6lZEWjP/lF5HF9bvEF8FabDg=:

比較: 一致 → ✅ ボディは改ざんされていない
```

もし途中でボディが改ざんされていた場合（`"price":"0.001"`への変更など）、再計算したダイジェスト値がヘッダーの値と一致しないため、この時点で拒否されます。

##### ステップ5 − 署名ベース`M`の再構築

サーバーは受信したリクエストの実際の値と`Signature-Input`を使って、署名者と同じ手順で署名ベース`M`を再構築します。

```text
"@method": POST                              ← 受信したリクエストのメソッド
"@authority": api.example.com                ← 受信したリクエストのホスト
"@path": /api/v1/orders                      ← 受信したリクエストのパス
"@query": ?currency=eth                      ← 受信したリクエストのクエリ
"content-digest": sha-256=:RK/0qy18MlB...=: ← 受信したリクエストのヘッダー値
"@signature-params": ("@method" ...)         ← 受信したSignature-Inputの値
```

リクエストの内容が改ざんされていなければ、この`M`はAliceが署名した時の`M`と完全に一致します。

##### ステップ6 − 署名の検証（EOA）

再構築した`M`からERC-191形式でハッシュ`H`を計算し、`ecrecover`で署名者のアドレスを復元します。
`ecrecover`はEthereumの基本的な暗号操作の1つで、「**このハッシュとこの署名を生成した秘密鍵のアドレスは何か？**」という問いに答える関数です。

```text
M（再構築した署名ベース）
  │
  ↓ ERC-191プレフィックス付与 + keccak256ハッシュ
H = 0x7a8b9c...（32バイト）
  │
  ↓ ecrecover(H, signature)
復元されたアドレス: 0x70997970c51812dc3a010c7d01b50e0d17dc79c8
  │
  ↓ keyidのアドレスと比較
keyidのアドレス:    0x70997970c51812dc3a010c7d01b50e0d17dc79c8

一致 → ✅ Aliceがこのリクエストに署名したことが証明された
```

ここが**ERC8128**の認証の核心部分です。
サーバーはAliceの秘密鍵を知りません。しかし、`ecrecover`で復元したアドレスが`keyid`のアドレスと一致するなら、このリクエストはそのアドレスの秘密鍵の所有者（Alice）が署名したことが数学的に証明されます。
さらに、`M`はリクエストの内容から構築されているため、リクエストが改ざんされていれば`M`が変わり、`H`も変わり、`ecrecover`で復元されるアドレスも変わるため一致しません。
つまり、「**Aliceが署名したこと**」と「**リクエストが改ざんされていないこと**」の両方が同時に検証されます。

:::message
リクエストの内容（メソッド、パス、クエリ、ボディ）が1箇所でも改ざんされていると、サーバーが再構築する`M`はAliceが署名した`M`と異なります。
その結果、ハッシュ`H`も異なり、`ecrecover`で復元されるアドレスはAliceのアドレスと一致しません。
これが**ERC8128**のリクエスト整合性保証の仕組みです。
:::

##### ステップ7 − nonceの記録

検証に成功したら、nonceを使用済みとしてnonceストアに記録します。
この記録により、同じ署名付きリクエストが再送信された場合にステップ3で拒否できるようになります。
TTL（Time To Live）は、nonceストアがこのエントリを保持する期間です。

```text
nonceストアに記録:
  キー: ("eip8128:1:0x70997970...c8", "k8Jx9mPq2nR5tV7wY0zA")
  TTL:  30秒（expires - 現在時刻 = 1736940060 - 1736940030）

→ この nonce は少なくとも30秒間（expiresまで）保持される
→ 同じnonce を含むリクエストが再送信された場合、ステップ3で拒否される
```

以上の全ステップを通過した場合のみ、サーバーはリクエストを認証済みとして処理し、Aliceの注文を受け付けます。

#### 改ざんが検知される例

攻撃者がリクエストを途中で改ざんした場合、どのステップで検知されるかを示します。

##### ボディの改ざん − 価格を変更

攻撃者がネットワーク上でリクエストを傍受し、価格を`"0.5"`から`"0.001"`に改ざんした場合を考えます。

```text
┌─────────────────────────────────────────────────────┐
│ 攻撃者の操作                                          │
│                                                       │
│ 元のボディ:     {"item":"nft_001","price":"0.5"}      │
│ 改ざん後:       {"item":"nft_001","price":"0.001"}    │
└─────────────────────────────────────────────────────┘

サーバーでの検証（ステップ4）:
  改ざんされたボディのダイジェスト:
    sha-256=:x9Yp3kM7nQ2wF4vR...（元とは異なる値）

  Content-Digestヘッダーの値（署名時に計算された元の値）:
    sha-256=:RK/0qy18MlBSVnWgjwz...

  → 不一致 ❌  ステップ4で検知、リクエスト拒否
```

仮に攻撃者が`Content-Digest`ヘッダーの値も書き換えた場合でも、署名ベース`M`に`content-digest`の値が含まれているため、ステップ6の署名検証で復元されるアドレスがAliceと一致せず、やはり拒否されます。

##### パスの改ざん − 別のエンドポイントに転送

攻撃者がパスを注文APIから管理者APIに変更した場合を考えます。

```text
┌─────────────────────────────────────────────────────┐
│ 攻撃者の操作                                          │
│                                                       │
│ 元のパス:      /api/v1/orders                         │
│ 改ざん後:      /api/v1/admin/delete-all               │
└─────────────────────────────────────────────────────┘

サーバーでの検証（ステップ5 → 6）:
  サーバーが再構築するM:
    "@path": /api/v1/admin/delete-all  ← 改ざん後のパスが使われる
    （他のコンポーネントは同じ）

  → Aliceが署名した M とは異なる文字列になる
  → ハッシュ H が異なる
  → ecrecoverで復元されるアドレス ≠ Aliceのアドレス
  → ❌ ステップ6で検知、リクエスト拒否
```

##### リプレイ攻撃 − 同じリクエストの再送信

攻撃者が正当な署名付きリクエストを傍受し、そのまま再送信した場合を考えます。

```text
┌─────────────────────────────────────────────────────┐
│ 攻撃の流れ                                            │
│                                                       │
│ 1. AliceがAPIサーバーに署名付きリクエストを送信         │
│ 2. 攻撃者がリクエストを傍受してコピー                   │
│ 3. APIサーバーがAliceのリクエストを正常に処理            │
│ 4. 攻撃者がコピーしたリクエストを再送信                 │
└─────────────────────────────────────────────────────┘

攻撃者のリクエスト（元と完全に同一）:
  nonce = "k8Jx9mPq2nR5tV7wY0zA"  ← Aliceのリクエストと同じnonce

サーバーでの検証（ステップ3）:
  nonceストアを確認:
    → "k8Jx9mPq2nR5tV7wY0zA" は既に使用済み

  → ❌ ステップ3で検知、リクエスト拒否
```

さらに、もし攻撃者が時間をおいてnonceの保持期間後に再送信した場合でも、`expires`の有効期限が切れているため、ステップ2のタイムスタンプ検証で拒否されます。

##### ホストの変更 − 別サーバーでの再利用

攻撃者が署名付きリクエストを傍受し、別の悪意あるサーバーに転送した場合を考えます。

```text
┌─────────────────────────────────────────────────────┐
│ 攻撃者の操作                                          │
│                                                       │
│ 元のホスト:    api.example.com                         │
│ 転送先:        evil.example.com                        │
└─────────────────────────────────────────────────────┘

evil.example.com での検証（ステップ5 → 6）:
  サーバーが再構築するM:
    "@authority": evil.example.com  ← 転送先のホスト名が使われる
    （他のコンポーネントは同じ）

  → Aliceが署名した M には "@authority": api.example.com が含まれる
  → M が異なるため署名検証に失敗
  → ❌ ステップ6で検知、リクエスト拒否
```

:::message alert
上記の改ざん検知はすべてRequest-Bound署名の場合です。
Class-Bound署名では一部のコンポーネントが署名に含まれないため、含まれていないコンポーネントの改ざんは検知できません。
セキュリティが重要な操作では必ずRequest-Bound署名を使用してください。
:::

## 補足

#### 設計目標

この提案は2つの主要な目標を持って設計されています。

**「Zero-Handshake Interoperability（事前ハンドシェイク不要の相互運用性）」** は、署名者が事前のハンドシェイクや機能発見なしに、準拠するすべての検証者と安全にやり取りできることを意味します。
リクエストが受け入れられるかどうかは、署名の検証とこの提案で定義された受け入れルールのみによって決定されます。

**「Security-First by Default（デフォルトでセキュリティ最優先）」** は、最も安全な設定（Request-Bound + Non-Replayable）が常にすべての準拠検証者でサポートされることを意味します。
同時に、明示的なセキュリティ/パフォーマンスのトレードオフを持つ仕組み（Replayable、Class-Boundなど）にオプトインできるようにしています。

#### なぜERC-191を採用したのか

この提案では署名方式として**ERC191**（`personal_sign`）を採用し、EIP-712は採用していません。

**ERC191**はEthereumの署名スタック全般で幅広くサポートされています（非インタラクティブな署名やマシン間通信を含む）。
型スキーマやドメインセパレータ、UI向けメタデータなしで任意のバイト列に署名できるため、RFC 9421の「**署名ベースMにそのまま署名する**」というモデルに自然に適合します。

EIP-712は構造化データ向けに設計されており、ウォレットUIでの人間による確認やフィッシング防止に有用です。
しかし、HTTPメッセージ署名のようにプログラム的に処理される場面では、その利点は限定的です。
EIP-712を使用する場合、RFC 9421の署名ベース`M`を表現するための追加のシリアライゼーション層（型データエンコーディング + ドメインセパレーション）が必要になり、汎用的なRFC 9421ツールとの相互運用性が低下します。

#### SIWEとの使い分け

**EIP4361**（SIWE）と**ERC8128**は異なるユースケースを対象としています。

| 項目 | SIWE（**EIP4361**） | **ERC8128** |
|------|---------------------|-------------|
| 対象 | フロントエンドの対話的ログイン | 個別HTTPリクエストの認証 |
| セッション | 必要（Cookie/JWT発行） | 不要 |
| リクエスト整合性 | なし | あり（署名で保証） |
| リプレイ保護 | セッション管理に依存 | nonce + 有効期限 |
| 適用場面 | Webアプリのログイン | API、エージェント、サーバー間通信 |

SIWEはユーザーがステートメントに署名してIDを確立し、セッションを発行する仕組みです。
**ERC8128**は個々のHTTPリクエスト自体をbearer credentialなしで認証する仕組みであり、マシン間通信、Gateway、AIエージェントなどに適しています。

#### nonceの運用

この提案ではnonceの取得方法を標準化していません。
デプロイ環境によって最適な方法が異なるためです。

- **クライアント生成nonce**（ランダムな128ビット値など）
    - 追加のラウンドトリップが不要で、エージェントやオフライン署名に適している。ただし、検証者側でnonceの保存が必要。
- **サーバー発行nonce**
    - サーバーが順序制御や厳密な管理を行えるが、ハンドシェイクのような追加のレイテンシが発生する。

Zero-Handshake Interoperabilityを維持するため、この提案はセマンティクスのみを標準化しています。
`nonce`が存在すればNon-Replayable、検証者は有効期間内の一意性を保証するという規則です。
ストレージバックエンド（インメモリ、共有キャッシュ、データベース）や並行処理の戦略は実装に委ねられています。

#### nonceからのリプレイ可能性の推定

初期の設計では、署名者の意図を明確にするための追加パラメータ（`request-type="non-replayable"`など）が検討されていました。
しかし、`nonce`の存在がNon-Replayableであることを直接かつ一意に示すため、追加パラメータは冗長であるとして最終的に削除されました。

#### アルゴリズムシグナリング

RFC 9421はIANA「HTTP Signature Algorithms」レジストリの値を使用する`alg`署名パラメータを定義しています。
しかし、この提案で使用するEthereum署名方式（ERC-191 over secp256k1 + EOAのアドレス復元 or SCAのERC-1271検証）は、現在そのレジストリに登録されていません。

未登録の`alg`値を定義すると、相互運用性が低下し実装間で曖昧な動作が生じるため、現在の設計では`alg`を省略し、アルゴリズムの選択を`keyid`フォーマットに暗黙的に含めています。
将来的にIANAの`alg`識別子が登録されれば、この提案を更新して`alg`の使用を許可または要求できます。

#### Replayable / Class-Bound署名が存在する理由

セキュリティ最優先のベースライン（Request-Bound + Non-Replayable）をすべての準拠検証者で必須としつつも、現実のシステムではレイテンシやコストのためのトレードオフが必要になることがあります。

Replayable署名は、短い有効期間内に同じ認可証明を複数回使用する時に、繰り返しの暗号検証オーバーヘッドを削減できます。
Class-Bound署名は、1つの認可証明で冪等な読み取りなどのリクエストの「種類」全体をカバーでき、署名者と検証者のオーバーヘッドをさらに削減します。

これらのモードは明示的にオプションであり、低リスクな操作と短い有効期間に限定して使用する必要があります。
リプレイ可能性とバインディング強度を分離することで、4つの組み合わせすべてが可能になり、少数の固定モードに実装を強制することを避けています。

## セキュリティ

#### nonce管理の並行処理

検証者は(`keyid`, `nonce`)のチェックと無効化を原子的操作として実装する必要があります。
並行処理や分散環境で非原子的な「チェックしてから書き込む」処理を行うと、同じnonceの2つの並行検証が両方成功するレースコンディションが発生する可能性があります。

実装では、原子的書き込みをサポートする共有ストア（トランザクショナルなデータベース書き込みや共有キャッシュでの原子的操作など）を使用し、受け入れた有効期間の分だけnonce状態を保持する必要があります。

#### リプレイ攻撃

署名付きリクエストを傍受し、再送信することで認可を不正に得る攻撃です。

Non-Replayable署名の使用とnonceの一意性保証が防御手段となります。
有効期限（`expires - created`）を短く設定することで、攻撃のウィンドウをさらに狭められます。
検証者はnonce保持期間を超える有効期間を持つNon-Replayable署名を拒否する必要があります。

#### パラメータ改ざん

署名済みリクエストのクエリパラメータやボディを改ざんする攻撃です。

Request-Bound署名を使用してメソッド、パス、クエリ、ボディをすべて署名に含めることが防御手段となります。
`content-digest`によりボディの整合性を検証できます。

#### ドメインとパスの混同

ある宛先向けの署名を別の宛先やエンドポイントで再利用する攻撃です。

`@authority`コンポーネントにより署名は特定のホストにのみ有効であり、`@path`コンポーネントにより特定のエンドポイントにのみ有効となります。

#### 鍵の漏洩

秘密鍵が漏洩した場合、攻撃者が正当な署名を生成できてしまいます。
これは署名方式の範囲外であり、安全な鍵管理が前提となります。

EOAの場合はポリシーベースの無効化（アドレスのブロックリストなど）で対応します。
SCAの場合はコントラクトの状態変更（署名者の変更など）によって、**ERC1271**の実装に応じた対応が可能です。

## 参考実装

#### 署名者（クライアント）の実装

以下は署名者の参考実装です。

```typescript
type Req = {
  method: string;
  authority: string; // host[:port]
  path: string;
  query?: string;    // includes leading "?"
  body?: Uint8Array;
  headers: Record<string, string>;
};

type SignParams = {
  keyid: `eip8128:${number}:0x${string}`;
  created: number;  // unix seconds
  expires: number;  // unix seconds
  nonce: string;    // sf-string (recommend base64url)
};

function covered(req: Req): string[] {
  const c = ["@method", "@authority", "@path"];
  if (req.query) c.push("@query");
  if (req.body && req.body.length > 0) c.push("content-digest");
  return c;
}

export async function signRequest(
  req: Req,
  wallet: { signMessage: (m: string) => Promise<Uint8Array> },
  p: SignParams
): Promise<Req> {
  if (req.body && req.body.length > 0 && !req.headers["content-digest"]) {
    req.headers["content-digest"] = computeContentDigest(req.body);
  }

  const components = covered(req);

  const sigInput = sfSerializeSignatureInput("eth", components, {
    created: p.created,
    expires: p.expires,
    nonce: p.nonce,
    keyid: p.keyid,
  });

  const M = createSignatureBase(req, "eth", sigInput);
  const sigBytes = await wallet.signMessage(M);

  req.headers["signature-input"] = sigInput;
  req.headers["signature"] = `eth=:${base64(sigBytes)}:`;
  return req;
}
```

`covered`関数がリクエストの内容に応じて署名対象コンポーネントを決定します。
ボディがある場合は`content-digest`を計算し、`sfSerializeSignatureInput`でRFC 9421形式の`Signature-Input`を構築します。
`createSignatureBase`でRFC 9421の署名ベース`M`を生成し、ウォレットの`signMessage`で**ERC191**形式の署名を取得します。

#### 検証者（サーバー）の実装

以下は検証者の参考実装です。

```typescript
export async function verifyRequest(
  req: Req,
  policy: VerifyPolicy
): Promise<boolean> {
  const headers = parseSignatureHeaders(req);

  const { sigInput, sig } = findEIP8128MessageSignature(headers);
  const { created, expires, nonce, keyid } = parseSignatureInput(sigInput);

  if (!validateTimestamps(created, expires, policy)) return false;

  if (!nonce) return false;

  const replayKey = `${keyid}:${nonce}`;

  if (await policy.nonceSeen(replayKey)) return false;

  if (!checkRequestIntegrity(req, sigInput)) return false;

  const M = rfc9421CreateSignatureBase(req, "eth", sigInput);
  const H = eth191Hash(M);
  const { chainId, address } = parseKeyid(keyid);

  const valid = await verifyMessage(chainId, address, H, sig);
  if (!valid) return false;

  await policy.invalidateNonce(replayKey, Math.max(0, expires - policy.now));

  return true;
}
```

`findEIP8128MessageSignature`はヘッダーからERC8128準拠の署名を探し、`eth`ラベルの署名を優先します。
タイムスタンプの検証、nonceの一意性確認、リクエスト整合性チェック（`content-digest`の再計算と照合）を行った後、署名ベース`M`を再構築して**ERC191**形式でハッシュ`H`を計算します。
`verifyMessage`はEOAの場合は`ecrecover`でアドレスを復元し、SCAの場合は**ERC1271**（および**ERC6492**やERC-8010）で検証を行います。

## 互換性

この提案はRFC 9421のアプリケーション層プロファイルとして`Signature-Input`と`Signature`のHTTPフィールドを使用します。
対応していないクライアントやサーバー、中間機器はこれらを通常のヘッダーとして無視するため、段階的な導入が可能です。

既存の認証方式（Cookie、APIキー、OAuth、SIWE）と共存できます。
複数の認証方式が受け入れられる場合、アプリケーション側で優先順位を定義する必要があります。

新しいRFC 9421アルゴリズムの登録は行わず、実装は`keyid`に基づいてこのプロファイルを認識し、Ethereum署名（EOAは**ERC191**、SCAは**ERC1271**）で検証を行います。

## 引用

https://eip.tools/eip/8128

https://ethereum-magicians.org/t/erc-8128-signed-http-requests-with-ethereum/27515

https://www.rfc-editor.org/rfc/rfc9421

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

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

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

## 最後に

今回は「ERC8128によるEthereumウォレットの署名を使ったHTTPリクエスト認証の仕組み」についてまとめてきました！
いかがだったでしょうか？

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

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

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

https://zenn.dev/heku

https://cardene.substack.com/

https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58

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