Authorize a domain by proof
See Authorize a domain by proof for what proving domain ownership buys you and why DNS is the root of trust behind it. This page is the full technical reference: the on-chain DNSSEC verifier, the witness-staging protocol, the sithbit domain authorize command tree, and measured compute costs.
sithbit domain authorize <domain> \
( --witness-file <path> | --witness-hex <hex> ) \
[--payer-keypair <path>] \
[--skip-preflight]
This is the proof-carrying sibling of
domain create. Where domain create authorizes a
domain because the delegate signed for it (that delegate-initiated,
delegate-signed path remains as-is), domain authorize
authorizes a domain because the transaction carries a witness — the DNSSEC
RRSIG chain proving the domain’s _solana.authority delegation — that the
on-chain program verifies for itself. No delegate signature is required,
and the submitter need not be anyone special: anyone may submit a proof and
pay for it, because the proof, not the signer, is the authority. Both paths
charge the same authorization fee.
The design rationale — why moving from a signed token to a verifiable proof takes the admin key out of the loop entirely — is the Proving behaviour to the chain design note.
Status: the on-chain verifier is live
The on-chain DNSSEC verifier is implemented and enforced. Given a staged witness buffer,
AuthorizeDomainByProofwalks the real chain-of-trust from the postoffice’s root KSK down to the leaf_solana.authority.<domain>TXT and mints the domain with the proven ed25519 authority — proven end-to-end in thedomain_programtest suite. It verifies all three DNSSEC signature algorithms a real ICANN-anchored chain uses: RSA-2048/SHA-256 (algorithm 8), ECDSA-P256/SHA-256 (algorithm 13), and Ed25519 (algorithm 15) — see How it works.Because a real witness (~2.7–3.1 KiB) exceeds Solana’s ~1232-byte transaction packet, it is staged into a program-owned buffer PDA first via chunked
WriteProofWitnessinstructions, and the buffer is closed and its rent refunded afterward withCloseProofWitness(both detailed below).
How it works
The witness is far too large to ride inside a single instruction, so authorization is a two-step flow against a program-owned buffer, with a third instruction to reclaim the buffer’s rent afterward:
- Stage the witness. The full DNSSEC chain is written into a program-owned
buffer PDA seeded on
[PROOF_WITNESS_SEED, payer, blake3(domain)](see How blake3 hashing works) — a fixed header plus the contiguous witness bytes, capped at 8 KiB — via a series of chunkedWriteProofWitnessinstructions (discriminant 27). Each carries an offset and a chunk of up toMAX_PROOF_WITNESS_CHUNK(900) bytes; the first write allocates the buffer to its declaredtotal_len, and later writes fill it in untilwritten_len == total_len. - Authorize. A single
AuthorizeDomainByProoftransaction then reads the fully-staged buffer and runs the verifier. Because the chain walk is compute-heavy (306–312k CU measured for an all-RSA chain, well past the 200k default — see Measured compute cost), this transaction must prepend aComputeBudgetset-compute-unit-limit instruction. - Reclaim the rent. Once authorization has landed (or the attempt is
abandoned),
CloseProofWitness(discriminant 28) closes the buffer PDA and refunds its full rent to the payer. Only the original payer may close it — the buffer PDA is seeded on the payer, so a different signer derives a different address and cannot reach it — and a partially-written buffer refunds just the same. The CLI emits this automatically after a successfuldomain authorize; for a failed or abandoned attempt, rundomain authorize --close-witnessyourself.
The authorization fee
A verified proof pays the same delegate-tuned domain-authorization fee
domain create charges — DOMAIN_AUTHORIZATION_FEE_LAMPORTS, 0.01 SOL by
default, tunable via SetDomainFee (see
Economics) — debited from the payer
and credited to the postoffice once, at the authorize step, after the chain
walk succeeds. The two authorization paths cost the same, so nobody routes
around the fee by choosing one path over the other. A failed proof charges
nothing beyond transaction fees — the domain is not created and no fee moves
(the staged buffer’s rent stays reclaimable via
domain authorize --close-witness).
The chain-of-trust walk
The verifier (program_common::dnssec::walk_chain_with) anchors at the
postoffice’s root_ksk fingerprint: a DS-style SHA-256 digest of
owner ‖ DNSKEY (RFC 4509) for some key in the root DNSKEY set, which must
self-sign that set. From there it walks each delegation down the tree — for
every link it canonicalizes the RRSIG (RFC 4034 §6), matches the DS digest
root → TLD → registrable zone, and checks the signature’s validity window
against the cluster clock — and finally verifies the leaf
_solana.authority.<domain> TXT RRSIG. The ed25519 public key that TXT
publishes (base58) becomes the new domain’s authority.
Three signature algorithms
A real ICANN-anchored chain mixes signature algorithms: the root and TLD zones sign with RSA-2048/SHA-256 (DNSSEC algorithm 8), while the registrable leaf zone is today typically ECDSA-P256/SHA-256 (algorithm 13 — used by Cloudflare, Route 53, and Google Cloud DNS); Ed25519 (algorithm 15) is valid but rare. The verifier handles all three, by two different routes:
- RSA (alg 8) is checked inline. Its RSASSA-PKCS1-v1_5 modular exponentiation
runs in-program through the allocator-free
sol_big_mod_expsyscall. An all-RSA chain needs nothing beyond the buffer. - ECDSA-P256 (alg 13) and Ed25519 (alg 15) ride the native precompiles.
Solana has no in-program P-256 or Ed25519 curve op, only the
Secp256r1SigVerifyandEd25519SigVerifyprecompiles. So for each non-RSARRSIGin the chain, the transaction includes one matching precompile instruction and passes the Instructions sysvar as an optional 6th account toAuthorizeDomainByProof. The program then introspects that sysvar to confirm a precompile in this transaction verified the exact(public key, message, signature)the DNSSEC link requires. (An all-RSA chain omits the sysvar account entirely.) The CLI derives and emits all of this automatically — see the walkthrough.
The one wrinkle between the two delegated algorithms: ECDSA-P256 signs a SHA-256 pre-hash of the canonical RRset, whereas Ed25519 signs the canonical octets raw and hashes them itself with SHA-512 — the verifier hands each precompile the message shape its algorithm expects.
Because the verifier calls sol_big_mod_exp, the target cluster must have the
enable_big_mod_exp_syscall feature active. It is active on mainnet; a
local surfpool validator needs --features-all.
Prerequisite: publish the root KSK
The proof chains back to a root key-signing-key (KSK) fingerprint stored
on the PostOffice account — a mail-program account the
domain program reads
cross-program. The delegate publishes it once (and rotates it
when ICANN rolls the root key):
sithbit postmaster ksk set <BASE58_32B> \
[--keypair <delegate keypair>] \
[--skip-preflight]
Read the fingerprint currently anchored on the PostOffice with the read-only
sithbit postmaster ksk get (prints the base58 value, or unset).
The fingerprint is a 32-byte value in base58 (the same encoding wallet
addresses use). Concretely it is the DS-style SHA-256 digest of
owner ‖ DNSKEY (RFC 4509) for the ICANN root KSK — the same digest a DS
record carries — which is what the verifier’s anchor step matches. The all-zero
value —
11111111111111111111111111111111 — is the “unset” sentinel and clears it;
while it is unset, domain authorize fails with RootKskUnset before it
looks at the witness at all.
You do not compute that digest by hand. IANA publishes it as the SHA-256
KeyDigest in its root trust anchor
(root-anchors.xml), and postmaster ksk iana turns that file into
the base58 value — a pure offline conversion that neither signs nor touches the
chain:
# from a downloaded anchor file (recommended — verify its signature first)
sithbit postmaster ksk iana --anchors-file root-anchors.xml
# or fetch it straight from data.iana.org (unverified — dev/preview only)
sithbit postmaster ksk iana
It prints each anchor’s key tag, digest, and base58 fingerprint, and — when
exactly one is active (no validUntil) — the ready-to-run ksk set line.
The trust decision stays with you: the CLI does not fetch the anchor inside
ksk set itself, because the whole security model rests on the delegate
deliberately vouching for the anchor (verified out-of-band), not on trusting
whatever a network lookup returns. It is a rare, governance-paced step — you
re-run it only when ICANN rolls the root key.
Rollovers: more than one active anchor
During a root-KSK rollover ICANN publishes two active anchors (neither
carries a validUntil) for the overlap period — as it is doing now for the
KSK-2024 introduction alongside KSK-2017. When it sees more than one active
anchor, ksk iana does not guess: it prints a warning, recommends the
newest by validFrom (with that anchor’s ready-to-run ksk set line), and
exits non-zero so the choice stays deliberate. Note that “newest” is a
convenience, not gospel — during the introduction phase the incoming key may be
published before it is signing. Confirm which key tag is operational against
ICANN’s rollover announcement, then pin exactly that one:
sithbit postmaster ksk iana --key-tag 20326
--key-tag selects that anchor’s ksk set line directly (and exits zero), so
once you have decided, the command is scriptable again. Because the root DNSKEY
RRset carries both keys throughout the overlap, a witness verifies against
either active anchor while both are present; pinning the key that will remain
avoids a re-ksk set when the old one is finally revoked.
Downloading and verifying the anchor
IANA signs root-anchors.xml with a detached S/MIME (CMS) signature
(root-anchors.p7s) — not PGP. --fetch-anchors <DIR> downloads the anchor,
that signature, and ICANN’s CA bundle, then prints the exact openssl command
to verify them and the follow-up derive step. It deliberately does not
derive a fingerprint itself — you verify first:
sithbit postmaster ksk iana --fetch-anchors ./anchors
then run the printed command:
openssl cms -verify -CAfile ./anchors/icannbundle.pem -inform DER \
-in ./anchors/root-anchors.p7s -content ./anchors/root-anchors.xml -binary
The -binary flag is required: without it openssl canonicalizes the detached
content as text (CRLF translation) and the digest fails to match. On older
OpenSSL use openssl smime -verify -inform der ... instead.
Verification successful means the XML is authentic; now derive from the file
you just verified:
sithbit postmaster ksk iana --anchors-file ./anchors/root-anchors.xml
Getting ICANN’s CA independently
There is a bootstrap trap here: icannbundle.pem was fetched from
data.iana.org over the same channel as the anchor it vouches for. On its
own it only proves the three files are internally consistent — a network
attacker who can serve you a forged root-anchors.xml can serve a matching
forged .p7s and icannbundle.pem too. Verifying downloaded content against a
downloaded signature using a downloaded CA proves nothing unless the CA reaches
you through a channel independent of the data. ICANN’s DNSSEC CA is a
private CA — it is not in your browser/system web-PKI trust store — so you
cannot lean on the usual roots. Practical ways to obtain it out-of-band, in
rough order of effort:
- Cross-check the digest itself, not the CA (simplest, and usually enough).
The root KSK’s DS digest is a widely-replicated public constant. Confirm the
hex
postmaster ksk ianaprints —E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8Dfor the current KSK-2017 (tag 20326) — against several independent, authenticated sources: your distro’s DNSSEC root-anchor package (below), ICANN’s site over web-PKI TLS, and RFC 7958. If independent sources agree on the digest, the S/MIME dance is belt-and-suspenders. - Use a copy shipped through your distro’s signed package channel. Packages
like Debian/Ubuntu
dns-root-data(/usr/share/dns/root.ds) andunbound(whoseunbound-anchorships a built-in copy of ICANN’s CA / the 2017 KSK to bootstraproot.key) reach you via APT/DNF’s GPG-signed repositories — a genuinely independent, cryptographically-verified channel. Pointopenssl -CAfileat unbound’s bundled cert, or just compare digests withunbound-anchor -v. - Fetch the CA over multiple independent network paths and compare. Download
icannbundle.pemfrom a different ISP, a cloud VM in another region, and/or over Tor, and compare the file’s SHA-256. A non-global adversary cannot MITM all paths at once, so matching hashes raise confidence. - Check the CA certificate’s own fingerprint against ICANN’s out-of-band publications (its DNSSEC practice statement and announcements).
For a one-time devnet/preview setup the bare ksk iana fetch is fine; for a
mainnet delegate, verify through at least one independent channel above before
you ksk set.
Note:
ksk setis a delegate-only governance action (error 66,NotDelegate, for any other signer), gated behind the CLI’spostmasterfeature. See The Postmaster.
Supplying the witness
The witness is the opaque RRSIG-chain bytes — the staged DNSSEC proof the
verifier walks. Provide it from exactly one of two sources (the CLI enforces
the choice):
--witness-file <path>— a file holding the raw witness bytes.--witness-hex <hex>— the witness as a hex string (an optional0xprefix is accepted).
The CLI bounds the witness client-side at MAX_PROOF_WITNESS_LEN (8 KiB — the
on-chain buffer’s cap) and refuses an empty one, both before it sends any
transaction, so a mis-sized witness never costs a fee. A real chain runs
~2.7–3.1 KiB, comfortably inside that bound.
Building the witness with gather-witness
Not in the default build.
gather-witnesslinks a DNS resolver stack, so — likediscover— it is gated behind the CLI’s opt-ingatherfeature and is absent from a stocksithbitbinary. Build one that has it withcargo build -p mail-client --features gather(or--all-features) before the commands below will resolve.
You do not assemble those bytes by hand. domain gather-witness collects them
from live DNS for you:
sithbit domain gather-witness <domain> \
[--resolver <ip>] \
[--out <path>]
It queries a recursive resolver with the DNSSEC DO bit set — so every answer
carries its RRSIG — walks the delegation from the root to your zone (finding
each cut by its DS record), and collects exactly the records the on-chain
verifier needs: the root DNSKEY self-signature, each zone’s parent-signed DS
and self-signed DNSKEY, and your leaf _solana.authority.<domain> TXT. It
serializes them into the witness buffer and — before writing anything — re-walks
the assembled chain locally with the very same program_common verifier the
program runs (RSA inline; ECDSA-P256/Ed25519 via host curve checks), so a witness
that would fail on-chain is caught for free, not paid for. On success it prints
the root KSK fingerprint the chain anchors to (which must match the one set
on-chain with ksk set) and the
proven authority key.
# write the witness to a file, then authorize with it
sithbit domain gather-witness sithbit.com --out proof.bin
sithbit domain authorize sithbit.com --witness-file proof.bin
# or without --out, it prints the hex for --witness-hex
sithbit domain gather-witness sithbit.com
--resolver defaults to 1.1.1.1; override it if your network blocks it or the
default does not return RRSIGs. (As noted above, gather-witness needs a CLI
built with --features gather.)
Publishing the DNS records the proof needs
gather-witness (and the on-chain verifier) require your domain to be a
DNSSEC-signed zone apex publishing a single authority TXT. Three one-time
setup steps get you there:
- Enable DNSSEC at your DNS host. Your host signs the zone and shows you a DS record (key tag, algorithm, digest type, digest). On Cloudflare: DNS → Settings → Enable DNSSEC; it signs with ECDSA-P256 (algorithm 13), which the verifier handles.
- Install that DS at your registrar. The DS must live in the parent
zone (e.g.
.com), and only your registrar — the company you bought the domain from — can write there. Copy the DS from your DNS host into the registrar’s DNSSEC panel; the registrar relays it to the TLD registry, which completes the signed delegation. Special case: if you registered the domain through Cloudflare Registrar (Cloudflare is both registrar and DNS host), enabling DNSSEC submits the DS for you — nothing to copy. The two-step copy only applies when your domain is registered elsewhere and merely uses Cloudflare for DNS. - Publish the authority TXT. Add exactly one TXT record at
_solana.authority.<domain>whose value is your wallet’s authority key, base58-encoded (the same convention the off-chaindomain-sithbitflow reads). More than one TXT at that name is rejected — the verifier requires exactly one.
Once the DS is live at the parent and the TXT is published, gather-witness
can collect a complete, verifiable chain.
The --payer-keypair funds the staging and authorize transactions, the new
domain account’s rent, and the authorization fee; it
defaults to the CLI’s configured keypair, and it is the only required
signer — no delegate signature is involved.
End-to-end walkthrough
Two commands take a domain from a witness to authorized. First the delegate
publishes the root KSK once (see Prerequisite: publish the root
KSK); then anyone holding the witness
runs domain authorize:
# once, by the delegate — anchors every proof to the ICANN root
sithbit postmaster ksk set <BASE58_32B>
# permissionless — anyone with the witness may submit and pay
sithbit domain authorize example.com --witness-file ./proof.bin
That second command runs the whole submission for you — you never stage the
buffer, build a precompile instruction, attach a ComputeBudget instruction,
or reclaim the buffer’s rent by hand:
- It stages the witness automatically, splitting it into ≤900-byte chunks
and writing each with a confirmed
WriteProofWitnesstransaction, printingStaged witness chunk N/Nas it goes (the buffer protocol is How it works). - It derives any precompile instructions the chain needs — for each
non-RSA (ECDSA-P256 or Ed25519)
RRSIGit re-walks the witness client-side (the sameprogram_commonchain walk the program runs, with a collecting verifier plugged into the same seam the on-chain precompile introspection uses) and builds one self-containedSecp256r1SigVerify/Ed25519SigVerifyinstruction over the exact(public key, canonical message, signature)tuple the on-chain walk will demand. An all-RSA chain needs none, and the transaction is unchanged from its historical form. - It then submits
AuthorizeDomainByProofwith aComputeBudgetset-compute-unit-limit instruction prepended automatically (400k, sized for the measured ~306–312k-CU all-RSA chain walk with headroom — see Measured compute cost), the derived precompile instructions alongside it, and — only when precompiles ride — the Instructions sysvar as the authorize instruction’s 6th account, and prints the submitted-proof transaction URL. - On a successful authorization it closes the spent witness buffer
automatically with
CloseProofWitness, refunding the buffer’s rent to the payer and printingClosed witness buffer; reclaimed N lamports. A failed authorize deliberately skips this step: the staged buffer stays put for a retry or inspection, reclaimable any time withdomain authorize --close-witness.
This is proven end-to-end in the client integration suite for both a genuine
all-RSA-2048 chain (authorize_by_proof_cli_with_staged_witness_succeeds) and
an ECDSA-P256-leaf chain — the shape most Cloudflare, Route 53, and Google
Cloud DNS zones have today — with the CLI-emitted precompiles
(authorize_by_proof_cli_with_ecdsa_leaf_succeeds).
One honest wrinkle: the on-chain matcher compares each precompile’s signature bytes against the
RRSIG’s raw bytes, and the secp256r1 precompile itself rejects high-S ECDSA signatures — so an alg-13RRSIGwhose signature is high-S cannot be proven on-chain at all. Real DNSSEC signers emit low-S in practice; the CLI normalizes to low-S on emission, which is the identity for those.
Reclaiming an abandoned witness buffer
sithbit domain authorize <domain> --close-witness \
[--payer-keypair <path>] \
[--skip-preflight]
A successful domain authorize reclaims the staged buffer’s rent
automatically, but a failed or abandoned attempt leaves the buffer behind
on purpose — the staged witness stays available for a retry or for inspection.
When you are done with it, domain authorize --close-witness submits
CloseProofWitness to close the buffer and refund its full rent, printing
the reclaimed lamports.
The --payer-keypair must be the same keypair that staged the witness: the
buffer PDA is seeded on the payer, so it is the only key that derives — and may
close — that buffer, and it is where the rent refund lands. A partially-staged
buffer (an attempt abandoned mid-write) closes and refunds just the same. If no
buffer exists for the (payer, domain) pair, the command refuses client-side
before sending any transaction.
Measured compute cost
The chain walk’s cost is measured through the deployed .so on a real
cluster (surfpool), driven by the two positive end-to-end tests. Both shapes
walk a three-zone chain (root → TLD → leaf):
- All-RSA chain (six RSA-2048 verifications, all inline via
sol_big_mod_exp): the authorize instruction consumed 305,704–311,868 CU across runs — the small spread tracks witness content (domain-name lengths and key values vary per run). - ECDSA-P256-leaf chain (four RSA-2048 verifications inline; the leaf’s two
P-256
RRSIGs proven bySecp256r1SigVerifyprecompile instructions): 229,845–231,181 CU. The precompile instructions themselves metered zero transaction-budget CU on this runtime, so the transaction-wide total was the program’s consumption plus 150 CU for theComputeBudgetinstruction itself.
The CLI’s 400,000-CU limit therefore keeps ~28% headroom over the worst
measured shape. Each additional all-RSA zone in a deeper chain costs roughly
+80k CU (two more RSA-2048 verifications at ~40k each), so a four-zone
chain approaches the limit and a deeper one would need it raised. The other
instructions are cheap and ride the 200k default: each WriteProofWitness
chunk measured ~9–11k CU and CloseProofWitness ~7–8k.