Confirming an email address has worked the same way for about twenty years: we mail you a link, you go find it, you click it, you come back. For a small share of Short.io signups, that round-trip no longer happens — the browser hands us a signed proof along with the form.
Why we bothered
We want onboarding to be as short as we can make it, and email verification is the step that resists that hardest. We also can’t drop it: a link shortener with unverified signups drowns in phishing and spam almost immediately, and the confirmation email is a large part of what keeps that under control.
So the step stays, but nothing about it is convenient. “Leave this page, open your email client, find our message, come back” is the worst moment in the flow and the exact opposite of what the rest of signup is trying to do. Chrome’s Email Verification Protocol (EVP) is the first proposal we’ve seen that removes the interruption without removing the check — which is why we’d rather help it become a standard than wait and see whether it does.
Signing up without the round-trip
EVP is in origin trial. Instead of mailing a link and waiting, the browser collects a signed assertion that the user controls the address, and hands it over with the form.
It defines four roles, and two of them are easy to run together by mistake. The mailbox
provider owns the email domain. The issuer is the service that manages the account and
signs assertions about it, which is not necessarily the same host: for Gmail the mailbox domain
is gmail.com, and it delegates by DNS to an issuer at accounts.google.com. The holder
is Chrome on the user’s device, and the verifier is the site collecting the address — the
only role we implement.
That delegation is the whole trust model in one hop. We look up the mailbox domain, the domain
names its issuer, and the issuer’s keys are the only thing we’ll accept a signature from. Today
_email-verification.gmail.com answers iss=accounts.google.com, and that issuer’s metadata
points its key set at a third host, verifiablecredentials-pa.googleapis.com.
On our side the signup form grows a hidden input marked
autocomplete="email-verification-token", carrying a nonce we minted server-side in its
nonce attribute. If the provider participates, Chrome fills that input’s value on submit and
we verify it.
The email field itself also has to be marked autocomplete="email", so the browser knows which
value the token is about. Ours said autocomplete="username" — a reasonable thing for a
credential-ish field, and what a password manager wants to see — and with that in place EVP
never engaged at all. Chrome doesn’t report why it skipped a field, so check this first rather
than fifth.
All of it is progressive enhancement: if anything doesn’t cooperate, we fall back to the email-link flow that was already there. A failed verification is never an HTTP error and never blocks a signup.
What we actually verify
Chrome fills the field with an SD-JWT with key binding — a ~-delimited concatenation:
<issuer-signed EVT JWT>~<disclosure>~…~<disclosure>~<key-binding JWT>
The nonce is single-use, consumed with an atomic get-and-delete before any signature work, so
two concurrent submissions of the same token can’t both get past it. Then comes issuer
discovery, which is unusual in that nothing is preconfigured: a DNS TXT record at
_email-verification.<mailbox-domain> names the issuer host,
/.well-known/email-verification on that host gives a jwks_uri, and the token is verified
against the key set found there.

Discovery is also where EVP hands you an SSRF hole, so harden it before you ship. The
issuer host arrives in a DNS TXT record, which means anyone who signs up with a domain they
control gets to choose a URL our server will fetch. We got lucky on timing: we already run a
small proxy service dedicated to fetching untrusted URLs, built for link previews, so issuer
metadata and key-set fetches go through it instead of a direct fetch. It range-checks the
host at connect time rather than before — which is what closes the DNS-rebinding window —
refuses redirects, and caps the response size, with egress network policy as the backstop
underneath.
Two of the remaining checks are about not trusting the token’s own claims. The issuer is
derived from the mailbox domain of the address that was submitted, never from an iss claim
inside the token — otherwise a token could nominate whoever it liked as its own authority. And
the audience is checked against origins from our own configuration, never against a request
header, because headers are attacker-controlled.
The subtle one is selective disclosure. An SD-JWT needn’t put every claim in the signed
payload. It can sign only a digest of a claim and send the claim itself alongside, as one of
those ~-delimited pieces that the holder appends at presentation time. The verifier is meant
to hash each piece it receives and confirm the result appears in the issuer’s signed digest
list. Skip that and merge the pieces as they arrive, and a holder can append their own
[salt, "email", "[email protected]"] to overwrite the address the issuer actually signed.
Chrome sends email as a direct claim today, so nothing exercises this — which is exactly why
it would sit there quietly until some provider changed shape.

sd_hash is easy to mistake for the defence here, and isn’t one. What it does is bind the
key-binding JWT to this exact SD-JWT and this exact set of selected disclosures, so nobody can
add, drop or alter a piece after the holder signed — the presentation is sealed. What it can’t
tell you is whether the issuer ever authorised any given disclosure, because it’s computed
over whatever the holder chose to present. Sealing the envelope and vouching for the contents
are different jobs; only the digest check does the second one.
Where it got interesting
A feature designed to fail quietly failed completely, and nothing noticed. Google’s EVP key
set publishes five Ed25519 keys, none carrying a kid — and EVT headers don’t carry one
either. Our JWT library matched all five, could not choose between them, and threw
JWKSMultipleMatchingKeys, which a catch around signature verification flattened into
“invalid signature”. Every genuine Chrome token was rejected. Signups kept working, users kept
getting confirmation emails, no error rate moved.
The fix walks the candidate keys the error carries, swallowing only genuine signature mismatches so the loop can’t become an accept-anything oracle. Against a real captured token, candidate #4 matched. The tests hadn’t caught it because our test double handed the verifier a single bare key, so key selection — the thing that broke — was never exercised at all.
Trying it yourself
We’ve enabled EVP for 0.5% of devices, which is a more honest way to put it than 0.5% of signups. There’s no feature-flag infrastructure in the dashboard and signup is pre-auth, so there’s no user id to bucket on; the gate is a client-side hash of PostHog’s anonymous device id. Because that hash is deterministic, a given device is always on the same side of it — so a device either has EVP on every visit or never sees it at all, rather than each signup attempt rolling the dice.
To put yourself in the bucket regardless, run this in the console on the site and reload the signup page:
localStorage.setItem("evp_force", "1")
The flag alone won’t show you anything, though, and this is where most of our own false alarms came from. Four things have to be true at once:
- Chrome, since the origin trial runs there through November 2026.
- Signed in to a participating provider in that same Chrome profile — today that means being logged into the Google Account for the Gmail address you’re testing with. A different profile, or a signed-out one, gets you nothing.
- The address picked from the autofill dropdown, not typed. This is the one that catches people. Chrome only issues a token for an address the user selected from autofill or autocomplete; typing the same address by hand produces no token at all, and support for manually typed addresses is listed as future work. So a hand-typed signup falling back to the email link is EVP working as specified, not EVP broken.
- A fresh signup, since the flow is pre-auth and there’s nothing to trigger from an existing account’s settings.
One more, learned the hard way: an installed 1Password extension stops the flow working. Our reading is that it takes over the email field’s dropdown, so the address gets filled by 1Password rather than selected from Chrome’s own autofill — and Chrome’s autofill selection is the thing that triggers issuance. Disable it for the origin, or test in a clean profile. It fails exactly like a broken rollout, which cost us more time than it should have.
If it works you’ll see nothing at all: no extra step, no new screen, the account already verified. The registration email still arrives, because we send it before verification comes back — you just never need to click it, and its token has already been cleared. Sending a message we’ve decided you don’t need is the obvious next thing to fix.
The general lesson isn’t about the cryptography; that part is specified. It’s that we built something whose entire purpose is to fail invisibly, then shipped it with no way to tell “failing invisibly” apart from “not running at all”.