Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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, AuthorizeDomainByProof walks 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 the domain_program test 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 WriteProofWitness instructions, and the buffer is closed and its rent refunded afterward with CloseProofWitness (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:

  1. 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 chunked WriteProofWitness instructions (discriminant 27). Each carries an offset and a chunk of up to MAX_PROOF_WITNESS_CHUNK (900) bytes; the first write allocates the buffer to its declared total_len, and later writes fill it in until written_len == total_len.
  2. Authorize. A single AuthorizeDomainByProof transaction 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 a ComputeBudget set-compute-unit-limit instruction.
  3. 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 successful domain authorize; for a failed or abandoned attempt, run domain authorize --close-witness yourself.

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_exp syscall. 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 Secp256r1SigVerify and Ed25519SigVerify precompiles. So for each non-RSA RRSIG in the chain, the transaction includes one matching precompile instruction and passes the Instructions sysvar as an optional 6th account to AuthorizeDomainByProof. 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 iana prints — E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D for 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) and unbound (whose unbound-anchor ships a built-in copy of ICANN’s CA / the 2017 KSK to bootstrap root.key) reach you via APT/DNF’s GPG-signed repositories — a genuinely independent, cryptographically-verified channel. Point openssl -CAfile at unbound’s bundled cert, or just compare digests with unbound-anchor -v.
  • Fetch the CA over multiple independent network paths and compare. Download icannbundle.pem from 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 set is a delegate-only governance action (error 66, NotDelegate, for any other signer), gated behind the CLI’s postmaster feature. 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 optional 0x prefix 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-witness links a DNS resolver stack, so — like discover — it is gated behind the CLI’s opt-in gather feature and is absent from a stock sithbit binary. Build one that has it with cargo 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:

  1. 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.
  2. 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.
  3. 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-chain domain-sithbit flow 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:

  1. It stages the witness automatically, splitting it into ≤900-byte chunks and writing each with a confirmed WriteProofWitness transaction, printing Staged witness chunk N/N as it goes (the buffer protocol is How it works).
  2. It derives any precompile instructions the chain needs — for each non-RSA (ECDSA-P256 or Ed25519) RRSIG it re-walks the witness client-side (the same program_common chain walk the program runs, with a collecting verifier plugged into the same seam the on-chain precompile introspection uses) and builds one self-contained Secp256r1SigVerify / Ed25519SigVerify instruction 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.
  3. It then submits AuthorizeDomainByProof with a ComputeBudget set-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.
  4. On a successful authorization it closes the spent witness buffer automatically with CloseProofWitness, refunding the buffer’s rent to the payer and printing Closed 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 with domain 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-13 RRSIG whose 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 by Secp256r1SigVerify precompile 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 the ComputeBudget instruction 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.