0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

メール確認コードを不要にする Email Verification Protocol (EVP)

0
Last updated at Posted at 2026-08-13

はじめに

アカウント登録時のメールアドレス確認は、Web でもっとも普及していて、もっとも面倒な手続きのひとつです。ユーザーは次のような往復を強いられます。

WICG の explainer によると、アクセス数上位 50 サイトのうち 95% がメールアドレスによるアカウント作成に対応しており、そのうち 73% はメール確認が完了するまでアカウント作成を先に進めさせません。つまり、この往復はコンバージョンの真正面に置かれています。

Email Verification Protocol(EVP) は、この往復そのものを消し去ろうという提案です。確認メールを一切送らずに、ブラウザが email プロバイダーから署名済みトークンを取得し、サイトに提示することでメールアドレスの所有を証明します。

2026年6月に IETF Internet-Draft が公開され、7月には Chrome でオリジントライアルが始まりました。この記事では、プロトコルの設計と実装のポイントを整理します。

仕様の全体像

EVP は「HTTP レベルのプロトコル」と「ブラウザ API」の 2 つの仕様に分かれています。

仕様 策定組織 内容 ステータス
Email Verification Protocol IETF 発行者ディスカバリー、トークン発行リクエスト、EVT / KB-JWT の形式と検証 Internet-Draft(draft-hardt-email-verification-01、2026年7月4日)
Email Verification API W3C / WICG HTML 拡張(autocomplete / nonce)、ブラウザの処理モデル Unofficial Draft(WICG)

著者は Dick Hardt 氏(Hellō)と Sam Goto 氏(Google)です。Hardt 氏は OAuth 2.0 の、Goto 氏は FedCM の中心人物で、EVP は両者の系譜が合流した設計になっています。

IETF ドラフトが「ブラウザと発行者の間の通信」を、W3C 仕様が「ユーザーとサイトとブラウザの間のやりとり」を担当する、という分担です。実装するときはどちらか片方だけを読んでも足りません。

登場人物とプロトコルフロー

EVP には 3 つの役割が登場します。

  • Verifier(RP) — メールアドレスを確認したい Web サイト
  • User Agent — 仲介するブラウザ
  • Issuer(発行者) — そのメールドメインについて権威を持つ email プロバイダー

全体の流れは次のようになります。

このフローの肝は 3 者モデル です。発行者はブラウザの公開鍵に紐づいた EVT を発行するだけで、どのサイトが確認を要求したのかを知りません。RP に紐づける作業(Key Binding)はブラウザが行います。

Web サイト側の実装

RP 側の実装は驚くほど小さく、フォームに隠し <input> を 1 つ追加するだけです。

<form action="/signup" method="post">
  <label for="email">メールアドレス</label>
  <input type="email" id="email" name="email" autocomplete="email">

  <!-- EVP 用の隠しフィールド -->
  <input type="hidden"
         name="evt"
         autocomplete="email-verification-token"
         nonce="xyz123456789">

  <button type="submit">登録</button>
</form>

ポイントは 2 つです。

  • autocomplete="email-verification-token" — このフィールドに EVT を埋めてほしい、というブラウザへの宣言
  • nonce 属性 — サーバー側で生成した 128 ビット以上のランダム値。<script> / <style> 用だった nonce 属性を <input> にも拡張したもので、ページのレンダリングごとに一意である必要がある

ユーザーがオートフィルからメールアドレスを選ぶと、ブラウザが裏側で発行者との通信を済ませ、フォーム送信の直前に隠しフィールドへトークンを流し込みます。サーバーには通常の POST として届きます。

POST /signup HTTP/1.1
Host: rp.example
Content-Type: application/x-www-form-urlencoded

email=user%40email.example&evt=eyJhbGciOiJFZERTQSIsImtpZCI6...

ブラウザがトークンを埋めるのは、オートフィルの候補から選択されたメールアドレスの場合だけです。ユーザーが手入力した場合や、EVP 非対応ブラウザの場合は隠しフィールドが空のまま届きます。従来の確認メールへのフォールバックは必ず用意してください。

発行者ディスカバリー

ブラウザは、選択されたメールアドレスのドメインから発行者を突き止めます。使うのは .well-known ではなく DNS TXT レコードです。

_email-verification.email-domain.example   TXT   iss=issuer.example

.well-known ではなく DNS を使う理由は、ドラフトの Design Rationale に明記されています。

  1. メール専用の独自ドメインには Web サーバーが無いことが多く、.well-known を要求すると導入の障壁になる
  2. メールドメインは apex ドメインであることが多く、CNAME が使えないため Web サイトのホスティングに追加のインフラが要る
  3. ドメイン所有者は MX / SPF / DKIM / DMARC で既に DNS レコードを管理していて、TXT レコードの追加は既存の運用に馴染む

さらにセキュリティ上の効果もあります。DNS へのアクセス権と Web サーバーへのアクセス権は普通は別なので、issuer.example の Web サイトを触れるだけの内部犯が勝手に発行者を仕立てて任意のメールアドレスの検証済みトークンを得る、ということができません。

発行者が判明したら、そのメタデータを取得します。

GET https://{issuer}/.well-known/email-verification
{
  "issuance_endpoint": "https://accounts.issuer.example/email-verification/issuance",
  "jwks_uri": "https://accounts.issuer.example/email-verification/jwks",
  "signing_alg_values_supported": ["EdDSA", "RS256"],
  "webauthn_supported": true,
  "private_email_supported": true
}

signing_alg_values_supported が省略された場合のデフォルトは EdDSA です。none は使用禁止と明記されています。

なお、ブラウザがユーザーのログイン状態とアカウント一覧を確認する部分には Login Status API と FedCM の accounts_endpoint が再利用されます。発行者側は Set-Login: logged-in ヘッダー、あるいは navigator.login.setStatus("logged-in") でログイン状態をブラウザに伝えておく必要があります。

トークン発行リクエスト

ブラウザは、リクエストごとに使い捨ての鍵ペアを生成し、RFC 9421(HTTP Message Signatures) で署名したリクエストを発行エンドポイントに POST します。

POST /email-verification/issuance HTTP/1.1
Host: accounts.issuer.example
Cookie: session=...
Content-Type: application/json
Sec-Fetch-Dest: email-verification
Signature-Input: sig=("@method" "@authority" "@path" \
    "cookie" "signature-key");created=1692345600
Signature: sig=:MEQCIHd8Y8qYKm5e3dV8y....:
Signature-Key: sig=hwk; kty="OKP"; crv="Ed25519"; \
    x="JrQLj5P_89iXES9-vFgrIy29clF9CC_oPPsw3c5D0bs"

{"email":"user@example.com"}

署名対象には @method@authority@pathsignature-key が必須で、Cookie ヘッダーがある場合は cookie も含めます。公開鍵は Signature-Key ヘッダーに hwk(Header Web Key)スキームでインラインに載せるため、事前の鍵登録は不要です。

この設計が生む性質は次のとおりです。

仕組み 効果
cookie を署名対象に含める 認証クッキーがリクエストに暗号的に束縛され、クッキーの注入・すり替えを防ぐ
created を 60 秒以内で検証 リプレイ攻撃の防止
Signature-Key の公開鍵 EVT の cnf クレームに入る鍵と一致することが保証される
鍵がリクエストごとに使い捨て ブラウザは発行者に対して自身を識別させず、検証フロー間での相関も避けられる

W3C 側の現行仕様と IETF ドラフトで、リクエスト形式が食い違っています。W3C 仕様(および Chrome の現在の実装)は、公開鍵をヘッダーに載せた request_token という JWT を application/x-www-form-urlencoded で送る形式です。IETF ドラフトはこれを deprecated として扱い、HTTP Message Signatures に置き換えています。発行者を実装する場合は、当面どちらの形式が来ても処理できるようにしておくのが安全です。

同様に、.well-known のパスも IETF ドラフトが /.well-known/email-verification、W3C 仕様が FedCM の well-known を使うという不整合があり、こちらは「今後 1 つに揃える」と明記されています。

EVT — Email Verification Token

発行者は、クッキーからユーザーを認証し、そのユーザーが当該メールアドレスを保有していることを確認したうえで EVT を返します。

HTTP/1.1 200 OK
Content-Type: application/json

{"issuance_token":"eyJhbGciOiJFZERTQSIsImtpZCI6IjIwMjQtMDgtMTkiLCJ0eXAiOiJldnQrand0In0...~"}

EVT は JWT で、ヘッダーの typevt+jwt です。ペイロードは次のようになります。

{
  "iss": "issuer.example",
  "iat": 1724083200,
  "cnf": {
    "jwk": {
      "kty": "OKP",
      "crv": "Ed25519",
      "x": "JrQLj5P_89iXES9-vFgrIy29clF9CC_oPPsw3c5D0bs"
    }
  },
  "email": "user@example.com",
  "email_verified": true
}

issiatcnfemailemail_verified が必須クレームです。cnf.jwk にはブラウザが生成した公開鍵が入り、これが後段の Key Binding の土台になります。プライベートメールアドレスが発行された場合は is_private_email: true が追加されます(このクレーム名は Sign in with Apple との互換性のために揃えられています)。

そして EVT の末尾には ~ が付きます。これは SD-JWT 形式に合わせるためです。

Key Binding — EVT を RP に束縛する

EVT を受け取ったブラウザは、まず自分で EVT を検証します(email が選択されたアドレスと一致するか、cnf.jwk が自分の生成した公開鍵か)。そのうえで KB-JWT を生成します。

{
  "aud": "https://rp.example",
  "nonce": "259c5eae-486d-4b0f-b666-2a5b5ce1c925",
  "iat": 1724083260,
  "sd_hash": "X9yH0Ajrdm1Oij4tWso9UzzKJvPoDxwmuEcO3XAdRC0"
}

ヘッダーの typkb+jwt、署名にはブラウザの秘密鍵を使います。sd_hash は EVT(末尾の ~ を含む)の SHA-256 ハッシュです。

最終的に RP に渡るのは、この 2 つをチルダで連結した EVT+KB です。

<EVT>~<KB-JWT>

EVT は既に末尾に ~ を持っているので、実際の構造は <JWT>~<KB-JWT> になります。これは SD-JWT(RFC 9682) の Key Binding 付き形式そのもので、選択的開示の機能こそ使いませんが、既存の SD-JWT ライブラリでそのままパース・検証できます。

「発行」と「提示」を別々の操作に分離できることが、3 者モデルを成立させている技術的な要です。

RP 側の検証

RP がやるべきことは、受け取った EVT+KB を分解して 2 つの署名を検証することです。jose を使った概念的な実装は次のようになります。

import { createHash } from "node:crypto";
import { createRemoteJWKSet, decodeJwt, decodeProtectedHeader, importJWK, jwtVerify } from "jose";

const RP_ORIGIN = "https://rp.example";

async function verifyEvtKb(evtKb, { expectedNonce, submittedEmail }) {
  // 1. EVT と KB-JWT に分解する
  const [evtJwt, kbJwt] = evtKb.split("~");

  // 2. EVT の iss が、メールドメインが委任した発行者と一致するか確認する
  const { iss, email } = decodeJwt(evtJwt);
  const issuer = await discoverIssuer(email); // _email-verification.<domain> の TXT を参照
  if (iss !== issuer) throw new Error("issuer mismatch");

  // 3. 発行者のメタデータから JWKS を取得し、EVT の署名を検証する
  const meta = await fetch(`https://${issuer}/.well-known/email-verification`).then((r) => r.json());
  const jwks = createRemoteJWKSet(new URL(meta.jwks_uri));
  const { payload: evt } = await jwtVerify(evtJwt, jwks, { typ: "evt+jwt" });
  if (evt.email_verified !== true) throw new Error("email not verified");

  // 4. EVT の cnf.jwk(= ブラウザの公開鍵)で KB-JWT を検証する
  const holderKey = await importJWK(evt.cnf.jwk, decodeProtectedHeader(kbJwt).alg);
  const { payload: kb } = await jwtVerify(kbJwt, holderKey, {
    typ: "kb+jwt",
    audience: RP_ORIGIN, // aud が自分のオリジンか
  });

  // 5. nonce とハッシュを突き合わせる
  if (kb.nonce !== expectedNonce) throw new Error("nonce mismatch");
  const sdHash = createHash("sha256").update(`${evtJwt}~`).digest("base64url");
  if (kb.sd_hash !== sdHash) throw new Error("sd_hash mismatch");

  // 6. フォームに入力されたアドレスと一致するか(大文字小文字は区別しない)
  if (evt.email.toLowerCase() !== submittedEmail.toLowerCase()) {
    throw new Error("email mismatch");
  }

  return { email: evt.email, isPrivate: evt.is_private_email === true };
}

audnoncesd_hash の 3 点確認がリプレイ対策の中心です。aud が自分のオリジンであることを確認しないと、他サイト向けに発行されたトークンを持ち込まれても気付けません。

プライベートメールアドレス

EVP には、Sign in with Apple の「メールを非公開」に相当する機能が組み込まれています。ブラウザはリクエストボディで新しいプライベートアドレスを要求できます。

{
  "email": "user@example.com",
  "private_email": true
}

同じ RP に再訪したときは、保存しておいたアドレスを directed_email で渡すと、発行者は同じアドレスを返します。

{
  "email": "user@example.com",
  "directed_email": "u7x9k2m4@privaterelay.example"
}

面白いのは、プライベートアドレスのドメインが元のメールドメインと一致しなくてよい点です。user@example.com に対して u7x9k2m4@privaterelay.different.example を発行してよく、その場合 EVT の iss はプライベートドメイン側の発行者になります。これによって、独自ドメイン(me@myname.example のようにドメイン自体が個人を特定してしまうケース)でも匿名性を確保できます。

なぜこの機能が必要なのか、という理由付けがドラフトには率直に書かれています。検証の摩擦を減らすこと自体が、プライバシー問題を加速させるからです。手軽になれば、ユーザーはより多くのサイトに実アドレスを渡すようになり、サイト間での名寄せの余地が広がります。プライベートアドレスは、非相関な識別子を実アドレスと同じ手軽さで渡せるようにすることで、この副作用を打ち消す仕掛けです。

クッキーが無いとき — WebAuthn フォールバック

発行者が webauthn_supported: true を宣言している場合、クッキーが無い・無効なときに WebAuthn チャレンジを返せます。

HTTP/1.1 401 Unauthorized
Content-Type: application/json

{
  "webauthn_challenge": {
    "challenge": "dGVzdC1jaGFsbGVuZ2UtZGF0YQ",
    "timeout": 60000,
    "rpId": "issuer.example",
    "allowCredentials": [{ "type": "public-key", "id": "Y3JlZGVudGlhbC1pZA" }],
    "userVerification": "preferred"
  }
}

中身は PublicKeyCredentialRequestOptions そのままです。ブラウザはアサーションを取得して、webauthn_response を含めた新しいリクエストを再送します。email プロバイダーにログインしていなくても、パスキーがあればその場で検証を完了できるという設計です。

セキュリティ上の考慮点

発行エンドポイントはブラウザ以外からも叩けるので、**メールアドレスの存在確認(probing)**が最大の懸念になります。ドラフトはこれに対して厳しい要求を置いています。

  • エラーレスポンスを統一する(MUST) — 「アドレスが存在しない」「存在するが未認証」「存在するが認証ユーザーの管理下にない」のいずれも、一律に authentication_required を返さなければならない
  • タイミング攻撃を緩和する(SHOULD) — アドレスの存在有無でコードパスを分岐させず、DB 参照や暗号処理を同じだけ実行する。あるいはレスポンス時間を一定値に正規化する
  • レート制限をかける(SHOULD) — IP 単位で制限する
  • Sec-Fetch-Dest: email-verification を検証する — ブラウザ由来である弱いシグナルにはなるが、偽装可能であることは仕様も認めている

もっとも、確認メールを送ってバウンスを見れば同じことは既に可能なので、EVP が新たな情報漏洩を生むわけではない、という整理もされています。

DNS 側では、ディスカバリーが TXT レコードに依存するため、DNS が侵害されると悪意ある発行者へ誘導されます。ブラウザは DNS-over-HTTPS / DNS-over-TLS を使い、可能なら DNSSEC を検証すべきとされています。

既存の手法との比較

確認メール(OTP / マジックリンク) ソーシャルログイン EVP
メール送信 必要 不要 不要
コンテキストの移動 必要 ポップアップ等 不要
RP が IdP に知られる 送信時に知られる 常に知られる 知られない
フィッシング耐性 低い(コードを渡せてしまう) オリジン束縛あり オリジン束縛あり
到達性の確認 できる できない できない
RP 間の名寄せ 実アドレスで可能 実装依存 プライベートアドレスで防げる
事業者ごとの個別実装 不要 プロバイダーごとに必要 不要(プロトコル共通)

EVP が証明するのは「ユーザーがそのアドレスを管理していること」であって、「そのアドレスにメールが届くこと」ではありません。ここはソーシャルログインと同じ制約です。配信可能性が業務上重要なら、別途確認する仕組みが要ります。

なお「SMS の WebOTP と同じ方式にすればいいのでは」という疑問には、ドラフトが明快に答えています。SMS は OS が受信を握るモバイル機能なので OS がオートフィルを仲介できるのに対し、メールはアプリケーション層のものでブラウザと協調できる OS 級のサブシステムが存在しない、というのが根本的な違いです。加えて SMS の送信元はアグリゲーター(Twilio 等)に集約されて RP を特定しにくいのに対し、確認メールは RP のドメインから直接届くため、送信するだけで email プロバイダーに RP が露出してしまいます。

実装・対応状況

2026年8月時点の状況です。

実装 役割 状態
Chrome ブラウザ オリジントライアル(デスクトップ Chrome 150〜153)
Edge ブラウザ オリジントライアル
Gmail 発行者 オリジントライアル(@gmail.com アドレスで追加設定なく試せる)
Hellō 発行者 試験的実装(発行エンドポイント、DNS TXT、JWKS)

標準化のステータスとしては、IETF 側は draft-hardt-email-verification-01 が Standards Track を目指す個人ドラフト、W3C 側は WICG の Unofficial Draft という段階です。他ブラウザベンダーの姿勢は次のようになっています。

  • Mozilla — 「Defer」。判断に必要な情報が揃う前に早期レビューに出された、という理由付けで、提案側は改めて標準化ポジションを求め直す意向を示しています
  • WebKit — 正式なレビューは未実施(No signal)。TPAC での非公式なフィードバックでは、email プロバイダーを動かすより WebOTP / one-time-code を拡張して IMAP クライアント側を動かす方向を好む、という反応だったと記録されています

つまり、現時点では Chromium 系の実験段階であり、仕様も 2 つの文書間で細部が食い違ったまま動いています。本番投入を検討する段階ではありませんが、フォールバック前提で組み込むぶんには壊れにくい設計になっています。

導入を検討するときの勘所

  • フォールバックは必須 — EVP が成立するのは「対応ブラウザ」「対応 email プロバイダー」「発行者にログイン中」「オートフィルから選択」がすべて揃ったときだけです。空のトークンが届く前提で設計します
  • nonce はサーバー側で生成し、セッションに紐づける — 128 ビット以上のエントロピーで、ページのレンダリングごとに一意、かつ有効期限付きにします
  • aud の検証を省略しない — トークンを自分のオリジンに束縛する唯一の手段です
  • 到達性の確認とは切り分ける — マーケティングメールの配信可能性を担保したい場合は、EVP だけでは要件を満たしません
  • 仕様の揺れを織り込む — 特に発行者を実装する場合、リクエスト形式と .well-known のパスが今後変わる前提でコードを分離しておくのが無難です

まとめ

EVP は、「確認コードのコピペ」という Web でもっとも摩擦の大きい体験を、ブラウザを仲介役に立てることで丸ごと省く提案です。技術的には、DNS による委任、RFC 9421 の HTTP 署名、SD-JWT の Key Binding という既存の部品を組み合わせて、発行者に RP を知らせないという新しい性質を作り出している点が面白いところです。

一方で、email プロバイダー・ブラウザ・Web サイトという 3 つの独立した層すべてに変更を要求する提案でもあり、普及には相応の時間がかかります。Mozilla と WebKit の反応が保留であることも含め、しばらくは Chromium の実験を観測しつつ、フォールバック付きで試す段階です。

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?