This page is fully verified, except for the page itself
You come home and the kitchen isn't right.
Cleaner, maybe. Your mug sits exactly where you left it, but the light through the window is off. You stand there holding the cup, turning slowly. Everything is where it should be and none of it is yours. You take a sip, trying to figure out what isn't right, and realise you don't know what you're drinking.
It's warm, but it isn't tea.
That's a domain hijack. Right door. Identical furniture. But not your place.
On a screen it's quieter. You type your bank's address, or click the bookmark you've used for years. The page loads. The padlock is there. The logo, the layout, the wobble on the login button: all correct, because they copied all of it. And you type your password into it, giving it to the hacker.
For a bank, that's a breach. Bad but survivable; the real ledger is on their machines, and they can hand your money back. For local-first and p2p apps it's an unrecoverable disaster. The whole point of what a lot of us are building is to push keys and data all the way down to the user, so the company can't betray you. Remove the company that could betray you and you also remove the one that could save you. A hijack here doesn't leak a session. It takes the keys, all the data, and there's no one who can make it right.
There's a technical problem underneath it: you can do every piece of web security correctly (valid certificate, perfect TLS, integrity-checked scripts) and none of it matters, because we don't control the domain registrar. The attacker tricks it, changes the domain name to point to their server, copies everything so it looks right, injects their code, and lets the victims open the door themselves.
Your browser does a bunch of security checks, but never asks: is this the right server, or just one that answers to the name?
A hijack is also only one way in. A compromised build pipeline, a poisoned dependency, a CDN serving something extra, an insider pushing one commit. They all end the same way, with code running in your browser that the developers never wrote. Which is strange, because your browser is perfectly capable of refusing that. Extension code is hash-verified before it runs. So is a mobile app.
But the page sitting right next to the extension, in the same browser, on the same engine, gets no such check?
Suspicious.
The blind spot
The closest thing we have is Subresource Integrity (SRI). The page lists a hash for every script, stylesheet, and image it pulls in; the browser checks each fetch and refuses anything that doesn't match. A compromised CDN delivers tampered JavaScript, the hash fails, the code never runs.
It works. But it verifies every ingredient the page brings in, not the page itself. The kitchen is the root HTML, the document listing all those hashes, and it has no hash check of its own.
Why not? Where would you even put a hash of the page?
You can't put the hash in the page itself, so it has to go somewhere the attacker can't change. The obvious candidate is DNS, the out-of-band layer that already says what this name means. But it can't go there either. A hijacker who can redirect your domain can just swap your real hash for their fake one.
DNSSEC has the same problem. It lets a domain sign its own records, but the trust chain runs back through the same registrars that get hijacked, so the attacker just signs as you.
So we need some way to check that the records themselves really come from the people who own the site.
The boring fix
Do what SRI does, but for the DNS records themselves. Hash them the first time, check against it after. The browser keeps a fingerprint of the place. If it ever resolves to something that doesn't match, get out. Don't open any drawers.
This is Trust On First Use, and we lean on it everywhere without calling it anything. SSH hands you a host key the first time you connect; after that, a mismatch screams at you. Signal does the same with safety numbers.
A browser extension can do this in two layers. Pin the DNS records on first sight, check them on every visit after. And use the same records to carry a content hash for the key pages, so the documents that need security get SRI protection down to their root.
DNS pinning is possible
The hard part is people. False positives train people to click through, and DNS records legitimately move all the time: CDNs, CNAME chains, cache TTLs, resolver disagreement.
So I've been measuring it. Some domains rotate records every minute as a matter of course. Others haven't changed in years. And they don't scatter. They cluster, by what kind of thing they are. Global consumer sites behave one way, banks another, messaging services another, and the static hosts that local-first apps deploy to are the most stable of the lot. Pinning policy can be written per cluster rather than per domain, which is what makes the false-positive rate survivable. Nothing in that requires the website operator to do anything, or know I exist. The research is in the policy, not the primitive.
Transparency logs
The first-visit gap, when there's nothing to compare against yet, is where a transparency log comes in. A public, append-only record of what a domain should serve, so you know if anything's changed before you go inside.
The pattern already runs under every HTTPS connection you make. Your browser trusts a few hundred Certificate Authorities to vouch that a domain is who it says it is. In 2011 a Dutch one, DigiNotar, was broken into and used to issue a fraudulent certificate for Google, which was then used to read the mail of hundreds of thousands of people in Iran. One compromised authority, anywhere, was enough to impersonate anyone. Certificate Transparency was Google's answer: every certificate a CA issues gets published in an append-only log that anyone can audit, so a forged one can't be issued quietly. It's live, it's real, and it caught things.
Except it made Google the gatekeeper. Anyone can run a log. Cloudflare runs one, Let's Encrypt runs one. But a log is only worth running if Chrome accepts it, and Chrome accepts the logs on a list Google's own programme maintains. The mechanism that was supposed to make that unnecessary, clients gossiping with each other to confirm they're all being shown the same log, never shipped. So the check that no single party controls the record was replaced by a single party keeping the list.
We took out the gatekeeper, then put it back with a changelog.
I'd been circling this for a while
For a year I'd been building zKal: ZK proofs that let people prove things like attendance, eligibility or reputation, without the data ever leaving their device. The EUDI wallet's privacy promises, except actually kept. I've since parked it, but the cryptography we developed lives on.
I worked on it with Mircea, a Metamask core dev who also built Veramo, an open source library that makes identity data portable so people aren't locked to one provider. Two timezones apart, Jitsi call, Excalidraw open, our own shorthand for crypto operations scrawled across the canvas.
We kept figuring out things that people said were impossible. Full key custody and rotation with no seed phrase. Restoration on a device with no server. Moving data between wallets with no reconfiguration.
I always needed a concrete use case to design against. I'd run scenarios and permutations, looking for simple designs. Mircea was usually three moves ahead, designing for a gotcha I couldn't see yet.
He kept steering me back to one thing. I'd hand him a clean, practical answer; he'd point at something and scrunch his forehead. "There! Domain-rooted security again." And send me back.
Sometimes it felt like he was Mr. Miyagi-ing me.
DNS hijacks felt out of scope. Until Berlin.
Berlin
I flew in, and we spent the day walking the parks, jumping between topics. I kept circling something that bugged me. Some explorations would dead-end at the same wall: "that's why everyone ships wallets as browser extensions."
I didn't want to accept it.
Extensions are clunky, their app stores are sketchy, and it felt like we keep dumping a whole app into a sandbox just because the sandbox was the only thing with verified code. I wanted to know where the actual security boundaries were, between the app, the wallet, the storage, instead of shoving everything behind one wall.
In your browser, extension code is hash-verified. The actual webpage sitting next to it isn't. Integrity for extensions, integrity for subresources, and nothing for the page that contains them.
I was already staring at the answer without seeing it: the verification chain I wanted could be bootstrapped from an extension. One small check, watching the page from its secure extension sandbox. Does this domain, this page, hash to what it should?
Mircea's eyes lit up. "That's actually a great idea. It's something that should really be built into browsers."
I was almost annoyed. I remember thinking: I wish the browser people had already sorted this out, so I could get back to the fun wallet stuff.
Then I went looking
So I built it, an extension that pins DNS records, and then worked my way backwards through the technical question I'd skipped.
But there was a bigger question I couldn't shake. If this should be a native browser feature, how does that even happen?
I know people on W3C committees. I've watched the sausage get made, and I've read the autopsies of how standards bodies consistently ruin good proposals, because the big players in the room all have a business model to protect.
So I looked at Let's Encrypt, the one org that got past both the standards process and the certificate cartel and changed what the whole web does by default. They now run their own transparency logs on Sunlight, a deliberately simple CT log design, and Sigsum, a minimal transparency log built so that trust doesn't accumulate anywhere in particular.
Those led me, ironically, to Google's transparency conference talks, where I discovered WAICT and WEBCAT.
WAICT, Web Application Integrity, Consistency and Transparency, is a draft specification with Cloudflare and Mozilla in it: hash every file a site serves into a manifest, commit the manifest to a transparency log witnessed by independent parties, have the browser refuse anything that doesn't match. It's a good draft, and we've just established what the big players do to those, so I'm not holding my breath. But Cloudflare describe the goal as app-store-grade security for web applications without a central gatekeeper, which is a sentence I could have written and didn't.
WEBCAT comes from the Freedom of the Press Foundation, and it's further along. A working extension, built on Sigsum, that verifies a site's manifest carries signatures from developers the site owner has named. Their client is the website operator; their users are journalists and whistleblowers, people who will do real work for real safety and who are targeted by people with budgets. Their enrolment even includes a cooldown before changes take effect, so a stolen domain can't be quietly re-enrolled. It's amazing work, and they'd already found most of the answers I was looking for.
All these assume the operator opts in. That's the part I'm wrestling with. It's the right assumption for a newsroom and the wrong one for the bank my mother uses, which will never enrol in anything. Protecting someone whose sites will never opt in lets me relax constraints the others can't. I can let domain lookups leak, for instance, since they leak through DNS anyway. (Besides, CONIKS gives us a way to make those lookups private later.) Operator-first and end-user-first: same wall, different sides.
So why isn't this solved
It has been tried. For over a decade. I watched a lot of it.
The DNS pinning primitive I'm making has a famous, and crazy, granduncle. HPKP, HTTP Public Key Pinning, shipped in Chrome in 2015. A site could pin its keys and the browser would refuse anything else.
Setting it up correctly was hard. You had to pin a backup key you weren't using yet, keep it safe for as long as the policy lasted, and get the rotation windows to line up with certificates that expire on their own schedule. Get any of that wrong and the browser does exactly what you told it to: refuse the site. With nothing you can change from the server, because the server is the thing being refused.
The protocol itself wasn't well designed, and website operators kept footgunning themselves. They pinned themselves off their own sites. Smashing Magazine did it to themselves with a year-long policy. And it worked as an attack, too. Get into a server briefly, pin keys the owner doesn't have, and hold the site to ransom.
Some people argued for fixing it: better tooling, safer defaults, a way to recover. Chris Palmer's intent to deprecate went out in October 2017 instead, and pointed operators at Certificate Transparency, which didn't really solve the same problem but was considered safer because you could recover from your own mistakes. Chrome deprecated HPKP in 2018 and removed it in 2019. Firefox never finished shipping it; Safari and Edge never started.
Same plot as before. The mechanism that let a site defend itself was removed. What replaced it was Certificate Transparency, enforced by browser policy that Google controls.
Browsers are dead, long live the browsers
To put anything new in a browser you needed the standards bodies and the big vendors to play along, and by then it was clear enough what that game does to anything threatening their app store or advertising revenue. They don't fix these problems because they sell the app store as the fix.
So if you wanted to change what a browser checks, you had to build your own browser.
Dat and Beaker gave you a peer-to-peer web with a browser you could install and a data model that travelled with you. Elegant, self-contained, still admired. Paul Frazee wrote it, and before that the first Secure Scuttlebutt client. When he stepped back he was clear about where it broke:
"The challenge was that Beaker apps had no backend. If you want to build 100% client-side SPAs, you need something akin to a Firebase: a toolkit of databases, users/identity, and networking. We took a lot of shots at building that, but struggled to create APIs which matched the browser's security and page-based runtime model."
There's domain-rooted security again. Mircea's spiritual ancestor, speaking.
Paul's the CTO at Bluesky now, where the answer is peer-to-peer structures on a federated network: portability kept, but with servers instead of peer to peer.
Scuttlebutt built a whole social world that worked off-grid. Gossip protocols, cryptographic identities, the works. Rabble ran Planetary on it, then moved to Nostr, and his account of why is worth reading whole:
"I was never building tools just for solarpunk anarchists."
Which meant building for communities where he wouldn't feel at home. He lists what came out of people hitting the same limits: earthstar, p2panda, moderator, chatternet, nostr, farcaster, and the at_protocol.
remoteStorage tried to prise your data loose from the app that held it, so an app could be swapped without losing the notes you took in it. The pattern was good enough that Solid picked up the same thread years later. It had Tim Berners-Lee, Inventor Of The World Wide Web, behind it. But almost no app adoption.
And people are still building new browsers. Peersky and a handful of others have some of what I've been describing baked in from the ground up. But decentralization is not a word that makes a normal person change browsers, and so far every one of them has found the same small, technical, already-convinced audience.
None of these died of bad cryptography. Some are really well designed and are still running today.
So there's a different problem.
The hubris
"I love these people, they're a team who can force silicon, lithium, and code to bend political reality. But I cannot stand the compromises we have made in the name of this hubris."
That's Cade Diehm, the same Cade who helped me with zKal. He was on the team in Hawai'i in January 2015 while Signal was being finished, arguing that phone numbers were the wrong identifier: "This app is going to be used by people escaping intimate violence. Their abusers know their phone numbers. You need to let them in with pseudonyms." He describes Signal's founder, Moxie Marlinspike, rolling the interface back with a shrug, insisting instead that address-book discovery work as smooth as butter. It was the first time he met someone who could "look at the risk and simply not see it".
Cade was right, and after a decade it's obvious. Usernames finally arrived in Signal in 2024. Even the people putting real cryptography in ordinary people's hands can miss what their compromises cost, because the people who pay aren't in the room. That's the hubris I'm trying to avoid.
Meanwhile, on your phone
You stand in your kitchen, mug in hand, and ask if it's yours. It tells you yes. You're comforted, somehow, even though something in you knows your real kitchen doesn't talk.
Except this time the kitchen isn't a website. It's your phone. And the one who's tricking you isn't a hijacker, it's Google itself. Plot twist!
And Google is now requiring every Android developer to register with them personally, with government ID and twenty-five dollars, before their apps can install at all. It starts in September, in four countries, through the app stores. By 2027 it covers all apps on certified devices, which is nearly every Android phone on earth, outside China.
Same move, third time. Take out the thing that let people check for themselves, then keep the list of who's allowed to be checked. I've stopped being surprised by it, and I don't think that's an improvement.
What's actually possible
You write an app. You sign it with your own key. You send it to a friend, or put it on a website, and their browser checks it against a public log that nobody owns. The same integrity guarantee an app store gives, but no need for the store.
Who made it, who has looked at it and what they found, travelling with the app instead of sitting in one company's database. No permission, from anyone.
The pieces exist. Sunlight and Sigsum are running. WAICT is being drafted by people who ship browsers. WEBCAT works today for operators who enrol. The pinning layer needs nobody at all. That's what I've built, and it runs against domains that have never heard of it.
What I'm working on now is the rest: reputation without a five-star rating database, where quiet signals like usage and drop-off tell the real story. Key theft and recovery, where sharding and a delay before a handover takes effect buy the time to catch attackers. And false positives low enough that a warning still makes you stop the tenth time you see it.
What are the right questions?
Which brings me back to Cade in that Signal mansion. Ten years ago you had to guess which trade you could afford. Surveillance was on the horizon, but not yet the shape of everything.
And it wasn't only founders guessing. It was whole ecosystems: everyone who built on top, ran the servers, wrote the clients, onboarded their friends, then watched for a decade. Some of those calls kept a project among people who were already convinced. Others put something sold as safety into hands that later had to pay for where it failed.
Those are lessons I want to learn from.
The big questions I'm left with aren't just technical. They're more like: what is adoption even supposed to look like for something whose whole selling point is that nobody owns it? And what to do about the fact that every institution positioned to help has a reason not to?