Status: R0.9 contract, written before `cd-connect` exists. Nothing here is implemented. It is written first because the review's order was explicit — contract, then controls that fail, then mechanism — and because this file has to make one decision the round could otherwise make silently: what a transport credential is in this system.
docs/discovery-model.md is canonical for the rendezvous plane. This file is canonical for the road every endpoint travels afterwards, and for the boundary between them.
The rule is three clauses, and the crate graph should be able to show them:
DISCOVERY CHOOSES WHERE WE TRY. cd-rendezvous
AUTHENTICATION DECIDES WHOM WE REACHED. cd-connect
ONLY THE LINEAGE DECIDES WHAT THEY MAY DO. R0.8, cd-core / cd-durable
Authentication is not a rendezvous responsibility. Saying "endpoint provenance is erased before authentication" is a property one can assert; putting the validation road in a crate the rendezvous plane merely feeds is the same property made structural — the arrows are one-way because the dependency graph is one-way, not because a control checked.
That is also why decode_exact_datagram does not belong in cd-rendezvous. Exact peer-frame parsing is part of the one validation road that static, cached and rendezvous endpoints all travel. It is not rendezvous. It moves.
cd-rendezvous (untrusted location plane)
NodeId -> EndpointHint[]
no authentication · no sessions · no authority
│
│ normalized, provenance stripped
▼
cd-connect (the one validation road)
PeerExpectation + EndpointCandidate
transport authentication
exact peer-frame parsing
expected NodeId comparison
│
▼
ValidatedPeerSession
│
STILL NO AUTHORITY
│
▼
R0.8 lineage decides what that session may do
No frozen crate depends on either new crate.
The transport credential is the EXPECTED REMOTE TRANSPORT PEER AUTHENTICATOR, established outside rendezvous. In the first concrete profile it is the remote peer's 32-byte WireGuard public key.
R0.6 already separated these experimentally, and R0.9 reuses that separation rather than inventing another credential model:
NodeId semantic identity
WireGuard public key transport credential
underlay endpoint location
No new cryptosystem and no crypto dependency. The local private key stays where it already is — in the kernel's WireGuard implementation. cd-connect never holds it, never receives candidate peer keys from a directory, and never changes which key is expected.
WireGuard fits the theorem unusually well, because its own roaming rule points the same direction the contract does: a peer's endpoint is updated from the outer source address of a packet that already authenticated. Authentication may update location; location never creates authentication.
R0.6 proved the transport key can rotate while the NodeId does not change. So rotation is somebody else's job. PeerExpectation is the caller's current trusted expectation, supplied by an outside configuration or key-management path.
If that expectation goes stale, connection fails. That is an availability loss, and it is not permission for the rendezvous service to supply a replacement credential. This is the Tailnet-Lock lesson in miniature: an untrusted coordination plane must not be able to substitute the keys that decide who participates.
PeerExpectation — what the caller already knowsPeerExpectation
expected_node_id NodeId, 32 bytes
expected_transport_peer TransportPeerRef
Established outside R0.9 and before any rendezvous query. It is an input, never an output. Nothing in cd-rendezvous may construct, widen or replace one.
TransportPeerRef is an opaque reference to a peer that already exists in the transport implementation. It is not a key the connector installs. The distinction is the whole point:
AN ENDPOINT HINT MAY RETARGET AN ALREADY-EXPECTED TRANSPORT PEER. IT MAY NOT CREATE A PEER, AND IT MAY NOT CHANGE WHICH CREDENTIAL IS EXPECTED.
Where the platform offers an update-existing-only primitive, use it, so the property is structural. Where a first lab has to go through a weaker shell interface, the control carries explicit before/after gates proving the peer public-key set is byte-identical and that no peer was added or replaced — and the shell convention is not claimed to be the eventual enforcement.
EndpointCandidate — an address, and deliberately nothing elseEndpointCandidate
addr underlay address:port
It carries no provenance. No source=static, no source=cache, no source=rendezvous, no confidence score, no vantage count, no age. Static, cached and rendezvous layers each normalize into this type before calling cd-connect.
This is provenance erasure made structural rather than asserted: a discovered endpoint cannot select a weaker validation path because the connector has no field to select on. Test-only instrumentation may expose the validation steps for comparison; production connector state must not need provenance to work.
ValidatedPeerSession — reached whom, and nothing moreValidatedPeerSession
node_id the expected NodeId, now confirmed
peer the TransportPeerRef that authenticated
addr where it answered
It is the end of clause two and the beginning of nothing. It confers no lifecycle authority. What such a session may do is decided by the R0.8 lineage, independently, and a session that authenticates perfectly still moves none of the six verbs.
connect(PeerExpectation, EndpointCandidate) -> Result<ValidatedPeerSession>
1 retarget the ALREADY-EXISTING transport peer at this candidate address
(update-existing-only; never create, never re-key)
2 transport authentication -- fails => stop
3 exact-datagram parse of the peer's frame -- a tail => stop
4 expected NodeId comparison, over DECODED BYTES -- differs => stop
5 ValidatedPeerSession
Each step's refusal is distinguishable from the others. A control that cannot tell "the network was down" from "the peer failed to authenticate" is measuring a proxy, and this project has lost rounds to exactly that.
Every equality in step 4 is over decoded bytes. R0.6's version compared 64 hexadecimal characters against 44 base64 characters and therefore could never fire.
endpoint does not establish transport credential
transport credential does not establish semantic identity
semantic identity does not establish transport credential
a validated session does not establish lifecycle authority
Four independent claims, four independent controls. Deriving any one from another is the defect.
And the negative form, about the rendezvous plane specifically. It may not:
receive a candidate transport key from the rendezvous service
replace the expected transport key
mint a transport peer
infer the transport key from a NodeId
infer a NodeId from a transport key
Falsifier for the whole family: a field, anywhere in the rendezvous protocol or state, capable of carrying credential material. If no such field exists, a malicious directory cannot substitute a credential because the credential is not an input from that plane — which is a stronger statement than any refusal check.
R0.9 HAS NO PEER TRUST-ON-FIRST-USE.
The caller possesses a PeerExpectation before it queries rendezvous. So a malicious rendezvous server's first answer can cause delay, misdirection, denial, and the metadata residual already declared in docs/discovery-model.md §3 — and it cannot establish a NodeId, a transport peer key, the binding between them, or any lifecycle authority.
Control: without a PeerExpectation the validated connector cannot be invoked at all, and nothing anywhere creates one from a rendezvous answer.
Unknown-peer introduction and DHT bootstrapping are a different trust contract and stay deferred (docs/r09-deferred-threat-backlog.md).
One transport profile: WireGuard, with a pre-established peer. Do not generalize to a TLS / Noise / QUIC credential enum yet — prove the first profile, then find out what the second one actually needs. A generic credential abstraction written before its second instance is a guess about the second instance.
Do not open a credential-management round unless the implementation shows the pre-established peer expectation is genuinely insufficient. "Credential" being a word that sounds cryptographic is not a reason for this layer to own cryptography.
§4 above was written before cd-connect existed and is kept as it was. GPT's review 2 found that the road it describes, as built, took a NodeId from whichever frame came back — a HELLO or an ACK — and read neither nonce: authentication said whom the transport reached, and nothing said that the datagram answered THIS attempt. Since R128 (docs/r09-review2-fixes-plan.md, E-01) the road is:
connect(PeerExpectation, EndpointCandidate, transport) -> Result<ValidatedPeerSession>
1 the handle is the expected peer
2 a fresh attempt nonce, made by connect() from the OS -- none => stop, nothing retargeted
3 retarget the ALREADY-EXISTING peer for this attempt (the HELLO carries the nonce)
4 transport authentication -- fails => stop
4a exact-datagram parse -- a tail => stop
4b correlation: an ACK echoing THIS attempt's nonce -- a HELLO, or another echo => stop
5 expected NodeId comparison, over DECODED BYTES -- only after 4b
6 ValidatedPeerSession
No caller supplies, chooses or reuses a nonce: AttemptNonce's field is private and fresh() is its only constructor. A transport's retarget begins ONE attempt, and must discard every piece of a previous attempt's state or refuse. Correlation is a property of the answer, not of the peer: it adds nothing to whom we reached and nothing to what they may do.