Repository navigation
v0.5.0 — several Shrines on one endpoint
One node can now host any number of Shrines behind a single listener, and a visitor names which one it wants without that name ever crossing the wire in the clear.
What's new
node.HostShrine(alpha, alphaHandler); // a second Signet adds a site
node.HostShrine(beta, betaHandler); // the same one again replaces it
node.ListenForPilgrims(); // one port, both sitesA visitor does nothing differently — it pins the address it wants, and the endpoint answers as that Signet or not at all. Browser DataChannels get the same treatment, so one node serves several sites to the web.
| API | Change |
|---|---|
CupriNode.HostShrine(...) |
Adds a site when given a new Signet; replaces when given the same one again. Previously it always replaced. |
CupriNode.ShrineAddresses |
New. Every cupri1… address hosted, in order. ShrineAddress still returns the first. |
CupriNode.AcceptPilgrimageOverVesselAsync(vessel, ct) |
New overload. Serves whatever the node hosts, resolving the target per visit. |
NoiseConjunction.AcceptAsync(..., resolveIdentity, ct) |
New overload. Chooses which identity to present from the requested target. |
NoiseConjunction.ShrineTargetTag(suite, handshakeHash, sigil) |
New. Computes the blinded target. |
Upgrade impact: none
Additive only — no existing signature changed. The target is a trailing binding field, the same backward-compatible pattern Moniker and WebRtcEndpoint use, so:
- an older Pilgrim sends no target and is served the first Shrine hosted — the only one it could have meant;
- an older host ignores the field entirely.
A node hosting exactly one Shrine behaves precisely as it did in 0.4.0.
Why it's shaped this way
This was on the roadmap next to Noise NK, on the assumption that an endpoint must choose which key to present during the handshake and therefore needed a new handshake pattern. It needed none. The Noise static key is generated fresh per connection and is unrelated to any Signet; identity is proved afterwards in the binding exchange — inside the encrypted transport, which the responder already reads before answering. The selector had a natural home all along.
Blinded, not named
The visitor sends
HKDF(ikm: target Sigil, salt: Noise handshake hash, info: "cuprinet:shrine-target:v1")
and the host recomputes it for each Signet it hosts, comparing in fixed time. Because a visitor must name its target before the host has proved it holds anything, two properties matter:
- A host that does not serve the site learns only "not one of mine." Probing an endpoint to see what it hosts does not tell that endpoint what you were looking for.
- The value differs every visit, since the handshake hash salts it — so it is no use as a handle for linking one visit to the next.
It is not a secret; anyone holding the address can compute it for a session whose handshake hash they know. It hides the target from parties who do not already know it, which is exactly the set that should not learn it.
A target that matches nothing
Refused in words, inside the encrypted channel — never as silence, and never by presenting a different site the endpoint happens to host. Serving the wrong site would be the genuinely dangerous failure here, so it has a test of its own.
Notes
- No CupriMark bump: the trailing-field pattern is the one
MonikerandWebRtcEndpointalready use without one. design/shrines.mdhad predicted a post-Noise session-kind marker would be needed for this. It was not, and that note now says so.- Full design: design/shrines.md → Several Shrines on one endpoint.
453 unit tests green.