mirror of
https://github.com/zap-proto/dev.git
synced 2026-10-30 15:05:18 +00:00
Red-team review (this session) caught half the protocol pages were AI
slop — describing Go APIs that don't exist in repos that contain only
schemas, plus performance tables contradicted by the only real bench
harness in tree. This is the surgery:
post-quantum:
- Drop the Z-Wing brand from public docs (user feedback: confusing).
- Reframe PQ as ZAP's stack: IETF X-Wing KEM (transport) + hybrid
Ed25519+ML-DSA-65 (identity) + PQ-RNS (name service). One canonical
story; layers are independent.
- Per-language status matrix shows realistic state (X-Wing KAT shipped
in 4 SDKs, sigs in all, resolver in progress).
protocols/{a2a,acp,fix,ws}:
- Marked "Status — spec-only, January 2026". Schema exists; runtime
doesn't. Removed every Go code snippet that called a non-existent
function. Replaced with a roadmap section.
protocols/rns:
- Rewrote against the actual schema at zap-proto/rns/schema/zap_rns.zap
(Record / Query / Response / signed-by-registry).
- Added the PQ-RNS end-to-end flow diagram and the DID definition
(base32(SHA3-256(kemPk || sigPk))).
- Documents the cross-language KAT fixture published at
zap-proto/rns/testdata/pqrns_kat.json.
protocols/mcp:
- Rewrote against the actual bridge.go API: NewBridge(*zap.Node),
AddServer, GetTools, CallTool, CallToolByName. Bridges existing
stdio MCP servers (it's a bridge, not a from-scratch impl).
- Dropped the invented zmcp.NewServer / Connect / ListTools API.
protocols/http:
- Killed the 6× req/s / 19× p99 / 6.2× RSS table — contradicted by
bench-results.txt which shows ZAP-HTTP currently ~10% slower.
- Replaced with honest current numbers + the value-prop statement.
protocols/native:
- Killed the 18× / 38× / 6.7× / 9.2× gRPC table — the bench harness
has no gRPC comparator, the numbers were fabricated.
- Replaced with the actual concurrency=32 numbers from bench-results
+ an explanation that the win is semantic, not currently latency.
transports:
- Killed the @zap-proto/wasm and Cloudflare-Worker code snippets that
referenced a non-existent npm package.
- Replaced with the honest pattern: browser→TLS→Worker→ZAP-over-X-Wing
to origin. Points readers at the SDK status table.
app/page.tsx:
- Dropped Z-Wing from the home page cards.
148 lines
7.9 KiB
Plaintext
148 lines
7.9 KiB
Plaintext
---
|
|
title: RNS over ZAP
|
|
description: 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](https://github.com/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](/docs/sdks) — 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](https://github.com/zap-proto/rns/blob/main/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+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:
|
|
|
|
- **`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](https://github.com/zap-proto/rns) (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](/docs/post-quantum) — KEM, identity, name-service layers
|
|
- [Native ZAP RPC](/docs/protocols/native) — how the channel reuses `Record.kemPubKey`
|
|
- [zap-proto/rns](https://github.com/zap-proto/rns) — schema + test vectors
|
|
- [zap-proto/papers](https://github.com/zap-proto/papers) — `rns-identity-binding` proof
|