Table of Contents

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

int

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

ushort

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

serverPublicKey byte[]

Server's ephemeral X25519 public key (32 bytes).

clientKeyPair CryptoHelper.X25519EphemeralKeyPair

Client's ephemeral keypair. Private key is zeroed after use.

clientMinVersion ushort

Minimum protocol version the client supports.

clientMaxVersion ushort

Maximum protocol version the client supports.

serverAgreedVersion ushort

Protocol version the server agreed to.

Returns

HandshakeService.ClientKeyAgreementResult

Result containing session keys; or Success=false on 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

remoteIp string

Canonical remote IP address string.

clientPublicKey byte[]

Client's X25519 public key (32 bytes).

timeBucket uint

Time bucket index from GetTimeBucket().

hmacKey byte[]

HMAC-SHA256 signing key.

connectionId int

Optional 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

uint

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

rawIp string

Raw 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

clientPublicKey byte[]

Client's ephemeral X25519 public key (32 bytes).

clientMinVersion ushort

Minimum protocol version the client supports.

clientMaxVersion ushort

Maximum protocol version the client supports.

Returns

HandshakeService.ServerKeyAgreementResult

Result containing server public key, session keys, and agreed version; or Success=false on 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

cookie byte[]

Cookie bytes received from the client.

remoteIp string

Canonical remote IP address string.

clientPublicKey byte[]

Client's X25519 public key (32 bytes).

timeBucket uint

Time bucket index to verify against.

hmacKey byte[]

HMAC-SHA256 signing key.

connectionId int

Optional connection-specific identifier to match ComputeHandshakeCookie(string, byte[], uint, byte[], int).

Returns

bool

true if 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

cookie byte[]

Cookie bytes received from the client.

remoteIp string

Canonical remote IP address string.

clientPublicKey byte[]

Client's X25519 public key (32 bytes).

hmacKey byte[]

HMAC-SHA256 signing key.

connectionId int

Optional connection-specific identifier to match ComputeHandshakeCookie(string, byte[], uint, byte[], int).

Returns

bool

true if 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

received byte[]

The confirmation tag received from the peer.

expected byte[]

The locally computed expected confirmation tag (from ExpectedClientKeyConfirmation or ExpectedServerKeyConfirmation).

Returns

bool

true if the tags match.