<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE rfc [
  <!ENTITY nbsp    "&#160;">
  <!ENTITY zwsp   "&#8203;">
  <!ENTITY nbhy   "&#8209;">
  <!ENTITY wj     "&#8288;">
]>
<!-- name="GENERATOR" content="github.com/mmarkdown/mmark Mmark Markdown Processor - mmark.miek.nl" -->
<rfc xmlns:xi="http://www.w3.org/2001/XInclude" version="3" ipr="trust200902" docName="draft-hardt-email-verification-02" submissionType="IETF" category="std" xml:lang="en" indexInclude="true">

<front>
<title abbrev="EVP">Email Verification Protocol</title><seriesInfo value="draft-hardt-email-verification-02" stream="IETF" status="standard" name="Internet-Draft"/>
<author initials="D." surname="Hardt" fullname="Dick Hardt"><organization>Hellō</organization><address><postal><street/>
</postal><email>dick.hardt@gmail.com</email>
</address></author><author initials="S." surname="Goto" fullname="Sam Goto"><organization>Google</organization><address><postal><street/>
</postal><email>goto@google.com</email>
</address></author><date/>
<area>Security</area>
<workgroup>TBD</workgroup>
<keyword>email</keyword>
<keyword>verification</keyword>
<keyword>identity</keyword>
<keyword>authentication</keyword>

<abstract>
<t>This document defines the Email Verification Protocol (EVP), the HTTP-level protocol by which a browser obtains a signed email verification token from an issuer and presents it to a relying party (RP). The protocol enables web applications to verify that a user controls an email address without sending a verification email. It uses a three-party model in which the browser intermediates between the RP and the issuer, hiding the RP's identity from the issuer and supporting private, per-RP email addresses to prevent cross-site correlation.</t>
<t>This document covers issuer discovery, the token issuance request, the Email Verification Token (EVT) and Key Binding JWT (KB-JWT) formats, and token verification. The browser API — how the user selects an email address and how the token is delivered to the RP — is defined in the companion W3C Email Verification API (<xref target="EVP-Browser"/>).</t>
</abstract>

<note><name>Discussion Venues</name>
<t><em>Note: This section is to be removed before publishing as an RFC.</em></t>
<t>Source for this draft and an issue tracker can be found at <eref target="https://github.com/dickhardt/email-verification">https://github.com/dickhardt/email-verification</eref>.</t>
<t>Discussion of this document takes place on the evp-discuss mailing list. Subscribe at <eref target="https://mailman3.ietf.org/mailman3/lists/evp-discuss.ietf.org/">https://mailman3.ietf.org/mailman3/lists/evp-discuss.ietf.org/</eref> and view the archive at <eref target="https://mailarchive.ietf.org/arch/browse/evp-discuss/">https://mailarchive.ietf.org/arch/browse/evp-discuss/</eref>.</t>
<t>The browser API aspects are being developed separately by the W3C (<xref target="EVP-Browser"/>).</t>
</note>

</front>

<middle>

<section anchor="introduction"><name>Introduction</name>
<t>Web applications verify email addresses to send emails to users (transactional notifications, marketing, password resets) and to identify users (as a stable identifier for account creation and authentication). The standard verification method—sending a one-time code via email—has two problems: verification friction and privacy leakage.</t>

<section anchor="verification-friction"><name>Verification Friction</name>
<t>The email one-time code flow requires the user to switch to their email client, wait for the message to arrive, find it (possibly in spam), read the code, return to the application, and enter it. Many users abandon this process before completing it.</t>
<t>Some approaches to reduce this friction:</t>

<ul>
<li><t><strong>Social login</strong>: When a user has an account with Google, Apple, or another identity provider, the application can obtain a verified email without sending a verification message. However, this requires the user to have and use a social account, and requires developers to integrate with each provider separately.</t>
</li>
<li><t><strong>Magic links</strong>: Instead of a code, the verification email contains a link the user clicks to verify. This eliminates copying and pasting the code, but still requires switching to the email client, waiting for delivery, and finding the email.</t>
</li>
</ul>
</section>

<section anchor="privacy-leakage"><name>Privacy Leakage</name>
<t>Email verification creates two privacy problems:</t>

<ol>
<li><t><strong>RP-to-RP correlation</strong>: When a user provides their real email address to multiple relying parties (RPs), those RPs can correlate the user across sites by comparing email addresses.</t>
</li>
<li><t><strong>User-RP visibility</strong>: The email provider learns which RPs the user visits and when. With email OTP, the provider sees verification emails from the sender and the delivery timing for every verification. With social login, the identity provider sees every RP request.</t>
</li>
</ol>
<t>Reducing friction in email verification accelerates both privacy problems — users verify to more sites, increasing correlation potential and provider visibility.</t>
</section>

<section anchor="friction-solution"><name>Friction Solution</name>
<t>The Email Verification Protocol (EVP) enables a web application to obtain a verified email address <strong>without sending an email</strong> and <strong>without the user leaving the web page</strong>. The browser intermediates between the RP and an issuer, obtaining a signed token that contains an email address for the user that the RP can verify. This eliminates the email delivery step entirely.</t>
<t><strong>Note on deliverability</strong>: Like social login, this protocol verifies that the user controls an email address — it does not verify that the email address can receive mail.</t>
</section>

<section anchor="privacy-solution"><name>Privacy Solution</name>
<t>EVP addresses both privacy problems:</t>
<t><strong>Three-party model</strong>: The browser intermediates between the RP and issuer, ensuring the issuer never learns which RP requested verification. See <eref target="#protocol-flow">Protocol Flow</eref> for details.</t>
<t><strong>Private email addresses</strong>: The browser can request a private email address instead of the user's actual email. Private addresses that differ per RP cannot be correlated across sites. See <eref target="#private-email">Private Email Addresses</eref> for details.</t>
</section>
</section>

<section anchor="protocol-flow"><name>Protocol Flow</name>
<t>This document specifies the IETF protocol aspects of email verification: the HTTP-level interactions between the browser, issuer, and the application, aka relying party (RP). How the browser obtains the email address from the user (browser APIs, user interface elements, etc.) and how the browser communicates with the RP is being defined by the W3C (<xref target="EVP-Browser"/>).</t>

<ul>
<li><t><strong>Issuer</strong>: The service that verifies the user controls an email address. See <eref target="#issuer-discovery">Issuer Discovery</eref> for how email domains delegate to issuers.</t>
</li>
<li><t><strong>Three-party model</strong>: The protocol uses a three-party model where the browser intermediates between the RP and issuer. The issuer issues a email verification token (EVT) to the browser containing the email address and the browser's key material—but not the RP identity. The browser then creates a key binding token (KB-JWT) that ties the EVT to a specific RP. The combined token (EVT+KB) is what the RP receives. This separation hides the RP from the issuer during verification.</t>
</li>
</ul>
<t>The following diagram illustrates the protocol flow between the RP Server, Browser, and Issuer:</t>

<artwork><![CDATA[Step                      RP Server     Browser              Issuer
                               |            |                    |
2.1 Email Discovery            |            |<- discover accts ->|
                               |            |   (push or pull)   |
                               |            |                    |
2.2 Session Binding            |--- nonce ->|                    |
                               |            |                    |
2.3 Email Acquisition          |      [obtain email from user]   |
                               |            |                    |
2.4 Token Request              |            |-- POST /issuance ->|
                               |            |    (email, ...)    |
                               |            |                    |
2.5 EVT Creation               |            |           [create EVT]
                               |            |                    |
2.6 Token Issuance             |            |<------ EVT --------|
                               |            |                    |
2.7 KB Creation                |        [create KB-JWT]          |
                               |            |                    |
2.8 Token Presentation         |<-- EVT+KB -|                    |
                               |            |                    |
2.9 Token Verification    [verify EVT+KB]   |                    |
                               |            |                    |
]]>
</artwork>

<section anchor="email-discovery"><name>Email Discovery</name>
<t>How the browser assembles candidate addresses — from its own store, from account information an issuer pushes or the browser pulls via FedCM (<xref target="LightweightFedCM"/>), or from the user typing one — is defined by the W3C Email Verification API (<xref target="EVP-Browser"/>) and is out of scope for this document. The browser MUST NOT offer an address that is not a valid email address (see <eref target="#valid-email-address">Valid Email Address</eref>).</t>
<t>Once the user selects an address, the browser performs <eref target="#issuer-discovery">Issuer Discovery</eref> on the address's domain; the DNS delegation alone determines the issuer, whatever the source of the address. From selection onward the address is never altered: the browser sends it byte-for-byte in the token request, and an issuer that verifies it — by its own processing rules — returns it byte-for-byte in the <tt>email</tt> claim. Every comparison of email addresses in this protocol is byte-for-byte.</t>
</section>

<section anchor="session-binding"><name>Session Binding</name>
<t>The RP Server generates a cryptographically random nonce with at least 128 bits of entropy and binds it to a session it has with the browser. The nonce MUST be unique per verification request and SHOULD be valid for a limited time window. How the RP Server provides the nonce to the browser is being defined by the W3C (<xref target="EVP-Browser"/>).</t>
</section>

<section anchor="email-acquisition"><name>Email Acquisition</name>
<t>The browser obtains an email address from the user. This mechanism is being defined by the W3C (<xref target="EVP-Browser"/>).</t>
</section>

<section anchor="token-request"><name>Token Request</name>
<t>Once the browser has the email address and nonce:</t>

<ol>
<li><t>The browser performs <eref target="#issuer-discovery">Issuer Discovery</eref> for the email address to obtain the issuer's metadata, including the <tt>issuance_endpoint</tt>.</t>
</li>
<li><t>The browser generates a fresh private/public key pair. The browser SHOULD select an algorithm from the issuer's <tt>signing_alg_values_supported</tt> array, or use <tt>Ed25519</tt> if not present.</t>
</li>
<li><t>The browser creates a signed request per <eref target="#http-signatures">HTTP Message Signatures</eref> and POSTs to the <tt>issuance_endpoint</tt>, including the issuer's cookies. The request body is a JSON object with the following parameters:</t>

<ul spacing="compact">
<li><tt>email</tt> (REQUIRED): The email address to verify, byte-for-byte as the user selected it</li>
<li>See <eref target="#private-email">Private Email Addresses</eref> for parameters to request private email addresses</li>
</ul></li>
</ol>

<sourcecode type="http"><![CDATA[POST /email-verification/issuance HTTP/1.1
Host: accounts.issuer.example
Cookie: session=...
Content-Type: application/json
Sec-Fetch-Dest: email-verification
Content-Digest: \
    sha-256=:p8W2nSiyrdmtuSb49YHusp+pXM3er/ZiwRThsUgvua8=:
Signature-Input: sig=("@method" "@authority" "@path" \
    "content-digest" "signature-key");created=1692345600
Signature: sig=:MEQCIHd8Y8qYKm5e3dV8y....:
Signature-Key: sig=hwk;kty="OKP";crv="Ed25519"; \
    x="JrQLj5P_89iXES9-vFgrIy29clF9CC_oPPsw3c5D0bs";alg="Ed25519"

{"email":"user@example.com"}
]]>
</sourcecode>
<blockquote><t>Note: The W3C Email Verification API (<xref target="EVP-Browser"/>) currently specifies a different request format: the browser constructs a signed JWT (<tt>request_token</tt>) with the public key in the header (<tt>alg</tt>, <tt>jwk</tt>) and <tt>aud</tt>, <tt>iat</tt>, and <tt>email</tt> in the payload, sent with <tt>Content-Type: application/x-www-form-urlencoded</tt>. Existing deployments use this format; it is considered deprecated by this specification in favor of HTTP Message Signatures.</t>
</blockquote></section>

<section anchor="evt-issuance"><name>EVT Issuance</name>
<t>On receipt of a token request:</t>

<ol>
<li><t>The issuer verifies the request per <eref target="#request-verification">Request Verification</eref>.</t>
</li>
<li><t>The issuer checks if the cookies represent a logged-in user who controls the requested email address. If they do not, the issuer returns an <tt>authentication_required</tt> error (see <eref target="#authentication-required">Authentication Required</eref>).</t>
</li>
<li><t>If authentication succeeds, the issuer creates an EVT per <eref target="#evt-creation">EVT Creation</eref> and returns it as the value of <tt>issuance_token</tt> in an <tt>application/json</tt> response. The issuer MAY include <tt>Set-Cookie</tt> headers to establish or update session state:</t>
</li>
</ol>

<sourcecode type="http"><![CDATA[HTTP/1.1 200 OK
Content-Type: application/json
Set-Cookie: session=...; Secure; HttpOnly; SameSite=None

{"issuance_token":"eyJhbGciOiJFZDI1NTE5Iiwia2lkIjoiMjAyNC0wOC0xOSIsInR5cCI6ImV2dCtqd3QifQ...~"}
]]>
</sourcecode>
<t>The browser MUST process any <tt>Set-Cookie</tt> headers in the response.</t>
</section>

<section anchor="kb-creation"><name>KB Creation</name>
<t>On receiving the <tt>issuance_token</tt>:</t>

<ol>
<li><t>The browser verifies the EVT per <eref target="#evt-verification">EVT Verification</eref>, additionally confirming:</t>

<ul spacing="compact">
<li>The <tt>email</tt> claim is byte-for-byte identical to the email address being verified</li>
<li>The <tt>cnf.jwk</tt> claim matches the public key the browser generated</li>
</ul></li>
<li><t>The browser creates a KB-JWT per <eref target="#kb-creation-detail">KB-JWT Creation</eref>, binding the EVT to the RP's origin and session nonce.</t>
</li>
<li><t>The browser concatenates the EVT and KB-JWT to form the EVT+KB.</t>
</li>
</ol>
<t>Example EVT+KB (line breaks for display):</t>

<artwork><![CDATA[eyJhbGciOiJFZDI1NTE5Iiwia2lkIjoiMjAyNC0wOC0xOSIsInR5cCI6ImV2dCtqd3QifQ.
eyJpc3MiOiJodHRwczovL2lzc3Vlci5leGFtcGxlIiwiaWF0IjoxNzI0MDgzMjAw...}.
signature~
eyJhbGciOiJFZDI1NTE5IiwidHlwIjoia2Irand0In0.
eyJhdWQiOiJodHRwczovL3JwLmV4YW1wbGUiLCJub25jZSI6IjI1OWM1ZWFlLTQ4...}.
signature
]]>
</artwork>
</section>

<section anchor="token-presentation"><name>Token Presentation</name>
<t>The browser provides the EVT+KB to the RP. This mechanism is being defined by the W3C (<xref target="EVP-Browser"/>).</t>
</section>

<section anchor="token-verification"><name>Token Verification</name>
<t>The RP receives the EVT+KB and verifies it by:</t>

<ol spacing="compact">
<li>Verifying the KB-JWT per <eref target="#kb-verification">KB-JWT Verification</eref></li>
<li>Verifying the EVT per <eref target="#evt-verification">EVT Verification</eref></li>
<li>Verifying the KB-JWT signature using the public key from the EVT's <tt>cnf.jwk</tt> claim</li>
</ol>
<t>If all verification steps pass, the RP has successfully verified that the user controls the email address in the <tt>email</tt> claim.</t>
</section>
</section>

<section anchor="issuer-discovery"><name>Issuer Discovery</name>
<t>Both the browser and the RP need to discover information about the issuer for a given email address. This section describes the discovery process.</t>
<blockquote><t>Note: The W3C Email Verification API (<xref target="EVP-Browser"/>) uses FedCM's well-known configuration (<tt>/.well-known/fedcm.json</tt>) for issuer discovery. This specification uses <tt>/.well-known/email-verification</tt>. These two specifications will align on a single well-known file.</t>
</blockquote>
<section anchor="dns-delegation"><name>DNS Delegation</name>
<t>The email domain delegates email verification to an issuer via a DNS TXT record. Given an email address, parse the email domain (<tt>EMAIL_DOMAIN) and look up the `TXT` record for `_email-verification.</tt>EMAIL_DOMAIN<tt>. The contents of the record MUST start with</tt>iss=<tt>followed by the issuer host name. There MUST be only one</tt>TXT<tt>record for</tt>_email-verification.$EMAIL_DOMAIN`.</t>
<t>Example record:</t>

<sourcecode type="bash"><![CDATA[_email-verification.email-domain.example   TXT   iss=issuer.example
]]>
</sourcecode>
<t>This record states that <tt>email-domain.example</tt> has delegated email verification to the issuer at the host <tt>issuer.example</tt>.</t>
<t>If the email domain and the issuer are the same domain, then the record would be:</t>

<sourcecode type="bash"><![CDATA[_email-verification.issuer.example   TXT   iss=issuer.example
]]>
</sourcecode>
<blockquote><t>Access to DNS records and email is often independent of website deployments. This provides assurance that an issuer is truly authorized as an insider with only access to websites on <tt>issuer.example</tt> could not setup an issuer that would grant them verified emails for any email at <tt>issuer.example</tt>.</t>
</blockquote></section>

<section anchor="issuer-identifier"><name>Issuer Identifier</name>
<t>The record carries a host name. The <strong>issuer identifier</strong> is the HTTPS origin of that host: the string <tt>https://</tt> followed by the host, with no port, no path, and no trailing slash. For the record above, the issuer identifier is <tt>https://issuer.example</tt>.</t>
<t>The identifier is an origin rather than a bare host so that it names the same thing as the <tt>iss</tt> claim of <xref target="OpenID.Core"/> and the issuer of <xref target="RFC8414"/>, and so that it is directly comparable to the values those specifications define. A bare host would have to be widened to an origin at every comparison, which is where mismatches arise.</t>
<t>Every use of the issuer identity in this specification — the <tt>iss</tt> claim of the EVT, the <tt>issuer</tt> member of the metadata document, and the base of the well-known URL — is this origin. Comparisons between them are byte-for-byte on the derived string; no scheme defaulting, port normalization, or trailing-slash tolerance is applied.</t>
</section>

<section anchor="issuer-metadata"><name>Issuer Metadata</name>
<t>Once the issuer identifier is known, fetch the metadata document from <tt>$ISSUER/.well-known/email-verification</tt>, where <tt>$ISSUER</tt> is the issuer identifier defined above. For <tt>https://issuer.example</tt> this is <tt>https://issuer.example/.well-known/email-verification</tt>.</t>
<t>The metadata document is JSON containing the following properties:</t>

<ul spacing="compact">
<li><em>issuer</em> - the issuer identifier. The value MUST be identical to the issuer identifier the document was fetched under, and a party that fetches the document MUST reject it if it is not. This is the check of <xref target="RFC8414"/>, Section 3.3, and it prevents a document served at one identity from claiming another.</li>
<li><em>issuance_endpoint</em> - the API endpoint the browser calls to obtain an EVT</li>
<li><em>jwks_uri</em> - the URL where the issuer provides its public keys to verify the EVT</li>
<li><em>signing_alg_values_supported</em> - OPTIONAL. JSON array containing a list of the signing algorithms (<tt>alg</tt> values) supported by the issuer for both HTTP Message Signatures and issued EVTs. Algorithm identifiers MUST be from the IANA "JSON Web Signature and Encryption Algorithms" registry, and MUST be fully specified per <xref target="RFC9864"/>: the polymorphic <tt>EdDSA</tt> identifier MUST NOT be used, and <tt>Ed25519</tt> or <tt>Ed448</tt> used instead. <tt>none</tt> and the symmetric MAC identifiers MUST NOT be used. For the HTTP Message Signature case these are the algorithms an issuer would state in <tt>Accept-Signature-Alg</tt> (<xref target="I-D.hardt-httpbis-signature-key"/>), and that document's requirements on the conveyed key apply. If omitted, <tt>Ed25519</tt> is the default. <tt>Ed25519</tt> SHOULD be included in the supported algorithms list.</li>
<li><em>private_email_supported</em> - OPTIONAL. Boolean indicating whether the issuer supports generating private email addresses. Defaults to <tt>false</tt>.</li>
</ul>
<t>Following is an example <tt>.well-known/email-verification</tt> file:</t>

<sourcecode type="json"><![CDATA[{
  "issuer": "https://issuer.example",
  "issuance_endpoint": "https://accounts.issuer.example/email-verification/issuance",
  "jwks_uri": "https://accounts.issuer.example/email-verification/jwks",
  "signing_alg_values_supported": ["Ed25519", "ES256"],
  "private_email_supported": true
}
]]>
</sourcecode>
</section>
</section>

<section anchor="http-signatures"><name>HTTP Message Signatures</name>
<t>This section defines how HTTP Message Signatures (<xref target="RFC9421"/>) are used in token requests. The browser signs requests to prove possession of a key pair, and the issuer verifies these signatures. The browser's public key is conveyed with the <tt>hwk</tt> scheme of <xref target="I-D.hardt-httpbis-signature-key"/>; that document's requirements apply in full, and this section states how they are met here.</t>

<section anchor="request-signing"><name>HTTP Request Signing</name>
<t>The browser creates a signed request by:</t>

<ol spacing="compact">
<li>Creating a JSON request body with the email address and optional parameters</li>
<li>Creating the <tt>Content-Digest</tt> header (<xref target="RFC9530"/>) over the request body</li>
<li>Creating the <tt>Signature-Key</tt> header using the <tt>hwk</tt> scheme (<xref target="I-D.hardt-httpbis-signature-key"/>) with the browser's public key</li>
<li>Creating the <tt>Signature-Input</tt> header specifying the covered components</li>
<li>Computing the signature base per <xref target="RFC9421"/> Section 2.5 and signing with the browser's private key</li>
<li>Creating the <tt>Signature</tt> header with the base64-encoded signature</li>
</ol>

<section anchor="request-body"><name>Request Body</name>
<t>The request body is a JSON object with the following fields:</t>

<ul spacing="compact">
<li><tt>email</tt> (REQUIRED): The email address to verify, byte-for-byte as the user selected it</li>
<li><tt>private_email</tt> (OPTIONAL): Request a new private email address. See <eref target="#private-email">Private Email Addresses</eref>.</li>
<li><tt>directed_email</tt> (OPTIONAL): A previously issued private email address to reuse. See <eref target="#private-email">Private Email Addresses</eref>.</li>
</ul>
<t>Example:</t>

<sourcecode type="json"><![CDATA[{
  "email": "user@example.com"
}
]]>
</sourcecode>
</section>

<section anchor="content-digest-header"><name>Content-Digest Header</name>
<t>The browser MUST include a <tt>Content-Digest</tt> header (<xref target="RFC9530"/>) carrying a digest of the request body. The <tt>sha-256</tt> algorithm MUST be supported and SHOULD be used.</t>

<artwork><![CDATA[Content-Digest: \
    sha-256=:p8W2nSiyrdmtuSb49YHusp+pXM3er/ZiwRThsUgvua8=:
]]>
</artwork>
<t>The email address being verified is carried in the request body, not in any covered component of the URL. Without a digest of that body among the covered components, the signature says nothing about which address was requested, and an intermediary or a party that captures the request can substitute a different one within the <tt>created</tt> window while the signature still verifies. Covering <tt>content-digest</tt> binds the address to the signature, and so to the key that appears in the EVT's <tt>cnf</tt> claim.</t>
</section>

<section anchor="signature-key-header"><name>Signature-Key Header</name>
<t>The <tt>Signature-Key</tt> header uses the <tt>hwk</tt> scheme to convey the browser's public key. The scheme is defined by <xref target="I-D.hardt-httpbis-signature-key"/>, whose parameter and algorithm requirements apply unchanged; the key's <tt>alg</tt> is the fully-specified identifier the browser selected per <eref target="#token-request">Token Request</eref>.</t>

<artwork><![CDATA[Signature-Key: sig=hwk;kty="OKP";crv="Ed25519"; \
    x="JrQLj5P_89iXES9-vFgrIy29clF9CC_oPPsw3c5D0bs";alg="Ed25519"
]]>
</artwork>
</section>

<section anchor="signature-input-header"><name>Signature-Input Header</name>
<t>The covered components MUST include <tt>@method</tt>, <tt>@authority</tt>, <tt>@path</tt>, <tt>content-digest</tt>, and <tt>signature-key</tt>. The <tt>created</tt> parameter MUST be included.</t>
<t>Covering the <tt>cookie</tt> component is RECOMMENDED when the Cookie header is available to the signer at signing time; the component MUST be omitted when the header is not present (per <xref target="RFC9421"/> Section 2.5). An issuer MUST NOT reject a request solely because <tt>cookie</tt> is not covered. See <eref target="#http-message-signature-security">HTTP Message Signature Security</eref> for what coverage provides.</t>

<artwork><![CDATA[Signature-Input: sig=("@method" "@authority" "@path" \
    "content-digest" "signature-key");created=1692345600
]]>
</artwork>
</section>

<section anchor="example-signed-request"><name>Example Signed Request</name>

<sourcecode type="http"><![CDATA[POST /email-verification/issuance HTTP/1.1
Host: accounts.issuer.example
Cookie: session=...
Content-Type: application/json
Sec-Fetch-Dest: email-verification
Content-Digest: \
    sha-256=:p8W2nSiyrdmtuSb49YHusp+pXM3er/ZiwRThsUgvua8=:
Signature-Input: sig=("@method" "@authority" "@path" \
    "content-digest" "signature-key");created=1692345600
Signature: sig=:MEQCIHd8Y8qYKm5e3dV8y....:
Signature-Key: sig=hwk;kty="OKP";crv="Ed25519"; \
    x="JrQLj5P_89iXES9-vFgrIy29clF9CC_oPPsw3c5D0bs";alg="Ed25519"

{"email":"user@example.com"}
]]>
</sourcecode>
</section>
</section>

<section anchor="request-verification"><name>HTTP Request Verification</name>
<t>The issuer MUST verify the request headers:</t>

<ul spacing="compact">
<li><tt>Content-Type</tt> is <tt>application/json</tt></li>
<li><tt>Sec-Fetch-Dest</tt> is <tt>email-verification</tt></li>
<li><tt>Content-Digest</tt> is present and names a digest algorithm the issuer supports</li>
<li><tt>Signature-Input</tt> is present</li>
<li><tt>Signature</tt> is present</li>
<li><tt>Signature-Key</tt> is present with <tt>sig=hwk</tt> scheme</li>
</ul>
<t>The issuer MUST verify the HTTP Message Signature per <xref target="RFC9421"/> and <xref target="I-D.hardt-httpbis-signature-key"/>. Resolving the key from the <tt>Signature-Key</tt> header, determining the signature algorithm, validating the key, and reporting failure through the <tt>Signature-Error</tt> header are defined by <xref target="I-D.hardt-httpbis-signature-key"/> and are not restated here.</t>
<t>This specification profiles that processing as follows:</t>

<ol spacing="compact">
<li>The <tt>hwk</tt> scheme is the only scheme this specification defines a use for. An issuer need implement no other, and rejects a request presenting one with <tt>unsupported_scheme</tt>.</li>
<li>The signature MUST cover at minimum <tt>@method</tt>, <tt>@authority</tt>, <tt>@path</tt>, <tt>content-digest</tt>, and <tt>signature-key</tt>. The issuer MUST reject a request whose signature does not. The <tt>cookie</tt> component is optional; an issuer MUST NOT reject a request solely because <tt>cookie</tt> is not covered, and MUST verify the signature over whatever covered components the request does list.</li>
<li>The issuer MUST recompute the digest of the received body and reject the request if it does not match the <tt>Content-Digest</tt> header, per <xref target="RFC9530"/>, Section 2. The issuer MUST perform this check before acting on the body. Verifying the signature over <tt>content-digest</tt> establishes only that the header was signed; it is the recomputation that ties the header to the bytes received.</li>
<li>The issuer MUST reject a request whose <tt>created</tt> timestamp is more than 300 seconds from the current time. The window allows for client clock skew, following the guidance of <xref target="RFC8725"/> that leeway should be a few minutes at most.</li>
<li>The algorithms the issuer accepts are the values of <tt>signing_alg_values_supported</tt> in its metadata (see <eref target="#issuer-metadata">Issuer Metadata</eref>), which serves the role <tt>Accept-Signature-Alg</tt> serves in <xref target="I-D.hardt-httpbis-signature-key"/>. An issuer that publishes the member SHOULD also send <tt>Accept-Signature-Alg</tt> on an <tt>unsupported_algorithm</tt> response, so a client that did not read the metadata learns the same set.</li>
</ol>
<t>See <eref target="#signature-errors">Signature Errors</eref> for how the issuer reports a failure alongside this specification's JSON error body.</t>
<t>The issuer MUST verify the request body:</t>

<ol spacing="compact">
<li>Parsing the JSON body and extracting the <tt>email</tt> field</li>
<li>Verifying the <tt>email</tt> field is a valid email address as defined below</li>
</ol>
</section>

<section anchor="valid-email-address"><name>Valid Email Address</name>
<t>An email address is valid for the purposes of this specification if it matches the "valid e-mail address" ABNF production of <xref target="WHATWG.HTML"/>, Section 4.10.5.1.5. The same definition applies to the <tt>email</tt> field of the token request, to the <tt>email</tt> claim of the EVT, and to a private email address issued under <eref target="#private-email">Private Email Addresses</eref>.</t>
<t><xref target="RFC5322"/> defines a broader grammar than this. It admits comments, folding whitespace, and quoted local parts that no provider issues in practice and that no browser will accept from a user, and parsers that implement it fully disagree with each other at the edges. The <xref target="WHATWG.HTML"/> production is a deliberate narrowing of <xref target="RFC5322"/> chosen for exactly this problem, and it is already what the browser applies to the address before the protocol begins, since the user supplies it through a control validated against that production. Adopting it here makes the issuer's check and the browser's the same check.</t>
<t>The production is willfully non-compliant with <xref target="RFC5322"/> and this specification adopts that non-compliance knowingly. An issuer whose users hold addresses outside it cannot verify them through this protocol.</t>
</section>
</section>

<section anchor="evt"><name>Email Verification Token (EVT)</name>
<t>The Email Verification Token (EVT) is a JWT issued by the issuer that contains a verified email address and the browser's public key. This section defines the EVT structure and how it is created and verified.</t>

<section anchor="evt-structure"><name>EVT Structure</name>
<t>The EVT is a JWT with the following structure:</t>

<section anchor="header"><name>Header</name>

<ul spacing="compact">
<li><tt>alg</tt> (REQUIRED): Signing algorithm. MUST be a fully-specified identifier from the IANA "JSON Web Signature and Encryption Algorithms" registry. The polymorphic <tt>EdDSA</tt> identifier MUST NOT be used; use <tt>Ed25519</tt> or <tt>Ed448</tt>.</li>
<li><tt>kid</tt> (REQUIRED): Key identifier of the key used to sign</li>
<li><tt>typ</tt> (REQUIRED): Set to "evt+jwt"</li>
</ul>
<t>Example:</t>

<sourcecode type="json"><![CDATA[{
  "alg": "Ed25519",
  "kid": "2024-08-19",
  "typ": "evt+jwt"
}
]]>
</sourcecode>
</section>

<section anchor="payload"><name>Payload</name>
<t>Required claims:</t>

<ul spacing="compact">
<li><tt>iss</tt>: The issuer identifier, an HTTPS origin (see <eref target="#issuer-identifier">Issuer Identifier</eref>)</li>
<li><tt>iat</tt>: Issued at time (seconds since epoch)</li>
<li><tt>cnf</tt>: Confirmation claim containing the browser's public key in <tt>jwk</tt> format (for SD-JWT Key Binding compatibility). The <tt>jwk</tt> is the key conveyed in the <tt>Signature-Key</tt> header of the token request, reproduced with the same members, including its fully-specified <tt>alg</tt>.</li>
<li><tt>email</tt>: The verified email address, byte-for-byte as it appeared in the token request</li>
<li><tt>email_verified</tt>: Boolean, MUST be <tt>true</tt></li>
</ul>
<t>Optional claims:</t>

<ul spacing="compact">
<li><tt>is_private_email</tt>: Boolean, set to <tt>true</tt> when the email is a private address</li>
</ul>
<t>Example:</t>

<sourcecode type="json"><![CDATA[{
  "iss": "https://issuer.example",
  "iat": 1724083200,
  "cnf": {
    "jwk": {
      "kty": "OKP",
      "crv": "Ed25519",
      "x": "JrQLj5P_89iXES9-vFgrIy29clF9CC_oPPsw3c5D0bs",
      "alg": "Ed25519"
    }
  },
  "email": "user@example.com",
  "email_verified": true
}
]]>
</sourcecode>
</section>

<section anchor="format"><name>Format</name>
<t>The EVT has a <tt>~</tt> appended to it for SD-JWT compatibility (see <eref target="#sd-jwt-compatibility">SD-JWT Compatibility</eref>).</t>
</section>
</section>

<section anchor="evt-creation"><name>EVT Creation</name>
<t>After verifying the request (see <eref target="#request-verification">Request Verification</eref>) and authenticating the user, the issuer creates the EVT:</t>

<ol spacing="compact">
<li>Construct the header with <tt>alg</tt>, <tt>kid</tt>, and <tt>typ</tt></li>
<li>Construct the payload with <tt>iss</tt>, <tt>iat</tt>, <tt>cnf</tt> (containing the public key from the <tt>Signature-Key</tt> header, including its <tt>alg</tt> member), <tt>email</tt>, and <tt>email_verified</tt></li>
<li>If a private email is requested, include <tt>is_private_email: true</tt> and set <tt>email</tt> to the private address</li>
<li>Sign the JWT with the issuer's private key corresponding to the <tt>kid</tt></li>
<li>Append <tt>~</tt> to the signed JWT</li>
</ol>
<blockquote><t>Note: The <tt>is_private_email</tt> claim name matches Apple's Sign in with Apple for compatibility with existing RP implementations.</t>
</blockquote></section>

<section anchor="evt-verification"><name>EVT Verification</name>
<t>Both the browser and RP verify the EVT. The verification steps are:</t>

<ol spacing="compact">
<li>Parse the EVT into header, payload, and signature components</li>
<li>Extract and validate the <tt>alg</tt> and <tt>kid</tt> from the header. Reject an <tt>alg</tt> that is absent, polymorphic, <tt>none</tt>, or a symmetric MAC identifier.</li>
<li>Extract and validate the <tt>iss</tt>, <tt>iat</tt>, <tt>cnf</tt>, <tt>email</tt>, and <tt>email_verified</tt> claims from the payload</li>
<li>Perform <eref target="#issuer-discovery">Issuer Discovery</eref> for the email domain to derive the issuer identifier, and verify the <tt>iss</tt> claim is byte-for-byte identical to it</li>
<li>Fetch the issuer's public keys from the <tt>jwks_uri</tt> in the issuer metadata</li>
<li>Verify the EVT signature using the public key identified by <tt>kid</tt></li>
<li>Verify <tt>iat</tt> is within an acceptable time window</li>
<li>Verify <tt>email_verified</tt> is <tt>true</tt></li>
</ol>
<t>The browser additionally verifies:</t>

<ul spacing="compact">
<li>The <tt>email</tt> claim is byte-for-byte identical to the email address being verified</li>
<li>The <tt>cnf.jwk</tt> claim matches the public key the browser generated</li>
</ul>
</section>
</section>

<section anchor="kb"><name>Key Binding (EVT+KB)</name>
<t>Key Binding ties an EVT to a specific RP and session through a Key Binding JWT (KB-JWT). The combined EVT+KB is what the RP receives and verifies.</t>

<section anchor="kb-structure"><name>KB-JWT Structure</name>
<t>The KB-JWT is a JWT with the following structure:</t>

<section anchor="header-1"><name>Header</name>

<ul spacing="compact">
<li><tt>alg</tt> (REQUIRED): Signing algorithm. MUST be the fully-specified identifier carried in the <tt>alg</tt> member of the EVT's <tt>cnf.jwk</tt>, which is the same key the browser used to sign the token request.</li>
<li><tt>typ</tt> (REQUIRED): Set to "kb+jwt" for SD-JWT library compatibility</li>
</ul>
<t>Example:</t>

<sourcecode type="json"><![CDATA[{
  "alg": "Ed25519",
  "typ": "kb+jwt"
}
]]>
</sourcecode>
</section>

<section anchor="payload-1"><name>Payload</name>

<ul spacing="compact">
<li><tt>aud</tt> (REQUIRED): The RP's origin</li>
<li><tt>nonce</tt> (REQUIRED): The nonce from the RP's session</li>
<li><tt>iat</tt> (REQUIRED): Issued at time</li>
<li><tt>sd_hash</tt> (REQUIRED): SHA-256 hash of the EVT for SD-JWT library compatibility</li>
</ul>
<t>Example:</t>

<sourcecode type="json"><![CDATA[{
  "aud": "https://rp.example",
  "nonce": "259c5eae-486d-4b0f-b666-2a5b5ce1c925",
  "iat": 1724083260,
  "sd_hash": "X9yH0Ajrdm1Oij4tWso9UzzKJvPoDxwmuEcO3XAdRC0"
}
]]>
</sourcecode>
</section>
</section>

<section anchor="evt-kb-format"><name>EVT+KB Format</name>
<t>The EVT+KB is formed by concatenating the EVT and KB-JWT separated by a tilde:</t>

<artwork><![CDATA[<EVT>~<KB-JWT>
]]>
</artwork>
<t>The EVT already has a trailing <tt>~</tt> from its SD-JWT format, so the full structure is:</t>

<artwork><![CDATA[<JWT>~<KB-JWT>
]]>
</artwork>
</section>

<section anchor="sd-jwt-compatibility"><name>SD-JWT Compatibility</name>
<t>The EVT+KB format is compatible with SD-JWT with Key Binding as specified in <xref target="I-D.ietf-oauth-selective-disclosure-jwt"/>, though this protocol does not use selective disclosure features. The following SD-JWT features are used:</t>

<ul spacing="compact">
<li><strong>Trailing <tt>~</tt> on EVT</strong>: The EVT uses the SD-JWT format (JWT with <tt>~</tt> suffix)</li>
<li><strong><tt>cnf</tt> claim</strong>: The EVT includes the <tt>cnf</tt> claim with <tt>jwk</tt> for holder key binding</li>
<li><strong><tt>typ: "kb+jwt"</tt></strong>: The KB-JWT uses the SD-JWT Key Binding JWT type</li>
<li><strong><tt>sd_hash</tt> claim</strong>: The KB-JWT includes the SD-JWT hash of the EVT</li>
<li><strong>Concatenation format</strong>: The EVT+KB uses the SD-JWT <tt>&lt;Issuer-signed-JWT&gt;~&lt;KB-JWT&gt;</tt> format</li>
</ul>
<t>Standard SD-JWT libraries can be used to parse and validate EVT+KB tokens.</t>
</section>

<section anchor="kb-creation-detail"><name>KB-JWT Creation</name>
<t>After verifying the EVT (see <eref target="#evt-verification">EVT Verification</eref>), the browser creates the KB-JWT:</t>

<ol spacing="compact">
<li>Construct the header with <tt>alg</tt> and <tt>typ</tt></li>
<li><t>Construct the payload with:</t>

<ul spacing="compact">
<li><tt>aud</tt>: The RP's origin</li>
<li><tt>nonce</tt>: The nonce from the RP's session</li>
<li><tt>iat</tt>: Current time</li>
<li><tt>sd_hash</tt>: SHA-256 hash of the EVT (including the trailing <tt>~</tt>)</li>
</ul></li>
<li>Sign the KB-JWT with the browser's private key</li>
<li>Concatenate with the EVT to form the EVT+KB</li>
</ol>
</section>

<section anchor="kb-verification"><name>KB-JWT Verification</name>
<t>The RP verifies the KB-JWT by:</t>

<ol spacing="compact">
<li>Parse the EVT+KB by separating at the tilde</li>
<li>Parse the KB-JWT into header, payload, and signature</li>
<li>Extract <tt>alg</tt> from the header and <tt>aud</tt>, <tt>nonce</tt>, <tt>iat</tt>, <tt>sd_hash</tt> from the payload</li>
<li>Verify <tt>alg</tt> matches the <tt>alg</tt> member of the EVT's <tt>cnf.jwk</tt>, and reject the KB-JWT if it does not</li>
<li>Verify <tt>aud</tt> matches the RP's origin</li>
<li>Verify <tt>nonce</tt> matches the nonce from the RP's session</li>
<li>Verify <tt>iat</tt> is within a reasonable time window</li>
<li>Compute the SHA-256 hash of the EVT and verify it matches <tt>sd_hash</tt></li>
<li>Verify the KB-JWT signature using the public key from the EVT's <tt>cnf.jwk</tt> claim, under the algorithm named by that key's <tt>alg</tt> member</li>
</ol>
</section>
</section>

<section anchor="private-email"><name>Private Email Addresses</name>
<t>Private email addresses allow users to provide site-specific email addresses to RPs, preventing RP-to-RP correlation of users by email address. A private email address can be:</t>

<ul spacing="compact">
<li><strong>Single-use</strong>: The browser requests a new private email and does not store it</li>
<li><strong>Reusable</strong>: The browser stores the private email and passes it back via <tt>directed_email</tt> for account continuity</li>
</ul>
<t>The choice between single-use and reusable is made by the browser or user, not the issuer. The first request to an RP always uses <tt>private_email: true</tt> to obtain a new private email address. For subsequent requests, the browser can either request another new private email or reuse an existing one by passing it in <tt>directed_email</tt>.</t>

<section anchor="request-parameters"><name>Request Parameters</name>
<t>The token request body supports one of the following parameters for private email addresses (mutually exclusive):</t>

<ul>
<li><t><tt>private_email</tt> (OPTIONAL): Boolean. When set to <tt>true</tt>, requests a new private email address instead of the user's actual email.</t>
</li>
<li><t><tt>directed_email</tt> (OPTIONAL): String. A previously issued private email address. When provided, the issuer returns the same private email address if it is valid and linked to the <tt>email</tt> in the request.</t>
</li>
</ul>
</section>

<section anchor="example-requests"><name>Example Requests</name>
<t>Request for a new private email address:</t>

<sourcecode type="json"><![CDATA[{
  "email": "user@example.com",
  "private_email": true
}
]]>
</sourcecode>
<t>Request to reuse a previously issued private email address:</t>

<sourcecode type="json"><![CDATA[{
  "email": "user@example.com",
  "directed_email": "u7x9k2m4@privaterelay.example"
}
]]>
</sourcecode>
</section>

<section anchor="requirements"><name>Requirements</name>

<ul spacing="compact">
<li>The private email MUST be a valid email address that the issuer can route to the user's actual mailbox</li>
<li>The private email SHOULD be unique per user and per RP origin (derived from the browser's context)</li>
<li>If <tt>directed_email</tt> is provided and is linked to the <tt>email</tt> address in the request, the issuer MUST return the same private email address</li>
<li>If <tt>directed_email</tt> is provided but is invalid or not linked to the <tt>email</tt>, the issuer MUST return an error</li>
<li>The private email address is included in the EVT <tt>email</tt> claim</li>
<li>The EVT MUST include <tt>is_private_email: true</tt> when a private email address is issued</li>
</ul>
</section>

<section anchor="issuer-flexibility"><name>Issuer Flexibility</name>
<t>The domain of the private email address does not need to match the domain of the user's actual email address. Additionally, the <tt>iss</tt> claim in the EVT is the issuer identifier for the private email domain, which may differ from the issuer the browser initially contacted.</t>
<t>For example, a user with <tt>user@example.com</tt> may receive a private email address <tt>u7x9k2m4@privaterelay.different.example</tt>. The EVT's <tt>iss</tt> claim would be the issuer identifier derived for <tt>privaterelay.different.example</tt>. The browser verifies the EVT by performing issuer discovery on the private email domain and validating the signature against that issuer's JWKS. This allows email providers to delegate private email functionality to a separate service. It also enables privacy for users with vanity domains (e.g., <tt>me@dickhardt.example</tt>) where the domain itself is a unique identifier that would otherwise reveal the user's identity.</t>
</section>

<section anchor="example-evt-payload"><name>Example EVT Payload</name>
<t>When a private email is issued, the EVT contains the private address in the <tt>email</tt> claim and includes <tt>is_private_email: true</tt>:</t>

<sourcecode type="json"><![CDATA[{
  "iss": "https://privaterelay.different.example",
  "iat": 1724083200,
  "cnf": {
    "jwk": {
      "kty": "OKP",
      "crv": "Ed25519",
      "x": "JrQLj5P_89iXES9-vFgrIy29clF9CC_oPPsw3c5D0bs",
      "alg": "Ed25519"
    }
  },
  "email": "u7x9k2m4@privaterelay.different.example",
  "email_verified": true,
  "is_private_email": true
}
]]>
</sourcecode>
<t>The browser MAY store the private email address so it can provide it as <tt>directed_email</tt> in future requests if the user wants to reuse the same private email address at an RP. This is analogous to how browsers store usernames and passwords for sites.</t>
<t>See <eref target="#privacy-considerations">Privacy Considerations</eref> for privacy analysis of private email addresses.</t>
</section>
</section>

<section anchor="error-responses"><name>Error Responses</name>
<t>If the issuer cannot process the token request successfully, it MUST return an appropriate HTTP status code with a JSON error response containing an <tt>error</tt> field and optionally an <tt>error_description</tt> field.</t>

<section anchor="invalid-content-type-header"><name>Invalid Content-Type Header</name>
<t>When the request does not include the required <tt>Content-Type: application/json</tt> header, the server MUST return the 415 HTTP response code.</t>
</section>

<section anchor="invalid-sec-fetch-dest-header"><name>Invalid Sec-Fetch-Dest Header</name>
<t>When the request does not include the required <tt>Sec-Fetch-Dest: email-verification</tt> header:</t>
<t><strong>HTTP 400 Bad Request</strong></t>

<sourcecode type="json"><![CDATA[{
  "error": "invalid_request",
  "error_description": "Missing or invalid Sec-Fetch-Dest header"
}
]]>
</sourcecode>
<t>The <tt>error_description</tt> SHOULD specify that the Sec-Fetch-Dest header is missing or invalid.</t>
</section>

<section anchor="signature-errors"><name>Invalid or Missing HTTP Message Signature</name>
<t>When the HTTP Message Signature is missing, malformed, or verification fails, the issuer MUST return the <tt>error</tt> field <tt>invalid_signature</tt> in the JSON body, and SHOULD include the <tt>Signature-Error</tt> response header of <xref target="I-D.hardt-httpbis-signature-key"/> naming the specific error code. The header is the machine-readable carrier; the JSON body remains for clients that do not process it.</t>
<t><strong>HTTP 400 Bad Request</strong></t>

<sourcecode type="http"><![CDATA[HTTP/1.1 400 Bad Request
Content-Type: application/json
Signature-Error: error=invalid_signature

{
  "error": "invalid_signature",
  "error_description": "HTTP Message Signature verification failed"
}
]]>
</sourcecode>
<t>This covers every signature-related failure defined by <xref target="I-D.hardt-httpbis-signature-key"/> — an unresolvable or invalid key, an unsupported algorithm or scheme, a missing covered component, an expired <tt>created</tt> timestamp, and a signature that does not verify — and a <tt>Content-Digest</tt> that does not match the received body. The body's <tt>error</tt> field is <tt>invalid_signature</tt> in all of them; the <tt>Signature-Error</tt> header carries which one it was, and the accompanying headers that document defines say what would have worked:</t>

<sourcecode type="http"><![CDATA[HTTP/1.1 400 Bad Request
Content-Type: application/json
Signature-Error: error=unsupported_algorithm
Accept-Signature-Alg: Ed25519, ES256

{
  "error": "invalid_signature",
  "error_description": "Unsupported signature algorithm"
}
]]>
</sourcecode>
<t>The two carry the same failure at different granularities, so an issuer MUST NOT return a <tt>Signature-Error</tt> header naming a failure the body contradicts.</t>
</section>

<section anchor="authentication-required"><name>Authentication Required</name>
<t>When the request lacks valid authentication cookies, contains expired/invalid cookies, or the authenticated user does not have control of the requested email address:</t>
<t><strong>HTTP 401 Unauthorized</strong></t>

<sourcecode type="json"><![CDATA[{
  "error": "authentication_required",
  "error_description": "User must be authenticated and have control of the requested email address"
}
]]>
</sourcecode>
</section>

<section anchor="invalid-parameters"><name>Invalid Parameters</name>
<t>When the request body is malformed, missing the <tt>email</tt> field, or contains invalid values:</t>
<t><strong>HTTP 400 Bad Request</strong></t>

<sourcecode type="json"><![CDATA[{
  "error": "invalid_request",
  "error_description": "Invalid or malformed request body"
}
]]>
</sourcecode>
</section>

<section anchor="private-email-not-supported"><name>Private Email Not Supported</name>
<t>When the request includes <tt>private_email</tt> or <tt>directed_email</tt> but the issuer does not support private email addresses (<tt>private_email_supported</tt> is <tt>false</tt> or absent in metadata):</t>
<t><strong>HTTP 400 Bad Request</strong></t>

<sourcecode type="json"><![CDATA[{
  "error": "private_email_not_supported",
  "error_description": "This issuer does not support private email addresses"
}
]]>
</sourcecode>
</section>

<section anchor="invalid-directed-email"><name>Invalid Directed Email</name>
<t>When the request includes <tt>directed_email</tt> but the private email address is invalid or not linked to the <tt>email</tt> address in the request:</t>
<t><strong>HTTP 400 Bad Request</strong></t>

<sourcecode type="json"><![CDATA[{
  "error": "invalid_directed_email",
  "error_description": "The directed_email is invalid or not linked to this email address"
}
]]>
</sourcecode>
</section>

<section anchor="server-errors"><name>Server Errors</name>
<t>For internal server errors or temporary unavailability:</t>
<t><strong>HTTP 500 Internal Server Error</strong></t>

<sourcecode type="json"><![CDATA[{
  "error": "server_error",
  "error_description": "Temporary server error, please try again later"
}
]]>
</sourcecode>
</section>
</section>

<section anchor="privacy-considerations"><name>Privacy Considerations</name>
<t>This section analyzes the privacy properties of the Email Verification Protocol, following the guidance in <xref target="RFC6973"/>.</t>

<section anchor="reduced-friction-tradeoff"><name>Reduced Friction Tradeoff</name>
<t>By reducing friction in email verification, EVP makes it easier for users to provide their email address to more sites. This convenience could accelerate the RP correlation problem—users may share a correlatable identifier with more RPs than they would if verification required more effort.</t>
<t>EVP addresses this tradeoff through private email addresses. When supported by the issuer, users can present a site-specific private email that cannot be correlated across RPs. This makes sharing a non-correlatable identifier just as easy as sharing the user's real email address, giving users a privacy-preserving option without additional friction.</t>
</section>

<section anchor="timing-correlation-by-email-providers"><name>Timing Correlation by Email Providers</name>
<t>The three-party model (see <eref target="#protocol-flow">Protocol Flow</eref>) prevents the issuer from learning which RP requested verification. When the RP uses the email only for identification and does not send emails, the email provider never learns about the RP at all. When the RP does send emails, the provider eventually learns about that RP, but only when email is actually sent—not at verification time. This dulls timing correlation.</t>
</section>

<section anchor="rp-correlation-via-email-addresses"><name>RP Correlation via Email Addresses</name>
<t>Private email addresses prevent RPs from correlating users across sites. Additional benefits:</t>
<t><strong>Protection from data breaches</strong>: If an RP suffers a data breach, only the private email is exposed—not the user's primary email address.</t>
<t><strong>Protection from unwanted email</strong>: Because the issuer controls private email routing, users can revoke or filter mail to specific addresses without affecting their primary inbox.</t>
</section>

<section anchor="issuer-knowledge"><name>Issuer Knowledge</name>
<t>The issuer learns certain information through the protocol:</t>

<ol>
<li><t><strong>Email addresses</strong>: The issuer learns that the user controls the email address in the request. This may reveal email addresses at domains the issuer is authoritative for that it did not previously know the user had.</t>
</li>
<li><t><strong>Verification requests</strong>: The issuer sees that verification was requested but does not learn which RP requested it (maintained by the three-party model).</t>
</li>
<li><t><strong>Private email mappings</strong>: When generating private emails, the issuer stores mappings between private addresses and user email addresses for mail routing.</t>
</li>
<li><t><strong>Email traffic</strong>: When RPs send email to private addresses, the issuer (operating the relay) learns about those communications.</t>
</li>
</ol>
</section>

<section anchor="rp-knowledge"><name>RP Knowledge</name>
<t>The RP can infer whether the user is logged into the issuer: the RP receives an EVT when the user is logged in, and receives an error when the user is not. This is inherent to any authentication-based verification scheme.</t>
</section>

<section anchor="browser-storage"><name>Browser Storage</name>
<t>The browser MAY store the private email address per RP origin to enable account continuity by passing it as <tt>directed_email</tt> in future requests. This is analogous to how browsers store usernames and passwords for sites.</t>
</section>
</section>

<section anchor="security-considerations"><name>Security Considerations</name>

<section anchor="http-message-signature-security"><name>HTTP Message Signature Security</name>
<t>The use of HTTP Message Signatures (<xref target="RFC9421"/>) provides several security benefits:</t>

<ol>
<li><t><strong>Request Integrity</strong>: The signature covers the HTTP method, authority, path, and — through <tt>content-digest</tt> — the request body, preventing tampering with any of these components. The body is what carries the email address being verified, so without that coverage the signature would attest to a request without attesting to which address it asked for.</t>
</li>
<li><t><strong>Cookie Binding (when covered)</strong>: Covering the <tt>cookie</tt> component binds the authenticating cookie to the signed request. Coverage cannot be assumed — in some browser architectures the Cookie header is attached after the request is signed, so the value does not exist at signing time. Without it, the signature still binds the request to the browser's key, and the cookie still authenticates the user.</t>
</li>
<li><t><strong>Replay Protection</strong>: The <tt>created</tt> timestamp in the <tt>Signature-Input</tt> header is verified to be within 300 seconds, bounding the replay window. A replayed request yields an EVT bound to the original browser's key through the <tt>cnf</tt> claim, so replay gains an attacker no token another party can use.</t>
</li>
<li><t><strong>Public Key Binding</strong>: The browser's public key transmitted via the <tt>Signature-Key</tt> header with the <tt>hwk</tt> scheme is bound to the request signature, ensuring the issuer knows which public key to include in the EVT's <tt>cnf</tt> claim. Because <tt>signature-key</tt> is a covered component, an attacker cannot substitute the key or the scheme without invalidating the signature; see Signature-Key Integrity in <xref target="I-D.hardt-httpbis-signature-key"/>.</t>
</li>
</ol>
</section>

<section anchor="signature-key-hwk-scheme"><name>Signature-Key hwk Scheme</name>
<t>The <tt>hwk</tt> (Header Web Key) scheme provides:</t>

<ol>
<li><t><strong>Self-Contained Key Distribution</strong>: The public key is transmitted inline, eliminating the need for a separate key lookup or registration process.</t>
</li>
<li><t><strong>Pseudonymity</strong>: The browser does not need to identify itself - the key serves as a pseudonymous identifier for the request.</t>
</li>
<li><t><strong>Ephemeral Keys</strong>: The browser generates fresh key pairs for each verification flow, limiting the correlation potential across different verification attempts.</t>
</li>
</ol>
</section>

<section anchor="fully-specified-algorithms"><name>Fully-Specified Algorithms</name>
<t>Every algorithm identifier in this protocol is fully specified: it names the signature operation completely, including curve and hash where applicable. A key whose algorithm is not determined by its identifier invites downgrade and confused-verifier conditions, in which two parties disagree on the operation a signature represents. <xref target="RFC9864"/> gives the analysis and deprecates the polymorphic identifiers, of which <tt>EdDSA</tt> is the one this protocol would otherwise have used.</t>
<t>For the browser's request-signing key, <xref target="I-D.hardt-httpbis-signature-key"/> imposes this and states how the issuer enforces it. The EVT and the KB-JWT are outside that document's scope, so this specification imposes it on them directly: on the issuer's EVT signing key (see <eref target="#evt-structure">EVT Structure</eref>) and on the KB-JWT, whose <tt>alg</tt> is fixed by the <tt>cnf.jwk</tt> it is verified against (see <eref target="#kb-structure">KB-JWT Structure</eref>).</t>
</section>

<section anchor="dns-delegation-security"><name>DNS Delegation</name>
<t>The delegation from an email domain to an issuer is carried in an unauthenticated DNS TXT record (<eref target="#dns-delegation">DNS Delegation</eref>). An attacker who controls what a party sees for <tt>_email-verification.$EMAIL_DOMAIN</tt> controls which issuer that party believes is authoritative for the domain. This section states what that yields, and to whom.</t>
<t>Two parties resolve the record independently: the browser, to find the issuance endpoint, and the RP, to check the <tt>iss</tt> claim of the EVT it receives (<eref target="#evt-verification">EVT Verification</eref>). The two lookups are not equally valuable to an attacker.</t>
<t><strong>Spoofing the browser's resolver alone gains little.</strong> The browser is directed to an attacker-controlled issuer and discloses the email address to it. It does not disclose the user's credentials: cookies are scoped to the real issuer's origin and are not sent to a different one. The attacker can mint an EVT, but its <tt>iss</tt> is the attacker's identifier, and the RP — resolving the record itself, over its own path — derives the real issuer identifier and rejects the token. The mismatch is what stops it, which is why <eref target="#issuer-identifier">Issuer Identifier</eref> requires the comparison to be exact.</t>
<t><strong>Spoofing the RP's resolver is the attack that matters.</strong> An attacker who controls the RP's view of DNS for the email domain, and who runs an issuer, can present an EVT the RP accepts for any address at that domain. Nothing later in verification catches this: the token is well-formed, correctly signed, and signed by the key the RP was told to trust. This is the residual risk of the design, and it is not mitigated elsewhere in this document.</t>
<t>The RP is better placed to address this than the browser. It is a server, resolving on its own infrastructure, and can validate DNSSEC or use a validating resolver over an authenticated channel. RPs SHOULD validate DNSSEC for <tt>_email-verification.$EMAIL_DOMAIN</tt> where the email domain is signed, and SHOULD resolve through a resolver they trust rather than whatever the host is configured with. Email domain operators publishing this record SHOULD sign their zone.</t>
<t>It has been argued that an authentication decision should never depend on data fetched from DNS. The objection is sound as a general rule and this document does not dismiss it. Two things are true alongside it. The delegation is a property of an email domain, and the authoritative source for a property of an email domain is that domain's DNS — the same place SPF, DKIM, and DMARC already sit, and the same records an attacker with this capability could already forge to redirect or authenticate mail for the domain. And moving the delegation to HTTPS would require every email domain to operate a web server, which <eref target="#why-dns-delegation">Why DNS Delegation?</eref> explains is the barrier this design set out to avoid. The exposure this creates is nonetheless real, unsigned DNS is weaker than HTTPS, and a deployment that cannot obtain DNSSEC on the email domains it accepts is accepting the risk described above.</t>
</section>

<section anchor="email-existence-probing"><name>Email Existence Probing</name>
<t>Any software—not just browsers—can send requests to an issuer's issuance endpoint. An attacker could attempt to use this to probe for valid email addresses:</t>

<ol spacing="compact">
<li><strong>Build email lists</strong>: Probe many addresses to identify valid ones for spam targeting.</li>
<li><strong>Account enumeration</strong>: Determine which email addresses have accounts at specific issuers.</li>
</ol>

<section anchor="uniform-error-responses"><name>Uniform Error Responses</name>
<t>To prevent probing, issuers MUST NOT return different error responses based on whether an email address exists. The <tt>authentication_required</tt> error should be returned uniformly whether:</t>

<ul spacing="compact">
<li>The email address does not exist at this issuer</li>
<li>The email address exists but the user is not authenticated</li>
<li>The email address exists but the authenticated user does not control it</li>
</ul>
<t>This ensures attackers cannot distinguish between "email exists" and "email does not exist" based on error responses.</t>
</section>

<section anchor="timing-attack-mitigations"><name>Timing Attack Mitigations</name>
<t>Response timing can also reveal whether an email address exists. If the issuer performs a database lookup only when the email exists, or takes different code paths based on email existence, an attacker can measure response times to infer information.</t>
<t>Issuers SHOULD mitigate timing attacks using techniques such as:</t>

<ul spacing="compact">
<li><strong>Uniform code paths</strong>: Execute the same operations (database lookups, cryptographic operations) regardless of whether the email exists, avoiding early returns that skip processing steps.</li>
<li><strong>Response delay normalization</strong>: Add delays to normalize response times across all error conditions to a consistent baseline.</li>
</ul>
</section>

<section anchor="additional-mitigations"><name>Additional Mitigations</name>

<ul spacing="compact">
<li><strong>User interaction required</strong>: The browser API requires user gesture and consent before initiating verification, preventing automated probing from browsers.</li>
<li><strong>Rate limiting</strong>: Issuers SHOULD rate-limit requests per IP address to slow down probing attempts from any client.</li>
<li><strong>Sec-Fetch-Dest verification</strong>: The required <tt>Sec-Fetch-Dest: email-verification</tt> header provides a signal that the request originates from a browser, though this can be spoofed by non-browser clients.</li>
<li><strong>Same information as email OTP</strong>: An attacker can already determine email existence by sending verification emails and checking for bounces. EVP does not create new information disclosure beyond what is already possible.</li>
</ul>
<t>Issuers SHOULD implement appropriate rate limiting and abuse detection.</t>
</section>
</section>
</section>

<section anchor="design-rationale"><name>Design Rationale</name>

<section anchor="why-not-solve-email-like-sms-otp"><name>Why Not Solve Email Like SMS OTP?</name>
<t>The WebOTP API and <tt>autocomplete="one-time-code"</tt> standards dramatically reduced friction for SMS verification. A natural question is why email verification cannot use the same approach. Several fundamental differences make this impractical:</t>
<t><strong>SMS is a mobile OS feature; email is application-layer</strong></t>
<t>SMS is integrated into mobile operating systems. The OS receives incoming messages and can parse them before any application sees them. This privileged position enables the OS to recognize origin-bound OTP formats and offer autofill directly to the browser.</t>
<t>Email operates at the application layer. There is no OS-level email subsystem that intercepts incoming messages. Email clients are ordinary applications—whether native apps, desktop programs, or web applications—with no special ability to coordinate with browsers for autofill.</t>
<t><strong>SMS verification is mobile; email verification spans platforms</strong></t>
<t>SMS OTP autofill works on mobile devices where the OS controls the messaging stack. Email verification happens on desktop computers, laptops, tablets, and phones. Any solution for email must work across all these platforms, not just mobile.</t>
<t><strong>SMS senders are aggregators; email senders are RPs</strong></t>
<t>SMS verification messages are typically sent through aggregator services (Twilio, AWS SNS, etc.) that send on behalf of many relying parties. The "sender" of the SMS is often a short code or phone number shared across multiple services. This means the phone number or sender ID carries little identifying information about which RP sent the message.</t>
<t>Email verification messages come directly from the RP's domain. The sender address, domain, and email headers identify the RP. This architectural difference means that email verification inherently reveals more about the RP to the email provider than SMS verification reveals to the carrier.</t>
</section>

<section anchor="why-the-three-party-model"><name>Why the Three-Party Model?</name>
<t>A simpler design would have the issuer create a token directly for the RP, with the RP as the audience. This is how social login works: the identity provider knows which application the user is logging into.</t>
<t>EVP uses a three-party model where the browser intermediates between the issuer and the RP. The issuer creates an EVT bound to the browser's ephemeral public key, and the browser creates a separate KB-JWT that binds the EVT to the RP. The issuer never learns the RP's identity.</t>
<t>This design choice is driven by privacy: for users with domain-based email accounts (personal domains, work accounts), the email provider should not learn which applications the user accesses. The architectural complexity of the three-party model is justified by this privacy benefit.</t>
</section>

<section anchor="why-sd-jwt"><name>Why SD-JWT?</name>
<t>The EVT uses the SD-JWT structure (specifically, the key binding capability from SD-JWT+KB) rather than a plain JWT. This choice provides:</t>

<ol>
<li><t><strong>Key Binding</strong>: The <tt>~</tt> separator and KB-JWT mechanism provide a standard way to bind a token to a holder's key, enabling the three-party model where issuance and presentation are separate operations.</t>
</li>
<li><t><strong>Library Support</strong>: SD-JWT libraries already exist and can parse EVTs, reducing implementation burden for RPs.</t>
</li>
<li><t><strong>Extensibility</strong>: While EVP does not currently use selective disclosure, the SD-JWT structure allows future extensions without changing the token format.</t>
</li>
</ol>
</section>

<section anchor="why-dns-delegation"><name>Why DNS Delegation?</name>
<t>The mail domain delegates email verification to an issuer via a DNS TXT record rather than a <tt>.well-known</tt> file. This choice aligns with how email infrastructure already works:</t>

<ol>
<li><t><strong>Email domains often lack web hosting</strong>: Many users have personal domains used only for email. Requiring a web server to host a <tt>.well-known</tt> file would create a barrier to adoption.</t>
</li>
<li><t><strong>Apex domain challenges</strong>: Email domains are typically apex domains (e.g., <tt>example.com</tt>), which do not support CNAME records. Hosting a web site on an apex domain requires additional infrastructure.</t>
</li>
<li><t><strong>Familiar tooling</strong>: Domain owners already manage DNS records for email (MX, SPF, DKIM, DMARC). Adding another TXT record fits existing workflows.</t>
</li>
</ol>
</section>

<section anchor="why-jwks-over-dkim-keys"><name>Why JWKS Over DKIM Keys?</name>
<t>The issuer publishes signing keys via a JWKS endpoint rather than reusing DKIM keys. While DKIM keys are already associated with email domains, JWKS provides practical advantages:</t>

<ol>
<li><t><strong>Key rotation</strong>: DKIM keys are rarely rotated in practice. JWKS rotation is common in OIDC deployments and follows established patterns.</t>
</li>
<li><t><strong>Algorithm flexibility</strong>: JWKS supports multiple key types and algorithms. DKIM key distribution was designed for a specific use case.</t>
</li>
<li><t><strong>Operational familiarity</strong>: Developers implementing EVP are likely familiar with JWKS from OAuth/OIDC work.</t>
</li>
</ol>
</section>

<section anchor="why-http-message-signatures-rather-than-request-jwt"><name>Why HTTP Message Signatures Rather Than Request JWT?</name>
<t>The original design used a JWT signed by the browser to carry the email address and browser's public key. The HTTP Message Signatures approach was chosen because:</t>

<ol spacing="compact">
<li><strong>Standards-Based</strong>: <xref target="RFC9421"/> is a published standard for signing HTTP messages, providing better interoperability</li>
<li><strong>Cookie Binding</strong>: HTTP Message Signatures can directly sign the <tt>cookie</tt> header where a signer is able to, providing stronger binding between authentication cookies and the request</li>
<li><strong>Flexibility</strong>: The signature can cover any HTTP components, making it easier to add additional protections in the future</li>
<li><strong>Simpler Key Distribution</strong>: The Signature-Key header provides a standardized way to distribute keys inline with the request</li>
</ol>
</section>
</section>

<section anchor="implementation-status"><name>Implementation Status</name>
<t><em>Note: This section is to be removed before publishing as an RFC.</em></t>
<t>This section records the status of known implementations of the protocol defined by this specification at the time of posting of this Internet-Draft, and is based on a proposal described in <xref target="RFC7942"/>. The description of implementations in this section is intended to assist the IETF in its decision processes in progressing drafts to RFCs.</t>
<t>The following implementations are known:</t>

<ul>
<li><t><strong>Hellō</strong> — <eref target="https://hello.coop">hello.coop</eref>. Organization: Hellō. Role: Issuer. Coverage: EVT issuance endpoint, issuer discovery via DNS TXT, JWKS endpoint. Level of maturity: exploratory.</t>
</li>
<li><t><strong>GMail</strong> — <eref target="https://gmail.com">gmail.com</eref>. Organization: Google. Role: Issuer. Coverage: EVT issuance endpoint, issuer discovery via DNS TXT, JWKS endpoint. Level of maturity: origin trial.</t>
</li>
<li><t><strong>Chrome</strong> — Organization: Google. Role: Browser. An origin trial is available at <eref target="https://developer.chrome.com/origintrials/#/view_trial/10696049115004929">https://developer.chrome.com/origintrials/#/view_trial/10696049115004929</eref>.</t>
</li>
<li><t><strong>Edge</strong> — Organization: Microsoft. Role: Browser. An origin trial is available at <eref target="https://developer.microsoft.com/en-us/microsoft-edge/origin-trials/trials/683027d2-a798-460f-9b35-03a95002d9af">https://developer.microsoft.com/en-us/microsoft-edge/origin-trials/trials/683027d2-a798-460f-9b35-03a95002d9af</eref>.</t>
</li>
</ul>
</section>

<section anchor="document-history"><name>Document History</name>
<t><em>Note: This section is to be removed before publishing as an RFC.</em></t>

<ul spacing="compact">
<li>draft-hardt-email-verification-02</li>
</ul>
<t>Tracked the breaking changes in draft-hardt-httpbis-signature-key-08. A token request serialized per -01 is rejected by an issuer implementing this version, and one serialized per this version is rejected by an issuer implementing -01. There is nothing to negotiate over, so both ends of a deployment move together.</t>

<ul>
<li>Added the required <tt>alg</tt> parameter to every <tt>Signature-Key</tt> example. Signature-Key -08 makes the key's <tt>alg</tt> the single source of truth for the algorithm; -07 forbade the parameter outright.</li>
<li>Replaced the polymorphic <tt>EdDSA</tt> identifier with <tt>Ed25519</tt> throughout: the browser's key, the default when <tt>signing_alg_values_supported</tt> is absent, the EVT header, the KB-JWT header, and every example. <tt>EdDSA</tt> is deprecated by <xref target="RFC9864"/> and forbidden by Signature-Key -08.</li>
<li>Required <tt>signing_alg_values_supported</tt> to carry fully-specified identifiers, and forbade <tt>EdDSA</tt>, <tt>none</tt>, and the symmetric MAC identifiers. The member governs issued EVTs as well as HTTP Message Signatures, so the constraint is stated here rather than left to Signature-Key.</li>
<li>Rewrote HTTP Request Verification to delegate to <xref target="RFC9421"/> and <xref target="I-D.hardt-httpbis-signature-key"/> rather than restate them, and to keep only what this specification profiles: <tt>hwk</tt> as the only scheme, the required covered components, the <tt>created</tt> window, and <tt>signing_alg_values_supported</tt> as the discovery-time form of <tt>Accept-Signature-Alg</tt>. The restated text was what went stale — the previous step listing the <tt>hwk</tt> parameters as <tt>kty</tt>, <tt>crv</tt>, and <tt>x</tt> silently became wrong when -08 added <tt>alg</tt>.</li>
<li>Carried the key's <tt>alg</tt> member into the EVT <tt>cnf.jwk</tt>, so the RP verifies the KB-JWT under a fully-specified algorithm rather than one derived from the key. Required the KB-JWT header <tt>alg</tt> to match it, and had the RP reject a KB-JWT where the two disagree.</li>
<li>Required the EVT header <tt>alg</tt> to be fully specified. The EVT is an ordinary JWT and outside the scope of Signature-Key, so nothing else imposes this.</li>
<li>Added the <tt>Signature-Error</tt> response header to signature error responses, alongside this specification's existing JSON error body, and said how the two relate: the body reports <tt>invalid_signature</tt> in every case and the header carries which failure it was.</li>
<li>Added a Fully-Specified Algorithms subsection to Security Considerations giving the reason and naming which of the three signatures each rule reaches.</li>
<li>Defined "valid email address" as the "valid e-mail address" production of <xref target="WHATWG.HTML"/> rather than leaving the term undefined, and said why that production rather than <xref target="RFC5322"/>. Addresses issue #2.</li>
<li>Added a DNS Delegation subsection to Security Considerations. The delegation rests on an unauthenticated TXT record and the document said nothing about it. Separated the two lookups: spoofing the browser's resolver discloses the email address but yields no token the RP will accept, since cookies are origin-scoped and the RP resolves the record itself; spoofing the RP's resolver is the attack that matters and is not mitigated elsewhere. Recommended DNSSEC validation by RPs and zone signing by email domain operators, and stated the residual risk. Addresses issue #6.</li>
<li>Made the issuer identifier an HTTPS origin rather than a bare host name, aligning the <tt>iss</tt> claim with <xref target="OpenID.Core"/> and <xref target="RFC8414"/> and with what the browser implementation already enforces. The DNS TXT record still carries a host name; the identifier is derived from it by prefixing <tt>https://</tt>, and every comparison is byte-for-byte on the derived string. Added an Issuer Identifier section stating the derivation once. Addresses issue #7.</li>
<li>Added the <tt>issuer</tt> member to the metadata document and required a fetching party to reject a document whose <tt>issuer</tt> does not match the identity it was fetched under, per <xref target="RFC8414"/>, Section 3.3. This is the check Signature-Key -08 added for its own discovery, applied here.</li>
<li>Made coverage of the <tt>cookie</tt> component RECOMMENDED when the header is available to the signer at signing time, rather than required, and forbade an issuer from rejecting a request solely because it is not covered. In some browser architectures the Cookie header is attached after the request is constructed and signed, so the requirement was unimplementable. Addresses issue #11.</li>
<li>Replaced the Email Discovery models with what this protocol requires: candidate addresses come from the W3C Email Verification API, the browser offers only valid email addresses, and from selection onward the address is never altered — it is sent byte-for-byte in the token request, returned byte-for-byte in the <tt>email</tt> claim, and every email comparison is byte-for-byte. Addresses issues #4, #13, and #24.</li>
<li>Required the <tt>Content-Digest</tt> header (<xref target="RFC9530"/>) on the token request and added <tt>content-digest</tt> to the covered components. The email address being verified is carried in the request body, which no covered component reached, so the signature attested to a request without attesting to which address it asked for. Required the issuer to recompute the digest against the received bytes rather than rely on the signature over the header alone. Addresses issue #3.</li>
<li>Removed the WebAuthn Authentication section, the <tt>webauthn_supported</tt> metadata member, and the WebAuthn challenge/response exchange. Nothing implements it, and it hardwired one authentication method into the protocol. A request without a valid session returns <tt>authentication_required</tt>. An issuer-provided login URL using the FedCM Login Status continuation is being explored as a replacement; that flow is an exploration only, dependent on the browser developing an acceptable user experience for the login popup.</li>
<li><t>Widened the <tt>created</tt> acceptance window from 60 to 300 seconds. Device clocks skew by minutes, not seconds, and the window is not load-bearing for security: a replayed request yields an EVT bound to the original browser's key. Follows the clock-skew guidance of <xref target="RFC8725"/>.</t>
</li>
<li><t>draft-hardt-email-verification-01</t>

<ul spacing="compact">
<li>Updated Implementation Status: completed GMail issuer entry, added Chrome and Edge origin trials.</li>
</ul></li>
<li><t>draft-hardt-email-verification-00</t>

<ul spacing="compact">
<li>Initial draft.</li>
</ul></li>
</ul>
</section>

</middle>

<back>
<references><name>References</name>
<references><name>Normative References</name>
<xi:include href="https://bib.ietf.org/public/rfc/bibxml3/reference.I-D.hardt-httpbis-signature-key.xml"/>
<xi:include href="https://bib.ietf.org/public/rfc/bibxml3/reference.I-D.ietf-oauth-selective-disclosure-jwt.xml"/>
<reference anchor="OpenID.Core" target="https://openid.net/specs/openid-connect-core-1_0.html">
  <front>
    <title>OpenID Connect Core 1.0</title>
    <author fullname="Nat Sakimura" initials="N." surname="Sakimura">
      <organization>NRI</organization>
    </author>
    <author fullname="John Bradley" initials="J." surname="Bradley">
      <organization>Ping Identity</organization>
    </author>
    <author fullname="Michael B. Jones" initials="M." surname="Jones">
      <organization>Microsoft</organization>
    </author>
    <author fullname="Breno de Medeiros" initials="B." surname="de Medeiros">
      <organization>Google</organization>
    </author>
    <author fullname="Chuck Mortimore" initials="C." surname="Mortimore">
      <organization>Salesforce</organization>
    </author>
    <date year="2014" month="November"/>
  </front>
</reference>
<xi:include href="https://bib.ietf.org/public/rfc/bibxml/reference.RFC.5322.xml"/>
<xi:include href="https://bib.ietf.org/public/rfc/bibxml/reference.RFC.8414.xml"/>
<xi:include href="https://bib.ietf.org/public/rfc/bibxml/reference.RFC.9421.xml"/>
<xi:include href="https://bib.ietf.org/public/rfc/bibxml/reference.RFC.9530.xml"/>
<xi:include href="https://bib.ietf.org/public/rfc/bibxml/reference.RFC.9864.xml"/>
<reference anchor="WHATWG.HTML" target="https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address">
  <front>
    <title>HTML Standard</title>
    <author>
      <organization>WHATWG</organization>
    </author>
    <date year="2026"/>
  </front>
  <refcontent>Living Standard</refcontent>
</reference>
</references>
<references><name>Informative References</name>
<reference anchor="EVP-Browser" target="https://wicg.github.io/email-verification/">
  <front>
    <title>Email Verification API</title>
    <author>
      <organization>WICG</organization>
    </author>
    <date year="2025"/>
  </front>
</reference>
<reference anchor="LightweightFedCM" target="https://github.com/fedidcg/LightweightFedCM">
  <front>
    <title>Lightweight FedCM</title>
    <author>
      <organization>FedID CG</organization>
    </author>
    <date year="2025"/>
  </front>
</reference>
<xi:include href="https://bib.ietf.org/public/rfc/bibxml/reference.RFC.6973.xml"/>
<xi:include href="https://bib.ietf.org/public/rfc/bibxml/reference.RFC.7942.xml"/>
<xi:include href="https://bib.ietf.org/public/rfc/bibxml/reference.RFC.8725.xml"/>
</references>
</references>

<section anchor="acknowledgments"><name>Acknowledgments</name>
<t>The authors would like to thank reviewers for their feedback on this specification.</t>
</section>

</back>

</rfc>
