Designing a Distributed System for an Online Multiplayer Game — Security Patch (Part 9)

Key Management

The game manager generates a new RSA key for each game, encrypts it with a shared key (AES), and stores it in the Redis for further requests.

Solution

There is no need to store the game's private key; keeping the public key is enough. Because the game manager API server only serves the game server's public key.

Man In Middle Attack

The public key is transferred over TLS, but It's vulnerable. In this attack, the attacker positions himself between the client and the server. When the client sends a request to the server to handshake, the attacker replays the request to the server and downloads the valid public key. Then it returns its owned public key to the client. The client encrypts its shared secret (AES key) with the attacker's public key and sends it to the server. The attacker decrypts the request via its private key and gets access to the client's secret key. Thus, the attacker can pack the request again, encrypt it with the server's public key, and send it to the server. Finally, the attacker can decrypt the server handshake response with the session ID and BOOM!

Solution

To cope with this attack, we should follow the same manner used in the browsers. Certificate authority! The game manager generates a new private key for each game and signs it with the master secret key. The master public key is stored in the game client to validate the game server's public key. Thus the attacker's public key is invalid because it has not a valid signature from the master private key! Puff!

Implementation

AWS also provides a private CA service. So we can store the master private key, an intermediate issuer certificate to issue the game servers' certificates. But I was not able to implement and test it locally because the localstack doesn't support ACM-PC yet.

type CA interface {
IssueCertificate(ctx context.Context, config NewCertConfig) (*Certificate, error)
CertPEM() []byte
}

type NewCertConfig struct {
OrganizationalUnit []string
Country []string
Locality []string
SerialNumber *big.Int
KeyID []byte
NotBefore time.Time
NotAfter time.Time
PrivateKey *rsa.PrivateKey
CommonName string
}

type Certificate struct {
PrivateKey *rsa.PrivateKey
PEM []byte
}
type LocalCAManager struct {
privateKey *rsa.PrivateKey
cert *x509.Certificate
certBytes []byte
}

type CAManagerConfig struct {
Organization []string
Country []string
Locality []string
PrivateKey *rsa.PrivateKey
SerialNumber *big.Int
NotBefore time.Time
NotAfter time.Time
}

func NewLocalCAManager(config CAManagerConfig) (*LocalCAManager, error) {
if config.PrivateKey == nil {
return nil, errors.New("invalid private key")
}

caManager := LocalCAManager{
privateKey: config.PrivateKey,
cert: &x509.Certificate{
SerialNumber: config.SerialNumber,
Subject: pkix.Name{
Organization: config.Organization,
Country: config.Country,
Locality: config.Locality,
},
NotBefore: config.NotBefore,
NotAfter: config.NotAfter,
IsCA: true,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
BasicConstraintsValid: true,
},
}

var err error
caManager.certBytes, err = x509.CreateCertificate(rand.Reader, caManager.cert, caManager.cert, &caManager.privateKey.PublicKey, caManager.privateKey)
if err != nil {
return nil, err
}

return &caManager, nil
}
func (l *LocalCAManager) IssueCertificate(ctx context.Context, config NewCertConfig) (*Certificate, error) {
var cert Certificate

if config.PrivateKey == nil {
reader := rand.Reader
var err error
cert.PrivateKey, err = rsa.GenerateKey(reader, defaultKeySize)
if err != nil {
return nil, err
}
}

x509Cert := &x509.Certificate{
SerialNumber: config.SerialNumber,
Subject: pkix.Name{
Organization: l.cert.Issuer.Organization,
OrganizationalUnit: config.OrganizationalUnit,
Country: config.Country,
Locality: config.Locality,
CommonName: config.CommonName,
},
NotBefore: config.NotBefore,
NotAfter: config.NotAfter,
SubjectKeyId: config.KeyID,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
KeyUsage: x509.KeyUsageDigitalSignature,
}

certBytes, err := x509.CreateCertificate(rand.Reader, x509Cert, l.cert, &cert.PrivateKey.PublicKey, l.privateKey)
if err != nil {
return nil, err
}

certPEM := new(bytes.Buffer)
pem.Encode(certPEM, &pem.Block{
Type: "CERTIFICATE",
Bytes: certBytes,
})

cert.PEM = certPEM.Bytes()

return &cert, nil
}
private bool ValidateCertificate(X509Certificate2 serverCert)
{
X509Chain chain = new X509Chain();

// The online or offline revocation is not needed
chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;

// By default, the Build method validates the certificate using system certificates, so set the VerificationFlags to AllowUnknownCertificateAuthority for private CA
chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;
chain.ChainPolicy.VerificationTime = DateTime.Now;
chain.ChainPolicy.UrlRetrievalTimeout = new TimeSpan(0, 0, 0);

// Add the root here. If any intermediate certificate exists, it must be added from bottom to top to ExtraStore as well. e.g. intermediate2, intermediate1, root
chain.ChainPolicy.ExtraStore.Add(RootCA);

var validCert = chain.Build(serverCert)
if (!validCert)
{
throw new Exception("server certificate is invalid");
}

var validChain = chain.ChainElements
.Cast<X509ChainElement>()
.Any(x => x.Certificate.Thumbprint == authority.Thumbprint);

if (!validChain)
{
throw new Exception("trust chain is invalid. thumbprints did not match.");
}

return true;
}

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Sajjad Rad

Sajjad Rad

Currently a software engineer, always an adventurer