Security Implications of Using fabrica-util's RSA and ECDH for Key Exchange in Game Servers

Using fabrica-util's RSA and ECDH primitives for game server key exchange introduces specific risks: RSA's reliance on PKCS#1 v1.5 padding creates Bleichenbacher oracle vulnerabilities, while Curve25519 ECDH lacks authentication and requires RSA signatures to prevent man-in-the-middle attacks.

The go-pantheon/fabrica-util repository provides cryptographic building blocks for Go-based game servers, implementing RSA encryption in security/rsa/rsa.go and Curve25519 ECDH in security/ecdh/curve25519.go. Understanding the security implications of combining these primitives is critical for establishing secure player sessions without introducing padding oracle attacks or unauthenticated key exchanges.

Architectural Overview

The fabrica-util library separates asymmetric operations into two distinct packages that are typically combined to establish secure sessions.

RSA Implementation

In security/rsa/rsa.go, the library exposes Encrypt, Decrypt, ParsePublicKey, and ParsePrivateKey functions. The test suite in security/rsa/rsa_test.go demonstrates that these functions utilize rsa.EncryptPKCS1v15 and rsa.SignPKCS1v15 for padding and signature operations. This implementation is designed for protecting small payloads—such as symmetric session keys—and verifying the authenticity of exchanged data through digital signatures.

Curve25519 ECDH Implementation

The security/ecdh/curve25519.go file implements high-performance elliptic-curve Diffie-Hellman using GenKeyPair, ParseKey, ComputeSharedKey, and ComputePubKey. These functions enable forward secrecy by generating ephemeral key pairs where GenKeyPair reads 32 random bytes from crypto/rand, ensuring high-entropy private keys that are never transmitted across the network.

Security Implications of RSA in fabrica-util

PKCS#1 v1.5 Padding Vulnerabilities

The library's use of rsa.EncryptPKCS1v15 and rsa.SignPKCS1v15 (as seen in the Encrypt function and test implementations) relies on PKCS#1 v1.5 padding, which is not recommended for new designs. This padding scheme is vulnerable to Bleichenbacher-style adaptive chosen-ciphertext attacks.

Implication for game servers: An attacker who can provoke the server to decrypt malformed ciphertexts—such as by sending malformed login packets—may recover plaintext session keys or mount padding-oracle attacks. In security/rsa/rsa.go, the absence of EncryptOAEP means the library does not expose RSA-OAEP, which provides stronger resistance to chosen-ciphertext attacks.

Key Size and Performance Trade-offs

The test files demonstrate key generation for both 2048-bit and 4096-bit RSA keys. While 2048 bits represents the modern minimum, 4096 bits offers superior security against factorization attacks. For real-time game servers, however, 4096-bit operations introduce significant CPU overhead that may increase latency during high-concurrency connection spikes.

Private Key Management Risks

The implementation expects callers to maintain the *rsa.PrivateKey in memory. Exposing this key through logging, insecure serialization, or core dumps would compromise all current and future sessions relying on that key pair. The library itself does not provide hardware security module (HSM) integration or automatic memory clearing.

Security Implications of Curve25519 ECDH

Forward Secrecy Strengths

Curve25519 is a well-studied elliptic curve that provides forward secrecy: compromise of the server's long-term RSA signing key does not reveal past session secrets derived from ephemeral ECDH exchanges. The ComputeSharedKey function in security/ecdh/curve25519.go generates unique shared secrets for each session, ensuring that stolen session keys cannot decrypt historical traffic.

Public Key Validation Limitations

The ParseKey function validates that public keys are exactly 32 bytes in length. However, it does not perform explicit subgroup validation checks. Because Curve25519 has a cofactor of 8, scalar multiplication inherently maps invalid points to valid curve points. While this prevents certain small-subgroup attacks, a hostile client could transmit malformed public keys that result in low-entropy shared secrets. Applications should validate derived secrets (e.g., by checking hash equality) before use.

Unauthenticated Key Exchange Vulnerability

ECDH alone is unauthenticated. Without additional verification, an attacker can perform a man-in-the-middle (MITM) attack by substituting their own public key during the exchange phase. The fabrica-util implementation does not include built-in authentication mechanisms within the ComputeSharedKey workflow, requiring external verification—typically RSA signatures—to bind the ECDH public key to the server's identity.

Combined Threat Model for Game Servers

When deploying both primitives together, specific attack vectors emerge that require architectural mitigation.

Threat Vector Mitigation Strategy using fabrica-util
Passive eavesdropping Use ecdh.GenKeyPair to derive fresh symmetric keys per session, then encrypt traffic with AES-GCM using the shared secret.
Active MITM Sign the ECDH public key (from ecdh.KeyToBytes) with the server's RSA private key using rsa.SignPKCS1v15; clients verify with rsa.VerifyPKCS1v15 before computing shared secrets.
Replay attacks Include a per-session nonce or timestamp in the RSA-signed payload; reject messages with stale nonces at the application layer.
Denial-of-service Reserve RSA operations only for signing initial ECDH public keys; use lightweight ECDH operations for the bulk of key exchange to prevent CPU exhaustion from 4096-bit RSA decryption operations.

Secure Implementation Pattern

The following production-ready pattern demonstrates how to safely combine these primitives to establish authenticated, forward-secret channels.

Server-Side: Generate and Sign ECDH Public Keys

First, generate a long-term RSA key pair for signing (typically 4096-bit for high-security game servers):

package main

import (
	"crypto/rand"
	"crypto/rsa"
	"log"
)

func generateRSAKey() *rsa.PrivateKey {
	key, err := rsa.GenerateKey(rand.Reader, 4096)
	if err != nil {
		log.Fatalf("RSA key generation failed: %v", err)
	}
	return key
}

During each player connection, generate an ephemeral Curve25519 key pair and sign the public key with RSA:

package main

import (
	"crypto"
	"crypto/rand"
	"crypto/rsa"
	"crypto/sha256"
	"encoding/base64"
	"log"

	"github.com/go-pantheon/fabrica-util/security/ecdh"
	utilrsa "github.com/go-pantheon/fabrica-util/security/rsa"
)

func serverKeyExchange(rsaPriv *rsa.PrivateKey) (pubKeyBase64 string, signatureBase64 string) {
	// Generate ephemeral Curve25519 key pair
	pri, pub, err := ecdh.GenKeyPair()
	if err != nil {
		log.Fatalf("ECDH key generation failed: %v", err)
	}

	// Serialize public key (32 bytes)
	pubBytes := ecdh.KeyToBytes(&pub)

	// Sign with RSA-PKCS1v15 (as implemented in fabrica-util)
	hashed := sha256.Sum256(pubBytes)
	sig, err := utilrsa.SignPKCS1v15(rand.Reader, rsaPriv, crypto.SHA256, hashed[:])
	if err != nil {
		log.Fatalf("RSA signing failed: %v", err)
	}

	return base64.RawURLEncoding.EncodeToString(pubBytes),
		base64.RawURLEncoding.EncodeToString(sig)
}

Client-Side: Verify and Compute Shared Secret

The client validates the server's identity and establishes the shared secret:

package main

import (
	"crypto"
	"crypto/rsa"
	"crypto/sha256"
	"encoding/base64"
	"log"

	"github.com/go-pantheon/fabrica-util/security/ecdh"
	utilrsa "github.com/go-pantheon/fabrica-util/security/rsa"
)

func clientKeyExchange(rsaPub *rsa.PublicKey, pubB64, sigB64 string) []byte {
	// Decode received values
	pubBytes, _ := base64.RawURLEncoding.DecodeString(pubB64)
	sigBytes, _ := base64.RawURLEncoding.DecodeString(sigB64)

	// Verify RSA signature using fabrica-util's implementation
	hashed := sha256.Sum256(pubBytes)
	if err := utilrsa.VerifyPKCS1v15(rsaPub, crypto.SHA256, hashed[:], sigBytes); err != nil {
		log.Fatalf("Signature verification failed: %v", err)
	}

	// Parse server's public key (validates 32-byte length)
	serverPub, err := ecdh.ParseKey(pubBytes)
	if err != nil {
		log.Fatalf("Invalid server public key: %v", err)
	}

	// Generate client ECDH key pair
	pri, _, err := ecdh.GenKeyPair()
	if err != nil {
		log.Fatalf("ECDH generation failed: %v", err)
	}

	// Compute shared secret
	secret, err := ecdh.ComputeSharedKey(pri, serverPub)
	if err != nil {
		log.Fatalf("Shared secret computation failed: %v", err)
	}
	return secret // Derive AES-GCM key from this secret
}

Summary

  • PKCS#1 v1.5 padding in fabrica-util's RSA implementation exposes game servers to Bleichenbacher oracle attacks; consider wrapping OAEP yourself or limiting RSA to signing operations only.
  • Curve25519 ECDH provides strong forward secrecy but requires authentication; always sign ecdh.GenKeyPair outputs with RSA to prevent MITM attacks.
  • Key size selection impacts performance; 4096-bit RSA offers better security but may degrade server performance under load compared to 2048-bit keys.
  • No built-in OAEP support means developers must implement additional padding schemes externally if chosen-ciphertext attack resistance is required for RSA encryption operations.
  • Combined RSA+ECDH architecture mitigates both passive eavesdropping and active interception when implemented with proper nonce handling and signature verification.

Frequently Asked Questions

Does fabrica-util support RSA-OAEP for improved security?

No, the security/rsa/rsa.go file in fabrica-util only exposes PKCS#1 v1.5 padding via EncryptPKCS1v15. If you require OAEP padding to mitigate Bleichenbacher attacks, you must implement it separately using Go's standard crypto/rsa package with EncryptOAEP, as the library does not provide this functionality natively.

How can I prevent man-in-the-middle attacks when using fabrica-util's ECDH?

Since ecdh.ComputeSharedKey performs unauthenticated Diffie-Hellman, you must bind the ephemeral public keys to verified identities. Use the rsa.SignPKCS1v15 function in security/rsa/rsa.go to sign the 32-byte public key output from ecdh.KeyToBytes, and have clients verify this signature with rsa.VerifyPKCS1v15 before calling ComputeSharedKey. Alternatively, migrate to the Noise protocol or TLS 1.3 for built-in authentication.

Is 2048-bit RSA sufficient for game server key exchange?

According to the test implementations in security/rsa/rsa_test.go, 2048-bit keys represent the minimum acceptable size, while 4096-bit keys provide superior security margins against future factorization attacks. However, 4096-bit operations consume significantly more CPU cycles; for high-throughput game servers, profile your authentication handshake latency before committing to larger key sizes.

What validation does fabrica-util perform on ECDH public keys?

The ecdh.ParseKey function in security/ecdh/curve25519.go validates that incoming public keys are exactly 32 bytes in length. It does not perform explicit subgroup validation checks because Curve25519's cofactor design inherently handles invalid points through scalar multiplication. However, you should verify the entropy of the resulting shared secret (e.g., by checking that ComputeSharedKey does not return all-zero bytes) to detect malicious input attempts.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →