A server-scoped device credential, proven in the handshake and kept by the launcher
Status: Accepted (2026-11-08)
Context
The auth resource of ADR 0052 needs to recognize the same device coming back to its server, without a global account id that would let servers link a player across the network, and without trusting anything the client merely claims. The private key behind such a credential must never enter the game process.
Two neighboring problems are solved in the same step. An unsigned ConnectReject let anyone who could send from the server's address abort a login with a false reason ("banned", "server full"). And the launcher reported the player as connected as soon as ServerHello arrived, before the auth resource had decided anything.
Decision
- The proof rides in the second
ClientHello.credential_keyis an Ed25519 public key, andcredential_proofis its signature over"DarqlineMP credential\0"‖server_key‖cookie‖nonce. The cookie is the server's own challenge, bound to the client's address, port and time window (ADR 0042); the nonce is bound to one connection attempt; the server key binds the proof to this server. All zeros means no credential. The server checks the proof after the cookie and the attempt limit. A proof that does not verify getsConnectRejectinvalid_credential; a proof that has already opened a connection gets no answer while its cookie lives, so a capturedClientHellocannot be replayed into a second session. See Handshake. - The credential is optional. Without one,
pending:credential()returnsniland the auth resource decides what that means. With one, the resource sees only an opaquecredential_id, the hex SHA-256 of the key, and only through the pending handle. It is the same for one computer on one server and different on every other server. - The launcher holds the keys and signs. The launcher (open source) keeps a separate random seed for each server key, encrypted for the current Windows user, and saves it before the first proof goes out. During the handshake the client asks the launcher for a proof of the server's cookie and its nonce; the launcher signs for the server key of the connect request it issued itself, never for a key the client names, and limits how many proofs one game process can get. The client waits up to one second for the answer and connects without a credential if none comes. The game process never sees the private key.
ConnectRejectis signed. It carries the server key and a signature over"DarqlineMP reject\0"‖ nonce ‖ the packet, and the client takes only a refusal signed by the expected key. Signing costs the server, so an IP gets at most 8 signed refusals at once and 2 per second; beyond that a refusedClientHellogets no answer.- Login is a visible stage.
ServerHello.auth_pendingtells the client that the connection waits for the auth resource; acceptance arrives asAuthAccepted, a critical event on the reliable event lane, after the login screen events if there are any (ADR 0055). The client reports the stage to the launcher: "signing in" while it waits, "connected" only afterAuthAccepted, and "rejected" when the login ends otherwise: "refused" with the server's reason after a refusal, a timeout or the auth resource stopping, "no answer" when the server goes silent.
Consequences
ClientHellogrows to 235 bytes,ServerHelloto 153 andConnectRejectto 122;AuthAccepted(0xA1) and theConnectRejectreason 10,invalid_credential, are new. The launcher's channel to the client gains a credential request and answer and the "signing in" stage.- A server the player connected to on their own cannot use the player's credential at another server: every proof is signed for the key of the server that receives it.
- Game traffic is not encrypted. A man-in-the-middle on the path can still abort a login by dropping packets and can act within the session after
ServerHello; the credential does not change that.
Alternatives considered
- A separate challenge in PreAuth. A new packet, single-use state on the server and one more round trip, while the cookie already is a server-issued challenge bound to the address. Rejected.
- One master secret with per-server seeds derived from it. The file would not grow, but one server's credential could not be reset alone. Rejected.
- Requiring a credential on servers with an auth resource. Development clients that connect without the launcher could not join. Rejected.
- Only relabeling the kick in the launcher. Without a PreAuth flag and an acceptance signal the client cannot tell a login in progress from a session. Rejected.
