RNS over ZAP
PQ-RNS — Resource Name Service binding names to post-quantum identity keys.
RNS over ZAP
RNS (Resource Name Service) resolves service names to post-quantum identity records. Instead of looking up an A/AAAA record and trusting whatever's at that IP, you resolve a name to a (kemPubKey, sigPubKey) pair signed by the issuing registry. The peer must prove possession of the matching private keys on connect — the identity is the address.
This is the part of ZAP that matters most under a quantum-capable adversary: even with perfect classical PQ transport, a name service that resolves to "whoever holds an X509 cert from a CA you trust" still concentrates trust in the CA. RNS removes the CA.
Status — spec-only, January 2026. The wire schema is published and the zap-proto/papers
rns-identity-bindingproof is the reference. The Go resolver is in active development; client/server implementations in TS / Python / Rust will follow. Until then, you can implement against the schema directly using any SDK — the canonical record encoding is language-neutral. Cross-language KATs ship as test fixtures, see below.
Wire schema
The schema is in zap-proto/rns/schema/zap_rns.zap. Verbatim:
struct Record
name Text # human-readable name, e.g. "acme.payments"
kemPubKey Data # X-Wing static public key (1216 bytes)
sigPubKey Data # hybrid sig public key (Ed25519 + ML-DSA-65, 1984 bytes)
ttl UInt32 # seconds — cache lifetime hint
notBefore UInt64 # unix nanos
notAfter UInt64
registry Text # which RNS authority issued this record
signature Data # registry signs everything except `signature` itself
struct Query
name Text
auth Data # optional — token signed by requester's long-term key
struct Response
union
record Record # found
notFound Text # name not registered
expired Record # registered but past notAfter — included for cache invalidation
denied Text # registry refused the queryTrust model
The resolver doesn't need to be trusted. A returned Record is verified end-to-end:
- Pull the record from any RNS resolver.
- Compute the canonical encoding of
(name, kemPubKey, sigPubKey, ttl, notBefore, notAfter, registry). - Verify
signatureagainst the registry's public key — published out-of-band, pinned at deploy time. Multiple registries are supported; clients trust whichever set of registry pubkeys their config holds. - Check
now ∈ [notBefore, notAfter]. If pastnotAfter, treat asexpired.
A forged or stale answer fails verification. A registry can't issue records for a name without also publishing the signature — there's no "trust me, the name maps here" — only "here's the registry's signed claim."
Compare to DNS+CA:
| DNS+CA | RNS | |
|---|---|---|
| Who you trust | DNS provider, registrar, CA, every CA in the trust store | Registry pubkey (you choose; you pin) |
| Who can spoof | CA mis-issuance, DNS poisoning, registrar hijack | Forging the registry's signature |
| What you get | An IP address | A keypair + endpoints |
| Authentication on connect | New TLS handshake, certs validate hostname | Same X-Wing pk that was in the record |
PQ-RNS — putting it together
The two keys in a record:
kemPubKeyis an X-Wing static public key. Future ZAP connections to this identity use this key aspk_Xin the X-Wing combiner. The handshake binds the channel to the resolved identity.sigPubKeyis a hybrid Ed25519 + ML-DSA-65 verification key. Used to sign capability tokens, attestations, and any application-level claims attributed to this identity.
End-to-end PQ flow:
client RNS resolver server
│ │ │
│── Query("acme.payments") ─────────────────→│ │
│ │ │
│←── Response(Record signed by registry) ────│ │
│ │
│ verify registry signature over Record │
│ pick endpoint from Record.endpoints │
│ │
│── X-Wing handshake (recipient pk = Record.kemPubKey) ────────→│
│ │
│←── X-Wing ct + sealed identity + ML-DSA-65 transcript sig ───│
│ │
│ verify ML-DSA-65 sig against Record.sigPubKey │
│ channel is bound to the resolved name │
│ │
│←── encrypted ZAP RPC over X-Wing-derived ChaCha20-Poly1305 ──→│No CA. No DNS hijack vector. The classical primitive (Ed25519, X25519) and the lattice primitive (ML-DSA-65, ML-KEM-768) are both needed to forge a session — breaking either alone doesn't help.
Watching for updates
Records are immutable per signature, but a registry can publish a new signed record with a later notBefore to rotate endpoints. RNS clients SHOULD subscribe rather than poll:
# pseudo-API across all SDKs
ch = rns.watch("acme.payments", registries=[...])
for record in ch:
update_load_balancer(record.endpoints)When a publisher rotates endpoints (failover, scale-out), every watcher sees it within one round trip.
DIDs (did:zap)
A did:zap:<hash> is the base32 SHA3-256 of the canonical (kemPubKey || sigPubKey) encoding. Resolving a DID is the same path as resolving a name, except the registry index is content-addressed instead of name-keyed.
did = "did:zap:" + base32(SHA3-256(kemPubKey || sigPubKey))Two implementations that produce the same DID for the same keypair are guaranteed bit-for-bit interop.
Cross-language KATs
The canonical encoding is locked by a known-answer test (KAT) shipped in every SDK. A test that doesn't reproduce the same DID from the same (kemPubKey, sigPubKey) fails immediately:
kemPubKey = bytes.fromhex("01" * 1216)
sigPubKey = bytes.fromhex("02" * 1984)
expectedDID = "did:zap:" + base32(SHA3-256(kemPubKey || sigPubKey))The Go reference computes the same DID; so does the Rust, Python, and TypeScript port (forthcoming). Any divergence on the encoding, the hash, or the byte order is a test failure — that's the floor of cross-language correctness.
KAT vectors and test runners are published at zap-proto/rns/testdata (forthcoming directory) so a third-party SDK can drop them in unchanged.
Per-language status
| Language | Schema codegen | Resolver client | Registry server | KAT |
|---|---|---|---|---|
| Go | ✅ | reference impl in progress | reference impl in progress | ✅ |
| TypeScript | ✅ | spec-only | — | ⏳ |
| Python | ✅ | spec-only | — | ⏳ |
| Rust | ✅ | spec-only | — | ⏳ |
| C++ | ✅ | — | — | ⏳ |
| Java | ✅ | — | — | ⏳ |
Spec-only means: the schema is published and codegen produces the record types in the language, but the resolver / registry runtime is not yet implemented. The KAT is what fixes the canonical encoding so client implementations can be developed independently and still interop.
Related
- Post-quantum overview — KEM, identity, name-service layers
- Native ZAP RPC — how the channel reuses
Record.kemPubKey - zap-proto/rns — schema + test vectors
- zap-proto/papers —
rns-identity-bindingproof