Class HandshakeService
- Namespace
- FishMMO.Auth.Implementation
- Assembly
- FishMMO-AuthShared.dll
Transport-agnostic service for X25519 ECDH handshake operations and stateless cookie challenge management. Provides both server-side and client-side key agreement, IP normalisation, and HMAC-SHA256 cookie computation/verification.
public static class HandshakeService
- Inheritance
-
HandshakeService
- Inherited Members
Fields
CookieTimeBucketSeconds
Time bucket width in seconds for stateless handshake cookies. Cookies are valid for the current bucket and the immediately preceding one, giving a maximum validity window of 2x this value.
public const int CookieTimeBucketSeconds = 30
Field Value
CryptoSuiteId
Identifies the set of cryptographic primitives (curve, hash, KDF) used by this build. Bound into the handshake transcript hash to prevent cross-suite transcript reuse and enable future algorithm agility without a full protocol version bump. Increment when any primitive changes (e.g. X25519→X448, SHA-256→SHA-512).
public const ushort CryptoSuiteId = 1
Field Value
Methods
ClientPerformKeyAgreement(byte[], X25519EphemeralKeyPair, ushort, ushort, ushort)
Performs client-side X25519 ECDH key agreement. Computes a transcript hash matching the server's computation, derives directional session keys. The client keypair's private key is consumed (zeroed) during this call.
public static HandshakeService.ClientKeyAgreementResult ClientPerformKeyAgreement(byte[] serverPublicKey, CryptoHelper.X25519EphemeralKeyPair clientKeyPair, ushort clientMinVersion, ushort clientMaxVersion, ushort serverAgreedVersion)
Parameters
serverPublicKeybyte[]Server's ephemeral X25519 public key (32 bytes).
clientKeyPairCryptoHelper.X25519EphemeralKeyPairClient's ephemeral keypair. Private key is zeroed after use.
clientMinVersionushortMinimum protocol version the client supports.
clientMaxVersionushortMaximum protocol version the client supports.
serverAgreedVersionushortProtocol version the server agreed to.
Returns
- HandshakeService.ClientKeyAgreementResult
Result containing session keys; or
Success=falseon failure.
ComputeHandshakeCookie(string, byte[], uint, byte[], int)
Computes a stateless HMAC-SHA256 handshake cookie binding the client's IP, public key, and time bucket. The server stores no state — validity is re-derived on echo.
public static byte[] ComputeHandshakeCookie(string remoteIp, byte[] clientPublicKey, uint timeBucket, byte[] hmacKey, int connectionId = -1)
Parameters
remoteIpstringCanonical remote IP address string.
clientPublicKeybyte[]Client's X25519 public key (32 bytes).
timeBucketuintTime bucket index from GetTimeBucket().
hmacKeybyte[]HMAC-SHA256 signing key.
connectionIdintOptional connection-specific identifier (e.g., transport connection ID) to distinguish clients behind shared NAT. Pass -1 to omit.
Returns
- byte[]
32-byte HMAC cookie.
Remarks
Input structure: [domain | timeBucket(4B) | ipLen(2B) | ipBytes | hasConnId(1B) | connId(4B if present) | publicKey].
Replay: Cookies are not single-use — a valid cookie may be replayed within its validity window (up to 2x CookieTimeBucketSeconds). Replay only permits re-attempting the ECDH handshake, which is still gated by per-IP and global rate limits. IMPORTANT: Those rate limits are NOT implemented by this service — the transport layer (e.g., FishNet broadcast handlers) MUST enforce per-IP connection throttling and a global handshake cap. Without them, the cookie challenge provides only minimal amplification protection against DDoS.
Replay hardening (optional): To further limit replay:
(A) rotate hmacKey periodically via HKDF(masterKey, timeWindow)
so captured cookies become invalid after rotation;
(B) include a per-boot server salt in the HMAC key derivation to invalidate
cookies across server restarts;
(C) require lightweight proof-of-work (hashcash) tied to the cookie.
GetTimeBucket()
Returns the current UTC time bucket index used for cookie expiration.
public static uint GetTimeBucket()
Returns
NormalizeIp(string)
Normalises a raw IP address string to its canonical form via IPAddress.
IPv4-mapped IPv6 addresses (e.g., ::ffff:192.168.1.1) are collapsed to plain IPv4
so that both representations share a single rate-limit and cookie identity.
Returns an empty string for null/unparseable input.
public static string NormalizeIp(string rawIp)
Parameters
rawIpstringRaw IP address string from the transport layer (must be the actual socket endpoint).
Returns
- string
Canonical IP string, or empty string if invalid.
Remarks
Trust model: Callers MUST pass the actual socket remote endpoint address,
NOT values from client-controlled headers such as X-Forwarded-For or
X-Real-IP, unless those headers are authenticated by a trusted reverse proxy.
Accepting spoofed headers would allow attackers to bypass per-IP rate limits and
forge cookie bindings.
When operating behind a load balancer or NAT gateway, consider binding cookies to an additional connection-specific identifier (e.g., connection ID or port) to distinguish clients that share the same external IP.
ServerPerformKeyAgreement(byte[], ushort, ushort)
Performs server-side X25519 ECDH key agreement. Negotiates protocol version from the client's advertised range, computes a transcript hash with domain separation and version binding, and derives directional AES-256 session keys via HKDF.
public static HandshakeService.ServerKeyAgreementResult ServerPerformKeyAgreement(byte[] clientPublicKey, ushort clientMinVersion, ushort clientMaxVersion)
Parameters
clientPublicKeybyte[]Client's ephemeral X25519 public key (32 bytes).
clientMinVersionushortMinimum protocol version the client supports.
clientMaxVersionushortMaximum protocol version the client supports.
Returns
- HandshakeService.ServerKeyAgreementResult
Result containing server public key, session keys, and agreed version; or
Success=falseon failure.
Remarks
Both ephemeral keypairs are discarded after use for forward secrecy.
The transcript hash binds both public keys and the full version negotiation to prevent cross-protocol replay and version downgrade attacks.
Low-order key rejection: Small-subgroup/low-order public keys are
detected by DeriveSharedSecret(byte[], byte[]),
which throws CryptographicException when the raw ECDH output is
all-zero (RFC 7748 §6.1). This method catches that exception and returns
Success=false.
VerifyHandshakeCookie(byte[], string, byte[], uint, byte[], int)
Verifies a handshake cookie against a specific time bucket in constant time.
public static bool VerifyHandshakeCookie(byte[] cookie, string remoteIp, byte[] clientPublicKey, uint timeBucket, byte[] hmacKey, int connectionId = -1)
Parameters
cookiebyte[]Cookie bytes received from the client.
remoteIpstringCanonical remote IP address string.
clientPublicKeybyte[]Client's X25519 public key (32 bytes).
timeBucketuintTime bucket index to verify against.
hmacKeybyte[]HMAC-SHA256 signing key.
connectionIdintOptional connection-specific identifier to match ComputeHandshakeCookie(string, byte[], uint, byte[], int).
Returns
- bool
trueif the cookie is valid for this time bucket.
VerifyHandshakeCookieWithRollover(byte[], string, byte[], byte[], int)
Verifies a handshake cookie against the current and immediately preceding time bucket. Tolerates bucket-boundary crossings by checking both.
public static bool VerifyHandshakeCookieWithRollover(byte[] cookie, string remoteIp, byte[] clientPublicKey, byte[] hmacKey, int connectionId = -1)
Parameters
cookiebyte[]Cookie bytes received from the client.
remoteIpstringCanonical remote IP address string.
clientPublicKeybyte[]Client's X25519 public key (32 bytes).
hmacKeybyte[]HMAC-SHA256 signing key.
connectionIdintOptional connection-specific identifier to match ComputeHandshakeCookie(string, byte[], uint, byte[], int).
Returns
- bool
trueif the cookie is valid.
VerifyKeyConfirmation(byte[], byte[])
Verifies a peer's key confirmation MAC in constant time. Call after receiving the peer's confirmation tag to authenticate that both sides derived identical session keys.
public static bool VerifyKeyConfirmation(byte[] received, byte[] expected)
Parameters
receivedbyte[]The confirmation tag received from the peer.
expectedbyte[]The locally computed expected confirmation tag (from ExpectedClientKeyConfirmation or ExpectedServerKeyConfirmation).
Returns
- bool
trueif the tags match.