- summary: Closes the proof-of-identity handshake by answering the server's challenge with a Schnorr response.
- area: Game.Client.Verification
- confidence: 0.94
Closes the proof-of-identity handshake by answering the server's challenge with a Schnorr response.
- Kind request
- Area Game.Client.Verification
- Named by beta, gamemapping
Answers ServerChallengeEvent and ends the three-step exchange opened by
ClientIdRequest and ClientChallengeInitRequest.
WHEN TO SEND IT
IMMEDIATELY ON ServerChallengeEvent, once, unconditionally. One construction site
and no other path.
THE WHOLE SCHEME, END TO END
This is the one page that carries the complete algorithm. It is a Schnorr proof of identity, and everything below is read out of the client rather than guessed:
CONSTANTS the client hard-codes one group and never negotiates it
P the RFC 2409 section 6.1 prime, 768-bit MODP, "Oakley group 1"
G 2
Q (P - 1) / 2
SECRETS neither ever travels
x int(1024 random bytes, unsigned, LITTLE-endian) mod P
drawn ONCE per process, kept for its whole life
k int( 50 random bytes, unsigned, LITTLE-endian)
drawn afresh for every verification poll
THE FOUR STEPS
1 ServerVerificationEvent -> ClientIdRequest
id = decimal string of G^k mod P (commitment R)
2 ServerSessionReadyEvent -> ClientChallengeInitRequest
challenge_key = decimal string of G^x mod P (public key A)
3 ServerChallengeEvent carries c, a decimal string
4 this message
proof = decimal string of (k + c*x) mod Q
and if that came out negative, add Q back
WHAT THE SERVER CHECKS
It recomputes G^proof mod P and compares it with (id * challenge_key^c) mod P. Those agree exactly when you knew both secrets, which is the entire point of the exchange. The arithmetic works because 2 generates the order-Q subgroup of that prime. If you skipped either earlier message there is nothing to compare against, and no amount of guessing here will pass.
FOUR WAYS TO GET THIS WRONG, ALL OF THEM SILENT
Each of these produces a well-formed message that simply fails, with no error you can see and nothing in the traffic to tell you which one you hit.
ENDIANNESS. Both random values are read LITTLE-endian and unsigned. Most big-integer libraries default to big-endian, and the number you get is completely different.
DECIMAL, NOT HEX. Every value on the wire is a base-10 string. There is no hex, no base64 and no binary anywhere in this exchange.
P VERSUS Q. The two exponentiations reduce modulo P; the proof reduces modulo Q. Swapping them is easy and fatal.
THE SIGN. The client reduces modulo Q and then adds Q back if the result came out negative. Languages disagree about the sign of a remainder, so normalise explicitly rather than trusting your own operator.
IF THE CHALLENGE IS ABSENT, YOU ANSWER YOURSELF
The challenge field has explicit presence, and the client checks whether it is there. If the event arrives with NO value, the client does not skip the reply and does not error - it derives its own challenge and answers anyway:
c = SHA-256 of the decimal strings of G, G^x mod P and G^k mod P,
concatenated with the client's device fingerprint, and read back
as a big-endian integer
proof = (k + c*x) mod Q, forced non-negative, exactly as above
THAT BRANCH SIGNS A MACHINE IDENTIFIER. The device fingerprint is the uppercase hex SHA-256 of the machine's unique device id concatenated with the MAC addresses of every network interface. So two machines answering the same value-less event produce different proofs, and the same fingerprint is also sent during login on the separate connection-server exchange. If you are reproducing the client's behaviour you have to reproduce this too, and if you are not, be aware of what it identifies.
THE NUMBERING GAP IS PADDING. This message also declares a six-value enum that no dumped build ever writes; proto3 leaves a zero-valued enum off the wire entirely. There is nothing for you to set there.
IT IS A CORRELATED REQUEST, like the other two steps.
WHAT IS READ AND WHAT IS INFERRED
The constants, the byte counts, both formulas and the value-less branch are read out of the client's own code, and the same structure is present in every dumped build. Two things are NOT: the verification equation above is what the server must be doing for this arithmetic to mean anything, but nothing in a client dump can see the server; and no capture has been observed of a challenge arriving without a value, so that branch is code that certainly exists rather than behaviour that has certainly been seen.
THIS IS AN ANTI-TAMPER HANDSHAKE, NOT A SECURITY BOUNDARY. A 768-bit MODP group has been below recommended strength for many years, and nothing here is encrypted - the exchange proves that whoever is talking held a key across three messages, and that is all it proves. Do not read more assurance into it than that, in either direction.
IT STAYS ON THE MAIN CONNECTION. While a fight-server transfer has a second
socket open (see IdentificationRequest) the client routes outbound traffic by
message type, and all three verification requests are pinned to the FIRST
connection. Gameplay traffic is what moves to the fight server, not this.
When you may send it
- Requires
authenticatedobserved
Related messages
- Answers
ServerChallengeEvent
Fields
| # | Field | Type | Meaning | You set it |
|---|---|---|---|---|
| 1 | proof | string | Decimal string of (k + c*x) mod Q, normalised non-negative. k is the nonce from ClientIdRequest, x the private exponent behind ClientChallengeInitRequest, c the server's challenge, Q = (P-1)/2 for the RFC 2409 768-bit group.When the challenge arrives with no value the client hashes its own, including a device fingerprint - see the description. | required |
Recovered with score 130, confidence 0.94, margin 130 over the runner-up. A margin close to the score means nothing else came near; a thin margin means a decoy nearly won. Members recovered: 1 of 1.
Aucun avis à afficher.