Files
llm-wiki/raw/articles/email-verification-protocol-draft-2026.md
T
2026-07-03 00:38:05 +09:00

896 lines
74 KiB
Markdown

---
source_url: "https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html"
ingested: 2026-07-02
sha256: 5785e14e32360190dc521e00fe46f261e051c49ee8d976edc4d09b2303ee5c07
discovered_from:
platform: discord
channel_id: "1028287639918497822"
channel_name: "chat"
message_id: "1522173636247687239"
author_id: "890908900520505354"
posted_at: "2026-07-02T09:34:35.500000000Z"
message_excerpt: |-
Email Verification Protocol draft link
---
| Internet-Draft | EVP | January 2026 |
| --- | --- | --- |
| Hardt & Goto | Expires 13 July 2026 | \[Page\] |
## Abstract
This document defines the Email Verification Protocol (EVP), which enables web applications to verify that a user controls an email address without sending a verification email. The protocol uses a three-party model where the browser intermediates between the relying party and an issuer, providing both improved user experience and privacy protection.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-abstract-1)
*Note: This section is to be removed before publishing as an RFC.*[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-note.1-1)
Source for this draft and an issue tracker can be found at [https://github.com/dickhardt/email-verification](https://github.com/dickhardt/email-verification).[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-note.1-2)
The browser API aspects are being developed separately by the W3C (\[\]).[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-note.1-3)
## Status of This Memo
This Internet-Draft is submitted in full conformance with the provisions of BCP 78 and BCP 79.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-boilerplate.1-1)
Internet-Drafts are working documents of the Internet Engineering Task Force (IETF). Note that other groups may also distribute working documents as Internet-Drafts. The list of current Internet-Drafts is at [https://datatracker.ietf.org/drafts/current/](https://datatracker.ietf.org/drafts/current/).[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-boilerplate.1-2)
Internet-Drafts are draft documents valid for a maximum of six months and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to use Internet-Drafts as reference material or to cite them other than as "work in progress." [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-boilerplate.1-3)
This Internet-Draft will expire on 13 July 2026.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-boilerplate.1-4)
## 1.
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.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-1-1)
### 1.1.
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.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-1.1-1)
Some approaches to reduce this friction:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-1.1-2)
- **Social login**: 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.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-1.1-3.1.1)
- **Magic links**: 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.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-1.1-3.2.1)
### 1.3.
The Email Verification Protocol (EVP) enables a web application to obtain a verified email address **without sending an email** and **without the user leaving the web page**. 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.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-1.3-1)
**Note on deliverability**: Like social login, this protocol verifies that the user controls an email address — it does not verify that the email address can receive mail.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-1.3-2)
## 2.
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 (\[\]).[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-2-1)
- **Issuer**: The service that verifies the user controls an email address. See [Issuer Discovery](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#issuer-discovery) for how email domains delegate to issuers.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-2-2.1.1)
- **Three-party model**: 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.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-2-2.2.1)
The following diagram illustrates the protocol flow between the RP Server, Browser, and Issuer:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-2-3)
```
Step RP Server Browser Issuer
| | |
2.1 Session Binding |--- nonce ->| |
| | |
2.2 Email Acquisition | [obtain email from user] |
| | |
2.3 Token Request | |-- POST /issuance ->|
| | (email, ...) |
| | |
2.4 EVT Creation | | [create EVT]
| | |
2.5 Token Issuance | |<------ EVT --------|
| | |
2.6 KB Creation | [create KB-JWT] |
| | |
2.7 Token Presentation |<-- EVT+KB -| |
| | |
2.8 Token Verification [verify EVT+KB] | |
| | |
```
[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-2-4)
### 2.1.
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 (\[\]).[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-2.1-1)
### 2.2.
The browser obtains an email address from the user. This mechanism is being defined by the W3C (\[\]).[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-2.2-1)
### 2.3.
Once the browser has the email address and nonce:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-2.3-1)
1. The browser performs [Issuer Discovery](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#issuer-discovery) for the email address to obtain the issuer's metadata, including the `issuance_endpoint`.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-2.3-2.1.1)
2. The browser generates a fresh private/public key pair. The browser SHOULD select an algorithm from the issuer's `signing_alg_values_supported` array, or use "EdDSA" if not present.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-2.3-2.2.1)
3. The browser creates a signed request per [HTTP Message Signatures](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#http-signatures) and POSTs to the `issuance_endpoint`, including the issuer's cookies. The request body is a JSON object with the following parameters:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-2.3-2.3.1)
- `email` (REQUIRED): The email address to verify [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-2.3-2.3.2.1)
- See [Private Email Addresses](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#private-email) for parameters to request private email addresses [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-2.3-2.3.2.2)
- See [WebAuthn Authentication](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#webauthn-authentication) for parameters to respond to a WebAuthn challenge [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-2.3-2.3.2.3)
```
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":"[email protected]"}
```
[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-2.3-3)
### 2.4.
On receipt of a token request:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-2.4-1)
1. The issuer verifies the request per [Request Verification](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#request-verification).[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-2.4-2.1.1)
2. The issuer checks if the cookies represent a logged-in user who controls the requested email address. If the issuer supports WebAuthn (`webauthn_supported: true`) and cookies are not present or invalid, the issuer MAY return a WebAuthn challenge (see [WebAuthn Authentication](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#webauthn-authentication)).[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-2.4-2.2.1)
3. If authentication succeeds, the issuer creates an EVT per [EVT Creation](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#evt-creation) and returns it as the value of `issuance_token` in an `application/json` response. The issuer MAY include `Set-Cookie` headers to establish or update session state:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-2.4-2.3.1)
```
HTTP/1.1 200 OK
Content-Type: application/json
Set-Cookie: session=...; Secure; HttpOnly; SameSite=None
{"issuance_token":"eyJhbGciOiJFZERTQSIsImtpZCI6IjIwMjQtMDgtMTkiLCJ0eXAiOiJldnQrand0In0...~"}
```
[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-2.4-3)
The browser MUST process any `Set-Cookie` headers in the response.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-2.4-4)
### 2.5.
On receiving the `issuance_token`:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-2.5-1)
1. The browser verifies the EVT per [EVT Verification](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#evt-verification), additionally confirming:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-2.5-2.1.1)
- The `email` claim matches the email address being verified [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-2.5-2.1.2.1)
- The `cnf.jwk` claim matches the public key the browser generated [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-2.5-2.1.2.2)
2. The browser creates a KB-JWT per [KB-JWT Creation](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#kb-creation-detail), binding the EVT to the RP's origin and session nonce.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-2.5-2.2.1)
3. The browser concatenates the EVT and KB-JWT to form the EVT+KB.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-2.5-2.3.1)
Example EVT+KB (line breaks for display):[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-2.5-3)
```
eyJhbGciOiJFZERTQSIsImtpZCI6IjIwMjQtMDgtMTkiLCJ0eXAiOiJldnQrand0In0.
eyJpc3MiOiJpc3N1ZXIuZXhhbXBsZSIsImlhdCI6MTcyNDA4MzIwMCwiY25mIjp7...}.
signature~
eyJhbGciOiJFZERTQSIsInR5cCI6ImtiK2p3dCJ9.
eyJhdWQiOiJodHRwczovL3JwLmV4YW1wbGUiLCJub25jZSI6IjI1OWM1ZWFlLTQ4...}.
signature
```
[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-2.5-4)
### 2.6.
The browser provides the EVT+KB to the RP. This mechanism is being defined by the W3C (\[\]).[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-2.6-1)
### 2.7.
The RP receives the EVT+KB and verifies it by:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-2.7-1)
1. Verifying the KB-JWT per [KB-JWT Verification](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#kb-verification) [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-2.7-2.1)
2. Verifying the EVT per [EVT Verification](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#evt-verification) [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-2.7-2.2)
3. Verifying the KB-JWT signature using the public key from the EVT's `cnf.jwk` claim [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-2.7-2.3)
If all verification steps pass, the RP has successfully verified that the user controls the email address in the `email` claim.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-2.7-3)
## 3.
Both the browser and the RP need to discover information about the issuer for a given email address. This section describes the discovery process.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-3-1)
### 3.1.
The email domain delegates email verification to an issuer via a DNS TXT record. Given an email address, parse the email domain (``EMAIL_DOMAIN) and look up the `TXT` record for `_email-verification.``EMAIL\_DOMAIN`. The contents of the record MUST start with` iss= `followed by the issuer identifier. There MUST be only one` TXT `record for` \_email-verification.$EMAIL\_DOMAIN\`.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-3.1-1)
Example record:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-3.1-2)
```bash
_email-verification.email-domain.example TXT iss=issuer.example
```
[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-3.1-3)
This record states that `email-domain.example` has delegated email verification to the issuer `issuer.example`.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-3.1-4)
If the email domain and the issuer are the same domain, then the record would be:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-3.1-5)
```bash
_email-verification.issuer.example TXT iss=issuer.example
```
[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-3.1-6)
> 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 `issuer.example` could not setup an issuer that would grant them verified emails for any email at `issuer.example`.[¶](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-3.1-7.1)
Once the issuer identifier is known, fetch the metadata document from `https://$ISSUER/.well-known/email-verification`. The request MUST follow redirects to the same path but with a different subdomain of the Issuer.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-3.2-1)
For example, `https://issuer.example/.well-known/email-verification` may redirect to `https://accounts.issuer.example/.well-known/email-verification`.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-3.2-2)
The metadata document is JSON containing the following properties:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-3.2-3)
- *issuance\_endpoint* - the API endpoint the browser calls to obtain an EVT [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-3.2-4.1)
- *jwks\_uri* - the URL where the issuer provides its public keys to verify the EVT [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-3.2-4.2)
- *signing\_alg\_values\_supported* - OPTIONAL. JSON array containing a list of the signing algorithms ("alg" 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. If omitted, "EdDSA" is the default. "EdDSA" SHOULD be included in the supported algorithms list. The value "none" MUST NOT be used.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-3.2-4.3)
- *webauthn\_supported* - OPTIONAL. Boolean indicating whether the issuer supports WebAuthn authentication as an alternative to cookies. If `true`, the issuer may return a WebAuthn challenge when cookies are not present or invalid. Defaults to `false`.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-3.2-4.4)
- *private\_email\_supported* - OPTIONAL. Boolean indicating whether the issuer supports generating private email addresses. Defaults to `false`.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-3.2-4.5)
> **Open Question**: Should URL properties be required to include the issuer domain as the root of their hostname?[¶](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-3.2-5.1)
Following is an example `.well-known/email-verification` file:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-3.2-6)
```json
{
"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
}
```
[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-3.2-7)
## 4.
This section defines how HTTP Message Signatures (\[\]) are used in token requests. The browser signs requests to prove possession of a key pair, and the issuer verifies these signatures.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-4-1)
### 4.1.
The browser creates a signed request by:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-4.1-1)
1. Creating a JSON request body with the email address and optional parameters [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-4.1-2.1)
2. Creating the `Signature-Key` header using the `hwk` scheme (\[\]) with the browser's public key [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-4.1-2.2)
3. Creating the `Signature-Input` header specifying the covered components [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-4.1-2.3)
4. Computing the signature base per \[\] Section 2.5 and signing with the browser's private key [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-4.1-2.4)
5. Creating the `Signature` header with the base64-encoded signature [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-4.1-2.5)
#### 4.1.1.
The request body is a JSON object with the following fields:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-4.1.1-1)
- `email` (REQUIRED): The email address to verify [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-4.1.1-2.1)
- `private_email` (OPTIONAL): Request a new private email address. See [Private Email Addresses](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#private-email).[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-4.1.1-2.2)
- `directed_email` (OPTIONAL): A previously issued private email address to reuse. See [Private Email Addresses](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#private-email).[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-4.1.1-2.3)
Example:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-4.1.1-3)
```json
{
"email": "[email protected]"
}
```
[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-4.1.1-4)
#### 4.1.2.
The `Signature-Key` header uses the `hwk` scheme to convey the browser's public key:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-4.1.2-1)
```
Signature-Key: sig=hwk; kty="OKP"; crv="Ed25519"; \
x="JrQLj5P_89iXES9-vFgrIy29clF9CC_oPPsw3c5D0bs"
```
[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-4.1.2-2)
#### 4.1.3.
The covered components MUST include `@method`, `@authority`, `@path`, and `signature-key`. The `cookie` component MUST be included when the Cookie header is present, and MUST be omitted when it is not (per \[\] Section 2.5). The `created` parameter MUST be included.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-4.1.3-1)
```
Signature-Input: sig=("@method" "@authority" "@path" \
"cookie" "signature-key");created=1692345600
```
[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-4.1.3-2)
#### 4.1.4.
```
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":"[email protected]"}
```
[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-4.1.4-1)
### 4.2.
The issuer MUST verify the request headers:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-4.2-1)
- `Content-Type` is `application/json` [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-4.2-2.1)
- `Sec-Fetch-Dest` is `email-verification` [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-4.2-2.2)
- `Signature-Input` is present [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-4.2-2.3)
- `Signature` is present [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-4.2-2.4)
- `Signature-Key` is present with `sig=hwk` scheme [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-4.2-2.5)
The issuer MUST verify the HTTP Message Signature by:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-4.2-3)
1. Parsing the `Signature-Key` header and extracting the public key from the `hwk` parameters (`kty`, `crv`, `x` for OKP keys) [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-4.2-4.1)
2. Parsing the `Signature-Input` header to determine the covered components [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-4.2-4.2)
3. Verifying that the signature covers at minimum: `@method`, `@authority`, `@path`, and `signature-key`. The signature MUST also cover `cookie` when the Cookie header is present.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-4.2-4.3)
4. Reconstructing the signature base per \[\] Section 2.5 [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-4.2-4.4)
5. Verifying the signature in the `Signature` header using the extracted public key [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-4.2-4.5)
6. Verifying the `created` timestamp in `Signature-Input` is within 60 seconds of the current time [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-4.2-4.6)
The issuer MUST verify the request body:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-4.2-5)
1. Parsing the JSON body and extracting the `email` field [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-4.2-6.1)
2. Verifying the `email` field contains a syntactically valid email address [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-4.2-6.2)
## 5.
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.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-5-1)
### 5.1.
The EVT is a JWT with the following structure:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-5.1-1)
#### 5.1.2.
Required claims:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-5.1.2-1)
- `iss`: The issuer identifier [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-5.1.2-2.1)
- `iat`: Issued at time (seconds since epoch) [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-5.1.2-2.2)
- `cnf`: Confirmation claim containing the browser's public key in `jwk` format (for SD-JWT Key Binding compatibility) [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-5.1.2-2.3)
- `email`: The verified email address [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-5.1.2-2.4)
- `email_verified`: Boolean, MUST be `true` [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-5.1.2-2.5)
Optional claims:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-5.1.2-3)
- `is_private_email`: Boolean, set to `true` when the email is a private address [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-5.1.2-4.1)
Example:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-5.1.2-5)
```json
{
"iss": "issuer.example",
"iat": 1724083200,
"cnf": {
"jwk": {
"kty": "OKP",
"crv": "Ed25519",
"x": "JrQLj5P_89iXES9-vFgrIy29clF9CC_oPPsw3c5D0bs"
}
},
"email": "[email protected]",
"email_verified": true
}
```
[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-5.1.2-6)
#### 5.1.3.
The EVT has a `~` appended to it for SD-JWT compatibility (see [SD-JWT Compatibility](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#sd-jwt-compatibility)).[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-5.1.3-1)
### 5.2.
After verifying the request (see [Request Verification](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#request-verification)) and authenticating the user, the issuer creates the EVT:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-5.2-1)
1. Construct the header with `alg`, `kid`, and `typ` [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-5.2-2.1)
2. Construct the payload with `iss`, `iat`, `cnf` (containing the public key from the `Signature-Key` header), `email`, and `email_verified` [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-5.2-2.2)
3. If a private email is requested, include `is_private_email: true` and set `email` to the private address [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-5.2-2.3)
4. Sign the JWT with the issuer's private key corresponding to the `kid` [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-5.2-2.4)
5. Append `~` to the signed JWT [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-5.2-2.5)
> Note: The `is_private_email` claim name matches Apple's Sign in with Apple for compatibility with existing RP implementations.[¶](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-5.2-3.1)
### 5.3.
Both the browser and RP verify the EVT. The verification steps are:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-5.3-1)
1. Parse the EVT into header, payload, and signature components [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-5.3-2.1)
2. Extract and validate the `alg` and `kid` from the header [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-5.3-2.2)
3. Extract and validate the `iss`, `iat`, `cnf`, `email`, and `email_verified` claims from the payload [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-5.3-2.3)
4. Perform [Issuer Discovery](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#issuer-discovery) for the email domain to verify the `iss` claim matches the issuer identifier [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-5.3-2.4)
5. Fetch the issuer's public keys from the `jwks_uri` in the issuer metadata [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-5.3-2.5)
6. Verify the EVT signature using the public key identified by `kid` [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-5.3-2.6)
7. Verify `iat` is within an acceptable time window [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-5.3-2.7)
8. Verify `email_verified` is `true` [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-5.3-2.8)
The browser additionally verifies:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-5.3-3)
- The `email` claim matches the email address being verified [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-5.3-4.1)
- The `cnf.jwk` claim matches the public key the browser generated [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-5.3-4.2)
## 6.
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.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-6-1)
### 6.1.
The KB-JWT is a JWT with the following structure:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-6.1-1)
#### 6.1.1.
- `alg` (REQUIRED): Signing algorithm (same as the browser's key pair) [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-6.1.1-1.1)
- `typ` (REQUIRED): Set to "kb+jwt" for SD-JWT library compatibility [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-6.1.1-1.2)
Example:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-6.1.1-2)
```json
{
"alg": "EdDSA",
"typ": "kb+jwt"
}
```
[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-6.1.1-3)
#### 6.1.2.
- `aud` (REQUIRED): The RP's origin [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-6.1.2-1.1)
- `nonce` (REQUIRED): The nonce from the RP's session [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-6.1.2-1.2)
- `iat` (REQUIRED): Issued at time [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-6.1.2-1.3)
- `sd_hash` (REQUIRED): SHA-256 hash of the EVT for SD-JWT library compatibility [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-6.1.2-1.4)
Example:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-6.1.2-2)
```json
{
"aud": "https://rp.example",
"nonce": "259c5eae-486d-4b0f-b666-2a5b5ce1c925",
"iat": 1724083260,
"sd_hash": "X9yH0Ajrdm1Oij4tWso9UzzKJvPoDxwmuEcO3XAdRC0"
}
```
[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-6.1.2-3)
### 6.2.
The EVT+KB is formed by concatenating the EVT and KB-JWT separated by a tilde:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-6.2-1)
```
<EVT>~<KB-JWT>
```
[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-6.2-2)
The EVT already has a trailing `~` from its SD-JWT format, so the full structure is:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-6.2-3)
```
<JWT>~<KB-JWT>
```
[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-6.2-4)
### 6.3.
The EVT+KB format is compatible with SD-JWT with Key Binding as specified in \[\], though this protocol does not use selective disclosure features. The following SD-JWT features are used:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-6.3-1)
- **Trailing `~` on EVT**: The EVT uses the SD-JWT format (JWT with `~` suffix) [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-6.3-2.1)
- **`cnf` claim**: The EVT includes the `cnf` claim with `jwk` for holder key binding [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-6.3-2.2)
- **`typ: "kb+jwt"`**: The KB-JWT uses the SD-JWT Key Binding JWT type [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-6.3-2.3)
- **`sd_hash` claim**: The KB-JWT includes the SD-JWT hash of the EVT [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-6.3-2.4)
- **Concatenation format**: The EVT+KB uses the SD-JWT `<Issuer-signed-JWT>~<KB-JWT>` format [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-6.3-2.5)
Standard SD-JWT libraries can be used to parse and validate EVT+KB tokens.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-6.3-3)
### 6.4.
After verifying the EVT (see [EVT Verification](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#evt-verification)), the browser creates the KB-JWT:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-6.4-1)
1. Construct the header with `alg` and `typ` [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-6.4-2.1)
2. Construct the payload with:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-6.4-2.2.1)
- `aud`: The RP's origin [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-6.4-2.2.2.1)
- `nonce`: The nonce from the RP's session [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-6.4-2.2.2.2)
- `iat`: Current time [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-6.4-2.2.2.3)
- `sd_hash`: SHA-256 hash of the EVT (including the trailing `~`) [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-6.4-2.2.2.4)
3. Sign the KB-JWT with the browser's private key [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-6.4-2.3)
4. Concatenate with the EVT to form the EVT+KB [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-6.4-2.4)
### 6.5.
The RP verifies the KB-JWT by:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-6.5-1)
1. Parse the EVT+KB by separating at the tilde [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-6.5-2.1)
2. Parse the KB-JWT into header, payload, and signature [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-6.5-2.2)
3. Extract `alg` from the header and `aud`, `nonce`, `iat`, `sd_hash` from the payload [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-6.5-2.3)
4. Verify `aud` matches the RP's origin [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-6.5-2.4)
5. Verify `nonce` matches the nonce from the RP's session [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-6.5-2.5)
6. Verify `iat` is within a reasonable time window [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-6.5-2.6)
7. Compute the SHA-256 hash of the EVT and verify it matches `sd_hash` [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-6.5-2.7)
8. Verify the KB-JWT signature using the public key from the EVT's `cnf.jwk` claim [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-6.5-2.8)
## 7.
When the issuer supports WebAuthn (`webauthn_supported: true` in metadata) and a token request lacks valid authentication cookies, the issuer MAY return a WebAuthn challenge to authenticate the user. This enables email verification even when the user is not logged into the issuer via cookies, using any WebAuthn-compatible credential (passkeys, security keys, platform authenticators).[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-7-1)
### 7.1.
Instead of returning an error or an EVT, the issuer returns a WebAuthn challenge. The issuer MAY include `Set-Cookie` headers to maintain challenge state:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-7.1-1)
**HTTP 401 Unauthorized** [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-7.1-2)
```
HTTP/1.1 401 Unauthorized
Content-Type: application/json
Set-Cookie: webauthn_state=...; Secure; HttpOnly; SameSite=None; Max-Age=300
{
"webauthn_challenge": {
"challenge": "dGVzdC1jaGFsbGVuZ2UtZGF0YQ",
"timeout": 60000,
"rpId": "issuer.example",
"allowCredentials": [
{
"type": "public-key",
"id": "Y3JlZGVudGlhbC1pZA"
}
],
"userVerification": "preferred"
}
}
```
[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-7.1-3)
The `webauthn_challenge` object follows the structure of PublicKeyCredentialRequestOptions as defined in \[\].[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-7.1-4)
The browser MUST process any `Set-Cookie` headers in the response. The issuer can use cookies to maintain challenge state, enabling stateless verification of the WebAuthn response. Alternatively, the issuer MAY store challenges server-side with a short TTL.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-7.1-5)
### 7.2.
After the browser obtains a WebAuthn assertion (this mechanism is being defined by the W3C (\[\])), it sends a new request to the issuance endpoint with the `webauthn_response`. The browser MUST include any cookies set by the challenge response:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-7.2-1)
```
POST /email-verification/issuance HTTP/1.1
Host: accounts.issuer.example
Cookie: webauthn_state=...
Content-Type: application/json
Sec-Fetch-Dest: email-verification
Signature-Input: sig=("@method" "@authority" "@path" "cookie" "signature-key");created=1692345600
Signature: sig=:...:
Signature-Key: sig=hwk; kty="OKP"; crv="Ed25519"; x="JrQLj5P_89iXES9-vFgrIy29clF9CC_oPPsw3c5D0bs"
{
"email": "[email protected]",
"webauthn_response": {
"id": "Y3JlZGVudGlhbC1pZA",
"rawId": "Y3JlZGVudGlhbC1pZA",
"response": {
"authenticatorData": "...",
"clientDataJSON": "...",
"signature": "..."
},
"type": "public-key"
}
}
```
[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-7.2-2)
The `webauthn_response` object follows the structure of PublicKeyCredential as defined in \[\].[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-7.2-3)
> Note: The `cookie` component MUST be included in the signature when cookies are present (such as those set by the challenge response). If no cookies are present, the `cookie` component is omitted per [HTTP Request Signing](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#request-signing).[¶](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-7.2-4.1)
### 7.3.
The issuer verifies the WebAuthn response against its stored credentials for the email address. If verification succeeds, the issuer returns the EVT as described in [EVT Issuance](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#evt-issuance).[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-7.3-1)
## 8.
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:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-8-1)
- **Single-use**: The browser requests a new private email and does not store it [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-8-2.1)
- **Reusable**: The browser stores the private email and passes it back via `directed_email` for account continuity [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-8-2.2)
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 `private_email: true` 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 `directed_email`.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-8-3)
### 8.1.
The token request body supports one of the following parameters for private email addresses (mutually exclusive):[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-8.1-1)
- `private_email` (OPTIONAL): Boolean. When set to `true`, requests a new private email address instead of the user's actual email.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-8.1-2.1.1)
- `directed_email` (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 `email` in the request.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-8.1-2.2.1)
### 8.2.
Request for a new private email address:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-8.2-1)
```json
{
"email": "[email protected]",
"private_email": true
}
```
[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-8.2-2)
Request to reuse a previously issued private email address:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-8.2-3)
```json
{
"email": "[email protected]",
"directed_email": "[email protected]"
}
```
[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-8.2-4)
### 8.3.
- The private email MUST be a valid email address that the issuer can route to the user's actual mailbox [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-8.3-1.1)
- The private email SHOULD be unique per user and per RP origin (derived from the browser's context) [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-8.3-1.2)
- If `directed_email` is provided and is linked to the `email` address in the request, the issuer MUST return the same private email address [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-8.3-1.3)
- If `directed_email` is provided but is invalid or not linked to the `email`, the issuer MUST return an error [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-8.3-1.4)
- The private email address is included in the EVT `email` claim [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-8.3-1.5)
- The EVT MUST include `is_private_email: true` when a private email address is issued [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-8.3-1.6)
### 8.4.
The domain of the private email address does not need to match the domain of the user's actual email address. Additionally, the `iss` claim in the EVT corresponds to the issuer for the private email domain, which may differ from the issuer the browser initially contacted.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-8.4-1)
For example, a user with `[email protected]` may receive a private email address `[email protected]`. The EVT's `iss` claim would be the issuer for `privaterelay.different.example`. 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., `[email protected]`) where the domain itself is a unique identifier that would otherwise reveal the user's identity.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-8.4-2)
### 8.5.
When a private email is issued, the EVT contains the private address in the `email` claim and includes `is_private_email: true`:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-8.5-1)
```json
{
"iss": "privaterelay.different.example",
"iat": 1724083200,
"cnf": {
"jwk": {
"kty": "OKP",
"crv": "Ed25519",
"x": "JrQLj5P_89iXES9-vFgrIy29clF9CC_oPPsw3c5D0bs"
}
},
"email": "[email protected]",
"email_verified": true,
"is_private_email": true
}
```
[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-8.5-2)
The browser MAY store the private email address so it can provide it as `directed_email` 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.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-8.5-3)
See [Privacy Considerations](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#privacy-considerations) for privacy analysis of private email addresses.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-8.5-4)
If the issuer cannot process the token request successfully, it MUST return an appropriate HTTP status code with a JSON error response containing an `error` field and optionally an `error_description` field.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-9-1)
### 9.2.
When the request does not include the required `Sec-Fetch-Dest: email-verification` header:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-9.2-1)
**HTTP 400 Bad Request** [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-9.2-2)
```json
{
"error": "invalid_request",
"error_description": "Missing or invalid Sec-Fetch-Dest header"
}
```
[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-9.2-3)
The `error_description` SHOULD specify that the Sec-Fetch-Dest header is missing or invalid.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-9.2-4)
### 9.3.
When the HTTP Message Signature is missing, malformed, or verification fails:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-9.3-1)
**HTTP 400 Bad Request** [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-9.3-2)
```json
{
"error": "invalid_signature",
"error_description": "HTTP Message Signature verification failed"
}
```
[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-9.3-3)
This includes cases where: - The `Signature`, `Signature-Input`, or `Signature-Key` headers are missing - The `Signature-Key` header does not use the `hwk` scheme or is malformed - The signature does not cover the required components - The signature verification fails using the public key from `Signature-Key` - The `created` timestamp is outside the acceptable time window [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-9.3-4)
### 9.4.
When the request lacks valid authentication cookies, contains expired/invalid cookies, or the authenticated user does not have control of the requested email address:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-9.4-1)
**HTTP 401 Unauthorized** [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-9.4-2)
```json
{
"error": "authentication_required",
"error_description": "User must be authenticated and have control of the requested email address"
}
```
[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-9.4-3)
### 9.5.
When the request body is malformed, missing the `email` field, or contains invalid values:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-9.5-1)
**HTTP 400 Bad Request** [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-9.5-2)
```json
{
"error": "invalid_request",
"error_description": "Invalid or malformed request body"
}
```
[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-9.5-3)
### 9.6.
When the request includes `private_email` or `directed_email` but the issuer does not support private email addresses (`private_email_supported` is `false` or absent in metadata):[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-9.6-1)
**HTTP 400 Bad Request** [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-9.6-2)
```json
{
"error": "private_email_not_supported",
"error_description": "This issuer does not support private email addresses"
}
```
[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-9.6-3)
### 9.7.
When the request includes `directed_email` but the private email address is invalid or not linked to the `email` address in the request:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-9.7-1)
**HTTP 400 Bad Request** [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-9.7-2)
```json
{
"error": "invalid_directed_email",
"error_description": "The directed_email is invalid or not linked to this email address"
}
```
[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-9.7-3)
For internal server errors or temporary unavailability:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-9.8-1)
**HTTP 500 Internal Server Error** [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-9.8-2)
```json
{
"error": "server_error",
"error_description": "Temporary server error, please try again later"
}
```
[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-9.8-3)
## 10.
This section analyzes the privacy properties of the Email Verification Protocol, following the guidance in \[\].[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-10-1)
### 10.1.
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.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-10.1-1)
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.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-10.1-2)
### 10.2.
The three-party model (see [Protocol Flow](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#protocol-flow)) 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.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-10.2-1)
Private email addresses prevent RPs from correlating users across sites. Additional benefits:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-10.3-1)
**Protection from data breaches**: If an RP suffers a data breach, only the private email is exposed—not the user's primary email address.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-10.3-2)
**Protection from unwanted email**: Because the issuer controls private email routing, users can revoke or filter mail to specific addresses without affecting their primary inbox.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-10.3-3)
### 10.4.
The issuer learns certain information through the protocol:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-10.4-1)
1. **Email addresses**: 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.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-10.4-2.1.1)
2. **Verification requests**: The issuer sees that verification was requested but does not learn which RP requested it (maintained by the three-party model).[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-10.4-2.2.1)
3. **Private email mappings**: When generating private emails, the issuer stores mappings between private addresses and user email addresses for mail routing.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-10.4-2.3.1)
4. **Email traffic**: When RPs send email to private addresses, the issuer (operating the relay) learns about those communications.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-10.4-2.4.1)
### 10.5.
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.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-10.5-1)
### 10.6.
The browser MAY store the private email address per RP origin to enable account continuity by passing it as `directed_email` in future requests. This is analogous to how browsers store usernames and passwords for sites.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-10.6-1)
## 11.
### 11.1.
The use of HTTP Message Signatures (\[\]) provides several security benefits:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-11.1-1)
1. **Request Integrity**: The signature covers the HTTP method, authority, path, and cookies, preventing tampering with any of these components.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-11.1-2.1.1)
2. **Cookie Binding**: By including the `cookie` component in the signature, the browser's authentication cookies are cryptographically bound to the specific request, preventing cookie injection or manipulation attacks.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-11.1-2.2.1)
3. **Replay Protection**: The `created` timestamp in the `Signature-Input` header is verified to be within 60 seconds, preventing replay attacks.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-11.1-2.3.1)
4. **Public Key Binding**: The browser's public key transmitted via the `Signature-Key` header with the `hwk` scheme is bound to the request signature, ensuring the issuer knows which public key to include in the EVT's `cnf` claim.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-11.1-2.4.1)
### 11.2.
The `hwk` (Header Web Key) scheme provides:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-11.2-1)
1. **Self-Contained Key Distribution**: The public key is transmitted inline, eliminating the need for a separate key lookup or registration process.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-11.2-2.1.1)
2. **Pseudonymity**: The browser does not need to identify itself - the key serves as a pseudonymous identifier for the request.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-11.2-2.2.1)
3. **Ephemeral Keys**: The browser generates fresh key pairs for each verification flow, limiting the correlation potential across different verification attempts.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-11.2-2.3.1)
### 11.3.
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:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-11.3-1)
1. **Build email lists**: Probe many addresses to identify valid ones for spam targeting.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-11.3-2.1)
2. **Account enumeration**: Determine which email addresses have accounts at specific issuers.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-11.3-2.2)
#### 11.3.2.
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.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-11.3.2-1)
Issuers SHOULD mitigate timing attacks using techniques such as:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-11.3.2-2)
- **Uniform code paths**: Execute the same operations (database lookups, cryptographic operations) regardless of whether the email exists, avoiding early returns that skip processing steps.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-11.3.2-3.1)
- **Response delay normalization**: Add delays to normalize response times across all error conditions to a consistent baseline.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-11.3.2-3.2)
#### 11.3.3.
- **User interaction required**: The browser API requires user gesture and consent before initiating verification, preventing automated probing from browsers.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-11.3.3-1.1)
- **Rate limiting**: Issuers SHOULD rate-limit requests per IP address to slow down probing attempts from any client.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-11.3.3-1.2)
- **Sec-Fetch-Dest verification**: The required `Sec-Fetch-Dest: email-verification` header provides a signal that the request originates from a browser, though this can be spoofed by non-browser clients.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-11.3.3-1.3)
- **Same information as email OTP**: 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.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-11.3.3-1.4)
Issuers SHOULD implement appropriate rate limiting and abuse detection.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-11.3.3-2)
## 12.
### 12.1.
The WebOTP API and `autocomplete="one-time-code"` 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:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-12.1-1)
**SMS is a mobile OS feature; email is application-layer** [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-12.1-2)
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.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-12.1-3)
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.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-12.1-4)
**SMS verification is mobile; email verification spans platforms** [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-12.1-5)
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.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-12.1-6)
**SMS senders are aggregators; email senders are RPs** [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-12.1-7)
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.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-12.1-8)
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.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-12.1-9)
### 12.2.
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.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-12.2-1)
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.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-12.2-2)
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.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-12.2-3)
### 12.3.
The EVT uses the SD-JWT structure (specifically, the key binding capability from SD-JWT+KB) rather than a plain JWT. This choice provides:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-12.3-1)
1. **Key Binding**: The `~` 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.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-12.3-2.1.1)
2. **Library Support**: SD-JWT libraries already exist and can parse EVTs, reducing implementation burden for RPs.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-12.3-2.2.1)
3. **Extensibility**: While EVP does not currently use selective disclosure, the SD-JWT structure allows future extensions without changing the token format.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-12.3-2.3.1)
### 12.4.
The mail domain delegates email verification to an issuer via a DNS TXT record rather than a `.well-known` file. This choice aligns with how email infrastructure already works:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-12.4-1)
1. **Email domains often lack web hosting**: Many users have personal domains used only for email. Requiring a web server to host a `.well-known` file would create a barrier to adoption.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-12.4-2.1.1)
2. **Apex domain challenges**: Email domains are typically apex domains (e.g., `example.com`), which do not support CNAME records. Hosting a web site on an apex domain requires additional infrastructure.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-12.4-2.2.1)
3. **Familiar tooling**: Domain owners already manage DNS records for email (MX, SPF, DKIM, DMARC). Adding another TXT record fits existing workflows.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-12.4-2.3.1)
### 12.5.
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:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-12.5-1)
1. **Key rotation**: DKIM keys are rarely rotated in practice. JWKS rotation is common in OIDC deployments and follows established patterns.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-12.5-2.1.1)
2. **Algorithm flexibility**: JWKS supports multiple key types and algorithms. DKIM key distribution was designed for a specific use case.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-12.5-2.2.1)
3. **Operational familiarity**: Developers implementing EVP are likely familiar with JWKS from OAuth/OIDC work.[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-12.5-2.3.1)
### 12.6.
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:[](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-12.6-1)
1. **Standards-Based**: \[\] is a published standard for signing HTTP messages, providing better interoperability [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-12.6-2.1)
2. **Cookie Binding**: HTTP Message Signatures can directly sign the `cookie` header, providing stronger binding between authentication cookies and the request [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-12.6-2.2)
3. **Flexibility**: The signature can cover any HTTP components, making it easier to add additional protections in the future [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-12.6-2.3)
4. **Simpler Key Distribution**: The Signature-Key header provides a standardized way to distribute keys inline with the request [](https://dickhardt.github.io/email-verification/draft-hardt-email-verification.html#section-12.6-2.4)