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

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

EIP-7702の署名対象とリプレイ保護

本記事ではEIP-7702について学習しているときに浮かんだ以下の2点の疑問について、go-ethereumの実装とEIP-7702の仕様を詳細に分析して解説する。

1. auth listに含まれる署名は何に対して署名をしてるのか?
2. auth listの署名を使ってリプレイ攻撃されないのか?

EIP-7702については過去記事を参考:
y-nakajo.hatenablog.com

1. Authorization Listの署名対象とメカニズム

何に対して署名するのか?

EIP-7702ではauth listとしてEOAの署名を渡すことで、そのEOAにcodeを割り当てることが可能となる。

EIP-7702の仕様書では、この署名対象を以下のように定義している:

"The authorization signature is created over `msg = keccak(MAGIC || rlp([chain_id, address, nonce]))`"
EIP-7702 Authorization

つまり、「chainID」と「address」とcodeを割り当てる「EOAのnonce」に対して署名が行われる。意外なのは委譲対象のcodeではなくそのcodeを持つContract Addressが署名対象であった点だ。ただ、これはContractのCodeは一度デプロイしたら変更できないという点を考えるとaddressへの署名だけで確かに十分である。

go-ethereumの該当箇所は以下:
core/types/tx_setcode.go:108-115

// SigHash returns the hash of SetCodeAuthorization for signing.
func (a *SetCodeAuthorization) SigHash() common.Hash {
    return prefixedRlpHash(0x05, []any{
        a.ChainID,    // チェーン識別子
        a.Address,    // 委譲先アドレス(コードではない)
        a.Nonce,      // 委譲元の現在nonce
    })
}

署名に含まれる3つの要素とその目的

1. ChainID - チェーン固有の識別子
特定のブロックチェーンでのみ有効な署名を保証し、クロスチェーンリプレイ攻撃を防止。

2. Address - 委譲先のアドレス
署名は委譲先の20バイトのアドレスに対して行われ、コードの内容は一切含まれない。前述した通り、Contractのcodeは変更不可なのでaddressへの署名で十分である。

3. nonce - リプレイ防止カウンター
委譲元アカウントの現在のトランザクションカウンターで、署名の一回性を保証

2. リプレイ攻撃防止とセキュリティメカニズム

上述したとおり、auth listに含める署名には署名を行うEOAのnonceが含まれる。また、setCodeTxが実行されauth listをもとに委譲コードが設定されると、そのEOAのnonceはインクリメントされる。
上記フローによって、auth listの署名は1回限りしか利用できないようになっている。
以下、上記説明に関するgo-ethereumの該当コードを示す。

auth list適用前(=setCode実行前)のnonceチェック:
core/state_transition.go:566-595

func (st *stateTransition) validateAuthorization(auth *types.SetCodeAuthorization) (authority common.Address, err error) {
    // 1. オーバーフロー防止
    if auth.Nonce+1 < auth.Nonce {
        return authority, ErrAuthorizationNonceOverflow
    }
    
    // 2. 署名検証とアドレス復元
    authority, err = auth.Authority()
    if err != nil {
        return authority, fmt.Errorf("%w: %v", ErrInvalidAuthorization, err)
    }
    
    // 3. 厳密なNonce照合
    if have := st.state.GetNonce(authority); have != auth.Nonce {
        return authority, ErrAuthorizationNonceMismatch
    }
    
    return authority, nil
}

setCode実行時のnonce更新処理:
core/state_transition.go:604-628

func (st *stateTransition) applyAuthorization(auth *types.SetCodeAuthorization) error {
    // 認可を検証
    authority, err := st.validateAuthorization(auth)
    if err != nil {
        return err
    }
    
    // Nonceを即座に増加(リプレイ防止)
    st.state.SetNonce(authority, auth.Nonce+1, tracing.NonceChangeAuthorization)
    
    // 委譲コードを設定
    if auth.Address == (common.Address{}) {
        st.state.SetCode(authority, nil, tracing.CodeChangeAuthorizationClear)
    } else {
        st.state.SetCode(authority, types.AddressToDelegation(auth.Address), 
                         tracing.CodeChangeAuthorization)
    }
    
    return nil
}

EIP-7702で保護可能な攻撃

  • 同一署名の再利用(リプレイ攻撃
    • 同じSetCodeAuthorizationを複数回使用しようとしても、nonce照合により即座に検出・拒否
  • 順序操作攻撃
    • setCodeを意図されていない順序で実行しようとしても、厳密なnonce順序要求により防止可能
  • キャンセル
    • 攻撃ではないが、auth listを代理人に渡した後にcode委譲を拒否することが可能。任意のtxを発行してnonceを更新することで無効化


以上、EIP-7702を調査するときに思いついた疑問について調査した結果をまとめた。このように複数の攻撃に対しても安全に利用可能なように設計されていた。
特にnonceを用いたリプレイ保護機能などはTxの保護機能を流用されており、無駄に複雑な防御機構を持たせずに保護を達成している点で非常に洗練されていると感じた。