ZAP Protocol
Protocols

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-binding proof 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 query

Trust model

The resolver doesn't need to be trusted. A returned Record is verified end-to-end:

  1. Pull the record from any RNS resolver.
  2. Compute the canonical encoding of (name, kemPubKey, sigPubKey, ttl, notBefore, notAfter, registry).
  3. Verify signature against 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.
  4. Check now ∈ [notBefore, notAfter]. If past notAfter, treat as expired.

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+CARNS
Who you trustDNS provider, registrar, CA, every CA in the trust storeRegistry pubkey (you choose; you pin)
Who can spoofCA mis-issuance, DNS poisoning, registrar hijackForging the registry's signature
What you getAn IP addressA keypair + endpoints
Authentication on connectNew TLS handshake, certs validate hostnameSame X-Wing pk that was in the record

PQ-RNS — putting it together

The two keys in a record:

  • kemPubKey is an X-Wing static public key. Future ZAP connections to this identity use this key as pk_X in the X-Wing combiner. The handshake binds the channel to the resolved identity.
  • sigPubKey is 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

LanguageSchema codegenResolver clientRegistry serverKAT
Goreference impl in progressreference impl in progress
TypeScriptspec-only
Pythonspec-only
Rustspec-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.

On this page