Docs · DANE
TLS with no certificate authority in it.
No CA can issue a certificate for a Handshake name — there is no registrar to validate it against. DANE replaces that whole layer: the fingerprint of the certificate is published in your name's own signed zone, anchored on-chain by a record only you can commit. A client that checks it trusts nobody but you.
The pin
One record shape, one profile, and nothing else is emitted or accepted:
_443._tcp.<label> TLSA 3 1 1 <sha256 of the cert's SubjectPublicKeyInfo>
_443._tcp TLSA 3 1 1 <...> # at the zone apex
3 1 1 is DANE-EE / SPKI / SHA-256: the certificate is
trusted because it is this key, not because someone vouched for
it. Because the pin is over the public key rather than the whole
certificate, renewing the certificate without rotating the key leaves the
pin valid.
The certificate on a Handshake name is self-signed — EC P-256,
CN=<name>, with the name and its wildcard as SANs.
There is nothing for a CA to do and no CA is asked.
# what the pin should be, from a certificate
openssl x509 -in cert.pem -noout -pubkey \
| openssl pkey -pubin -outform DER \
| openssl dgst -sha256
Who signs the DS
DANE only means anything if the TLSA record itself is provable, and that
needs a DNSSEC chain that terminates in something nobody but the name's
owner can write. On Handshake that terminus is the on-chain
DS record.
| Piece | Who | Notes |
|---|---|---|
| Certificate and private key | HNS.ONE | Generated once and reused; the key is never rotated on renewal |
| TLSA record in the zone | HNS.ONE | Written only after the browser gate passes |
| Zone signing key and RRSIGs | HNS.ONE | One combined key per zone, algorithm 13, flags 257. It never leaves the operator's machine — the edge terminates TLS but cannot sign a zone |
The DS record on-chain | You | Digest type 2. We hand you the four fields; only your key can commit them |
The split is on wallet ownership, and it fails safe: anything other than a clean, positive "this wallet holds the name" is treated as external and produces a handoff rather than a signature. We do not assume we may anchor a name we cannot demonstrably prove we hold.
Enable it and read its state:
POST https://app.hns.one/api/dane/enable {"name": "yourname", "label": "www"}
GET https://app.hns.one/api/dane/status?name=yourname
For a name you hold, the response carries the exact
commit_on_chain_ds object to publish, plus the TLSA we
serve. Nothing is signed or published if the gate below rejects it.
The browser gate
Before a pin is signed and published, the exact validator that ships in Wildroot is run against it, imported as a module rather than reimplemented. If it rejects the chain, nothing is written — no partial state, no "publish and hope".
The reasoning is that a DANE pin is a promise about how a client will behave. Publishing one the client would refuse is worse than publishing none: it takes the name down for exactly the clients that check.
What a client actually does
- Wildroot, on a Handshake name — validates, and fails closed
The socket is opened with WebPKI verification off, because the
trust is DANE, not a CA. On mismatch the socket is destroyed and the
page becomes
HTTP 526 Certificate not trusted. There is no CA fallback anywhere in that path. Connections carrying a DANE verdict are never pooled, so a keep-alive reuse cannot skip the check. - No pin published — plain HTTP, and only on a proved absence
If the zone gives an authoritative "no such record", the client
proceeds unencrypted and marks it untrusted. A SERVFAIL or a refused
query is not an absence: those fail closed with
HTTP 525 No secure route. - Unsupported TLSA profile — treated as a mismatchA zone that publishes a profile the client cannot check has still stated a policy. Ignoring it would let that policy be silently discarded.
- Every other browser — no DANE at allChrome, Firefox and
Safari do not implement it and will not. That is why the
<name>.hns.onegateway exists with an ordinary CA certificate, and why the two paths are two different TLS identities for the same page.
DoH fallback. When the client resolves over DoH instead of its own node — which includes whenever the private-lookup mode is on — it cannot prove a pin, so the address is served over plain HTTP and marked untrusted rather than being silently upgraded.
The Android build. Its DANE support is accept-on-match only: a CA-valid certificate that mismatches the pin is currently accepted, and an HTTPS failure downgrades rather than refusing. Full fail-closed behaviour is not shipped there.
Rotation
The primary defence is not rotating. Certificate renewal reuses the existing private key, so the certificate changes and the pin does not. That has already been exercised: our gateway certificate was reissued with a different SAN set and an unchanged public key, and every pin stayed valid.
Where a key genuinely has to change, the correct procedure is additive — publish the new pin alongside the old, let it propagate, then serve the new certificate, then remove the old pin. A client mid-propagation always matches one of the two.
A certificate-renewal hook checks, on every renewal, that the published pins still match the certificate about to be installed, over DNSSEC-validated lookups. A lookup it cannot complete is reported as "could not be verified", never as "the key moved" — the distinction matters, because acting on the wrong one publishes a wrong pin.
Enabling DANE on a name through the panel replaces the TLSA at that owner name rather than adding beside it. Today that is masked by key reuse — the pin does not move — but the code has no dual-pin path, so a genuine key change on a Handshake name is a hard cutover with a propagation window where checking clients fail.
What is pinned right now
| Name | State |
|---|---|
14898 |
DS on-chain, TLSA at the apex and three labels, matching the served self-signed certificate |
10103 |
DS on-chain, TLSA on two labels |
w3, 0xme |
DS on-chain, DNSSEC only — no TLSA published |
hns.one, mail, odoh,
query |
Pinned on the ICANN side and validating |
pds.hns.one |
Not pinned. It moved to its own host with its own certificate and the pin has not been re-issued |
Our signer has never been checked by an independent validator. Signatures are verified by the same code that produces them, which catches implementation bugs but not a shared misreading of the spec. Treat the DNSSEC half as unvalidated until an outside validator has passed it.
Enabling DANE from the public plane is not proven. The endpoints exist and the branch that hands off to a self-custodying owner works, but the signing step there reads the operator machine's zone file rather than the tenant one and will refuse rather than sign. Ask us and it gets done on the operator side.
A name delegated elsewhere with a foreign DS cannot be fixed by
us. If your name carries a DS anchoring a key we do
not hold, every validating resolver fails closed on it and no amount of
serving on our side helps. Remove the stale DS on-chain
first.