Skip to content

v0.5.0 — several Shrines on one endpoint

Choose a tag to compare

@github-actions github-actions released this 29 Aug 05:50
· 8 commits to main since this release

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 sites

A 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 Moniker and WebRtcEndpoint already use without one.
  • design/shrines.md had 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.