Tamper-proof domains

The nicest part of custom domains on Portrait was that they were not just a settings field in a database. The link between a domain and a Portrait could be reconstructed from public DNS plus an Ethereum signature.

The code called the field tld, but what we were really storing was the full root domain, like ryanshahine.me. The proof was simple: if you wanted a domain to resolve to a Portrait, you had to control the DNS for that domain and the wallet that owned the Portrait ID.


Sign the root domain

The frontend parsed the user input, reduced it to the root domain, and asked the wallet to sign one compact message: {rootDomain}={portraitId}.

That made the proof legible and portable. You could recover the signer from a normal Ethereum signed message and compare it to the owner of the Portrait ID onchain. No server secret was needed, and no private backend state was part of the proof.

const rootDomain = getRootDomain(domain)
const message = `${rootDomain}=${portraitId}`
signMessage({ message })
typescript

Publish the proof in DNS

After signing, the frontend showed the user DNS instructions. Point the domain at the Portrait proxy IP with an A record and publish a TXT record containing the portraitId and the signature.

The proof was not hidden inside Portrait. It lived in public DNS, where anyone could query it.

A @ 18.214.196.199
TXT @ "portraitId=47 signature=0xabc..."
txt

Verification was public

Before persisting a domain, the backend verified the wallet signature against the exact message ${tld}=${portraitId}, confirmed the A record pointed at the proxy, and confirmed the TXT record contained the matching portraitId and signature. It also rejected domains already claimed by another Portrait.

But the backend was not the source of truth. Every piece of the proof — the wallet signature, the DNS records, the Portrait ID owner onchain — was independently inspectable. The database just cached the result after the public checks passed.


The proxy served the domain

The last mile lived in portrait-proxy-domain-service. A small proxy on a static IP terminated TLS with Let's Encrypt, read the incoming host header, resolved the domain to a Portrait, and served the right page. It also normalised www. so the root domain stayed canonical.

That is the full loop. The wallet proved ownership, DNS published the binding, the backend checked both, and the proxy made the domain live.

request ---->
site <----
1/6
>request=Host
.dns=A AAAA TXT
.proof=portraitId sig
.verify=domain bind
.name=registry
.site=return

Why call it tamper-proof

It is not tamper-proof in the sense that nothing can ever go wrong. DNS can still be hijacked and registrars can still be compromised.

What this model gives you is tamper-evidence and independent verification. Portrait could not silently bind a domain to the wrong profile without the wallet signature and the DNS record matching. The proof was public, inspectable, and re-runnable by anyone.

Acknowledgements

Written from the actual domain flow in portrait-frontend, portrait-api-backend, and portrait-proxy-domain-service.

Footnotes

  1. The message signed is ${rootDomain}=${portraitId} after normalizing the user input through getRootDomain() in the frontend DomainDialog.

  2. The proxy service was designed for a static Lightsail IP and Let's Encrypt/Greenlock, which is why the A record had to point at a specific machine before the domain could be served securely.