はじめに

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

以下でも情報発信しているので、興味ある記事があればぜひ読んでみてください!

今回は、メール認証の仕組みであるDKIMの公開鍵ハッシュをブロックチェーン上に登録・検証できるレジストリの標準インターフェースを提案しているERC7969についてまとめていきます!

以下にまとめられているものを解説しながらまとめていきます。

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

概要

ERC7969は、DomainKeys Identified Mail(DKIM)の公開鍵ハッシュをEthereumブロックチェーン上に登録・検証するための標準インターフェースです。

DKIMとは、メールの送信元が偽装されていないことを証明するための仕組みです。 メールを送信する時、送信サーバーは自分のドメインの秘密鍵でメールに署名を付けます。 受信側はDNS(ドメイン名システム)に公開されている公開鍵を使って署名を検証し、「このメールは確かにexample.comから送られたものだ」と確認できます。

ERC7969はこのDKIMの公開鍵のハッシュ値をスマートコントラクト上のレジストリに保存します。 ドメインの所有者がDNSのTXTレコードから公開鍵を取得し、そのハッシュ値を登録することで、オンチェーンでメールの送信元を検証できるようになります。 zkEmail(ゼロ知識証明を使ったメール検証技術)やアカウント抽象化と組み合わせることで、メールアドレスだけでウォレットを操作したり、アカウントを復旧したりするユースケースが実現できます。

以下の図は、メール送信からオンチェーン検証までの全体フローを示しています。

DKIM検証の全体フロー

送信サーバーが秘密鍵でメールに署名し、公開鍵はDNSで公開されます。 ERC7969のレジストリはこの公開鍵のハッシュをオンチェーンに保存し、zkEmailやスマートコントラクトウォレットがメールの送信元を検証できるようにします。

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

動機

メールベースの本人確認をオンチェーンで実現する必要性

アカウント抽象化(ERC4337)の普及とzkEmail技術の登場により、メールアドレスの所有権をオンチェーンで証明する仕組みの標準化が求められるようになりました。

従来、ブロックチェーン上で本人確認を行うには秘密鍵の管理が前提でした。 しかし、一般的なユーザーにとって秘密鍵やシードフレーズの管理は難しく、紛失すると資産にアクセスできなくなるという課題がありました。

一方、メールアドレスはほぼすべてのインターネットユーザーが持っており、使い慣れた認証手段です。 DKIMの仕組みを使えば、特定のメールアドレスからメールが送られたことを暗号学的に証明できます。 この証明をオンチェーンで検証できれば、メールアドレスを起点としたウォレット操作や認証が可能になります。

アカウント抽象化との連携

zkEmailとこのレジストリを組み合わせると、メールベースのアカウント抽象化が実現できます。

ユーザーはDKIM署名を使ってメールアドレスの所有権を証明し、以下のことが可能になります。

  • スマートコントラクトウォレットの作成と管理
  • メールの認証情報を使ったトランザクションへの署名
  • ソーシャルリカバリーの仕組みの実装

アカウントリカバリー

レジストリはアカウントリカバリーの仕組みも可能にします。 ユーザーがウォレットの秘密鍵を紛失した場合でも、メールアドレスの所有権をオンチェーンで証明することでウォレットへのアクセスを復旧できます。 DKIM署名の暗号学的な性質により、このプロセスは信頼できる第三者なしで安全に行えます。

仕様

インターフェース定義

ERC7969が定義するインターフェースは非常にシンプルです。 ドメイン名のハッシュとDKIM公開鍵のハッシュのマッピングを管理し、検証するための最小限の機能を提供します。

javascript
pragma solidity ^0.8.25;

/// @title ERC-XXX DKIM Registry Interface
/// @dev ERC-165 identifier: 0xdee3d600
interface IDKIMRegistry {
    event KeyHashRegistered(bytes32 domainHash, bytes32 keyHash);
    event KeyHashRevoked(bytes32 domainHash);

    function isKeyHashValid(
        bytes32 domainHash,
        bytes32 keyHash
    ) external view returns (bool);
}

このインターフェースは3つの要素で構成されています。 KeyHashRegisteredイベントとKeyHashRevokedイベントで鍵の登録・失効を通知し、isKeyHashValid関数で特定のドメインに対する公開鍵ハッシュの有効性を確認します。

以下のセクションでは、パラメータとして使用されるドメインハッシュとキーハッシュの詳細を説明します。

ドメインハッシュ

domainHashパラメータは、ドメイン名を小文字に変換してからハッシュ化した値です。

例えばexample.comというドメインの場合、以下のようにkeccak256でハッシュ化します。

javascript
domainHash = keccak256(bytes("example.com"))

サブドメインの場合も同様に、完全なドメイン名を1つの文字列としてハッシュ化します。

javascript
domainHash = keccak256(bytes("mail.example.com"))

レジストリはドメインとサブドメインを独立したエンティティとして扱います。 つまり、example.comに登録された公開鍵ハッシュはmail.example.comには自動的に適用されません。 各サブドメインは独自のDKIM公開鍵ハッシュの登録を持つことができます。

以下の図は、レジストリのデータ構造を示しています。

DKIMレジストリのデータ構造

1つのドメインハッシュに対して複数の公開鍵ハッシュを登録でき、それぞれの有効・失効を個別に管理します。

ドメインとサブドメインの独立性

これはDKIMの実際の運用と一致しています。 メール送信サーバーは通常selector._domainkey.example.comのようなDNSレコードを持ち、サブドメインごとに異なる鍵を使うことが一般的です。

キーハッシュ

keyHashパラメータは、DKIM公開鍵の暗号学的ハッシュ値です。 公開鍵はRFC 6376で規定されたDKIMの標準フォーマットに従います。

ハッシュ関数の選択は実装に委ねられています。 一般的な選択肢としてkeccak256がありますが、ゼロ知識証明に適したPoseidonハッシュなど他の暗号学的に安全なハッシュ関数も使用できます。

Poseidonハッシュ

Poseidonは、ゼロ知識証明の回路内で効率的に計算できるよう設計されたハッシュ関数です。 zkEmailのようなZK(ゼロ知識証明)アプリケーションでは、証明生成の効率を大幅に改善するためにPoseidonが選ばれることがあります。

DKIM公開鍵の仕様

DKIM公開鍵はRFC 6376で規定されたフォーマットに従います。

鍵の要件を以下の表にまとめます。

要件 説明
フォーマット DNSレコードのp=パラメータに記載された値
エンコーディング Base64エンコード
鍵の種類 有効なRSA公開鍵
ヘッダー PEMヘッダーや追加のフォーマットは含めない
空白 ホワイトスペースや改行を含めない

公開鍵は、ドメインのDNS TXTレコードに公開されているp=パラメータの値そのものです。

登録フロー

DKIM公開鍵をレジストリに登録するまでの流れを図にまとめます。

sequenceDiagram
    participant Owner as ドメイン所有者
    participant DNS as DNSサーバー
    participant Registry as DKIMレジストリ
    participant Client as 検証クライアント

    Owner->>DNS: DKIM TXTレコードを設定
    Note over DNS: v=DKIM1, p=MIGfMA0G...
    Owner->>DNS: 公開鍵(pパラメータ)を取得
    DNS-->>Owner: Base64エンコードされた公開鍵
    Owner->>Owner: keyHash = keccak256(公開鍵)
    Owner->>Owner: domainHash = keccak256(ドメイン名)
    Owner->>Registry: setKeyHash(domainHash, keyHash)
    Registry-->>Registry: KeyHashRegistered発行
    Client->>Registry: isKeyHashValid(domainHash, keyHash)
    Registry-->>Client: true / false

この図の流れを具体例で説明します。 RFC 6376に記載されている以下のDKIMレコードを使用します。

javascript
$ORIGIN _domainkey.example.org.
brisbane IN TXT ("v=DKIM1; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQ"
                  "KBgQDwIRP/UC3SBsEmGqZ9ZJW3/DkMoGeLnQg1fWn7/zYt"
                  "IxN2SnFCjxOCKG9v3b4jYfcTNh5ijSsq631uBItLa7od+v"
                  "/RtdC2UzJ1lWT947qR+Rcac2gbto/NMqJ0fzfVjH4OuKhi"
                  "tdY9tf6mcwGjaNBcWToIMmPSPDdQPNUYckcQ2QIDAQAB")

このレコードのp=パラメータの後に続くBase64文字列がDKIM公開鍵です。

  1. ドメイン所有者がDNS TXTレコードからDKIM公開鍵を取得し、p=パラメータの値を抽出します。

  2. 公開鍵のkeccak256ハッシュを計算します。

    javascript
    bytes32 keyHash = keccak256(bytes("MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQ..."));
  3. ドメイン名のkeccak256ハッシュを計算します。

    javascript
    bytes32 domainHash = keccak256(bytes("example.org"));
  4. レジストリに登録します。

    javascript
    registry.setKeyHash(domainHash, keyHash);

このようにドメイン名も公開鍵も直接文字列としてコントラクトに保存するのではなく、ハッシュ値として保存します。 これにより、ストレージコストを削減しつつ、プライバシーも保護できます。

イベント

KeyHashRegistered

javascript
event KeyHashRegistered(bytes32 domainHash, bytes32 keyHash);

新しいDKIM公開鍵ハッシュがドメインに対して登録された時に発行されるイベントです。

どのドメイン(domainHash)にどの公開鍵ハッシュ(keyHash)が登録されたかを記録します。 オフチェーンのインデクサーやモニタリングシステムがこのイベントを監視することで、レジストリの変更をリアルタイムに追跡できます。

パラメータ 型 indexed 説明
domainHash bytes32 - 登録対象のドメイン名のハッシュ値
keyHash bytes32 - 登録されたDKIM公開鍵のハッシュ値

KeyHashRevoked

javascript
event KeyHashRevoked(bytes32 domainHash);

DKIM公開鍵ハッシュがドメインから失効された時に発行されるイベントです。

鍵のローテーション(定期的な鍵の更新)や、鍵の漏洩が判明した場合に古い鍵を無効化する目的で使用されます。

パラメータ 型 indexed 説明
domainHash bytes32 - 失効対象のドメイン名のハッシュ値

関数

isKeyHashValid

javascript
function isKeyHashValid(
    bytes32 domainHash,
    bytes32 keyHash
) external view returns (bool);

指定されたドメインハッシュに対して、指定された公開鍵ハッシュが有効かどうかを返す読み取り関数です。

zkEmailの検証回路やスマートコントラクトウォレットが、メールのDKIM署名に使用された公開鍵が正当なものかを確認する時にこの関数を呼び出します。 例えば、ユーザーが「自分はexample.comのメールアドレスの持ち主だ」と主張した場合、検証側はそのDKIM署名に使われた公開鍵のハッシュがレジストリに登録されているかを確認することで、主張の正当性を検証できます。

  • 引数
パラメータ 型 説明
domainHash bytes32 検証対象のドメイン名のハッシュ値
keyHash bytes32 検証するDKIM公開鍵のハッシュ値
  • 戻り値

bool 公開鍵ハッシュが有効な場合はtrue、無効な場合はfalseを返します。

補足

設計がシンプルな理由

インターフェースは意図的に最小限の機能に絞られています。 ドメインハッシュと公開鍵ハッシュの登録・検証・失効だけを標準化し、アクセス制御の方法や登録のワークフローは実装者に委ねています。

この設計により、以下のメリットがあります。

  • 実装のバリエーションが広がる(中央集権的な管理者方式、DAO方式、オラクル方式など)
  • zkEmailに限らず、DKIM検証を利用するあらゆるアプリケーションで共通のインターフェースを使える
  • ガスコストが低く、シンプルで監査しやすい

また、KeyHashRegisteredとKeyHashRevokedの2つのイベントにより、オフチェーンのシステムが鍵の登録・失効を効率的に追跡できます。 レジストリの整合性を維持するうえで、イベントによる変更履歴の記録は重要な役割を果たしています。

ハッシュ関数の柔軟性

仕様書のRationaleでは、ドメイン名と公開鍵の両方にkeccak256を使うことが一貫性と安全性の面で推奨されています。 一方で、公開鍵のハッシュについては実装者が暗号学的に安全な任意のハッシュ関数を選ぶことも許容されています。

これはzkEmailのようなゼロ知識証明アプリケーションとの互換性を考慮した設計です。 ZK回路内ではkeccak256は計算コストが高いため、PoseidonやSHA-256など回路に適したハッシュ関数を使う方が効率的です。 インターフェースをbytes32型で統一しているため、どのハッシュ関数を使っても同じインターフェースで互換性が保たれます。

既存規格との関係

ERC7969は新しいインターフェースを導入するもので、既存のコントラクトや規格に影響を与えません。

ERC4337(アカウント抽象化)と組み合わせることで、メールベースのアカウント操作が可能になります。 また、ERC165(インターフェース検出)をサポートしており、コントラクトがDKIMレジストリを実装しているかどうかをプログラム的に検出できます(インターフェース識別子は0xdee3d600)。

ERC165については以下の記事を参考にしてください。

セキュリティ

秘密鍵とドメインの管理

ドメイン所有者は、DKIM秘密鍵とドメイン名の管理を厳重に行う必要があります。 秘密鍵が漏洩した場合、攻撃者がそのドメインのメール署名を偽造できるため、レジストリに登録された公開鍵と合わせてメールアドレスの所有権を不正に主張される恐れがあります。

アクセス制御

レジストリの実装には、不正な登録を防ぐための適切なアクセス制御メカニズムが必要です。 参考実装ではOpenZeppelinのOwnableを使用しており、コントラクトのオーナーだけが鍵の登録と失効を行えます。 しかし、本番環境ではマルチシグやDAOベースのガバナンスなど、より堅牢なアクセス制御の導入を検討すべきです。

鍵のローテーション

DKIMの公開鍵は定期的にローテーション(更新)されるのが一般的です。 レジストリの実装は、古い鍵の失効と新しい鍵の登録をスムーズに行えるようにする必要があります。 鍵の更新が遅れると、正当なメールの検証が失敗したり、失効すべき古い鍵で不正な認証が行われる可能性があります。

レート制限

スパム的な大量登録を防止するため、レート制限の実装も考慮すべきです。 悪意あるアクターがレジストリに大量の偽の登録を行うことで、ストレージコストの増加やイベントログの肥大化を引き起こす可能性があります。

ハッシュ関数の選択

domainHashとkeyHashに使用するハッシュアルゴリズムは慎重に選ぶ必要があります。 特に、将来的にハッシュ関数をアップグレードする場合は十分な計画が必要です。 新旧のハッシュ関数が混在すると、同じドメイン・同じ公開鍵でも異なるハッシュ値が生成され、検証の一貫性が失われる恐れがあります。

参考実装

以下は、OpenZeppelinのOwnableを使用したDKIMレジストリの参考実装です。

contracts/DKIMRegistry.sol
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/access/Ownable.sol";
import "./interfaces/IDKIMRegistry.sol";

contract DKIMRegistry is IDKIMRegistry, Ownable {
    constructor(address _owner) Ownable(_owner) { }

    // ドメイン名ハッシュ => 公開鍵ハッシュ => 有効フラグ
    mapping(bytes32 => mapping(bytes32 => bool)) private _keyHashes;

    function isKeyHashValid(
        bytes32 domainHash,
        bytes32 keyHash
    ) public view returns (bool) {
        return _keyHashes[domainHash][keyHash];
    }

    function setKeyHash(
        bytes32 domainHash,
        bytes32 keyHash
    ) public onlyOwner {
        require(keyHash != bytes32(0), "cannot set zero hash");
        _keyHashes[domainHash][keyHash] = true;
        emit KeyHashRegistered(domainHash, keyHash);
    }

    function setKeyHashes(
        bytes32 domainHash,
        bytes32[] memory keyHashes
    ) public onlyOwner {
        require(keyHashes.length > 0, "empty array");
        for (uint256 i = 0; i < keyHashes.length; i++) {
            setKeyHash(domainHash, keyHashes[i]);
        }
    }

    function revokeKeyHash(
        bytes32 domainHash,
        bytes32 keyHash
    ) public onlyOwner {
        delete _keyHashes[domainHash][keyHash];
        emit KeyHashRevoked(domainHash);
    }
}

このコントラクトは以下の機能を提供しています。

ストレージ構造

_keyHashesはネストされたマッピングです。 第1キーがドメイン名のハッシュ、第2キーが公開鍵のハッシュで、値が有効フラグ(bool)です。 この構造により、1つのドメインに対して複数の公開鍵を登録できます。 これはDKIMの実運用で、複数のメールサーバーが異なるセレクターで異なる鍵を使う場面に対応しています。

setKeyHash

単一の公開鍵ハッシュを登録する関数です。 onlyOwner修飾子により、コントラクトのオーナーだけが実行できます。 ゼロハッシュ(bytes32(0))の登録は禁止されており、無効な鍵の登録を防ぎます。 登録に成功するとKeyHashRegisteredイベントが発行されます。

setKeyHashes

複数の公開鍵ハッシュを一括で登録する関数です。 内部的にはsetKeyHashを繰り返し呼び出しています。 空の配列を渡すとリバートします。

revokeKeyHash

特定のドメインから公開鍵ハッシュを失効させる関数です。 マッピングの値をdeleteで削除(falseに戻す)し、KeyHashRevokedイベントを発行します。 鍵のローテーションや漏洩時の無効化に使用します。

KeyHashRevokedイベントはdomainHashのみを記録しますが、関数自体はdomainHashとkeyHashの2つの引数を取り、そのドメインの特定の公開鍵だけを失効させます。

  • 引数
パラメータ 型 説明
domainHash bytes32 失効対象のドメイン名のハッシュ値
keyHash bytes32 失効させるDKIM公開鍵のハッシュ値

引用

最後に

今回は「ERC7969によるDKIM公開鍵のオンチェーンレジストリ」についてまとめてきました! いかがだったでしょうか?

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

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