How `serverPubKey` Eliminates MITM Vulnerabilities in MySQL RSA Authentication

The serverPubKey DSN parameter in go-sql-driver/mysql prevents man-in-the-middle attacks during password encryption by allowing the client to use a pre-registered RSA public key instead of requesting one from the server.

The go-sql-driver/mysql package encrypts passwords using RSA public key cryptography when authenticating with sha256_password or caching_sha2_password plugins. By default, the driver requests the server's public key over the network, creating a vulnerability window where an attacker could substitute a malicious key. The serverPubKey parameter closes this gap by enabling out-of-band key verification that bypasses the network request entirely.

The MITM Risk of Default RSA Key Exchange

When the driver does not know the server's public key in advance, it follows a default RSA key exchange flow that exposes a man-in-the-middle (MITM) attack vector. In auth.go, the encryption process follows three steps:

  1. The client sends a request for the server's public key.
  2. The server replies with a PEM-encoded RSA key.
  3. The driver encrypts the password using rsa.EncryptOAEP (via the encryptPassword helper).

This round-trip is functional but creates a critical vulnerability: an attacker who can intercept the packet could supply a malicious RSA key. If the client encrypts the password using the attacker's key, the attacker can decrypt it and compromise the credentials.

How serverPubKey Secures the Authentication Channel

The driver provides a safer alternative through the serverPubKey DSN parameter, which eliminates the MITM window by removing the network request entirely. Instead of fetching the key from the server, the driver uses a key that the client has verified and registered out-of-band.

Registering Trusted Keys in the Driver

Before connecting, applications register their trusted RSA public keys using RegisterServerPubKey. According to the implementation in auth.go (lines 31-39), this function stores the key in the serverPubKeyRegistry:

func RegisterServerPubKey(name string, pubKey *rsa.PublicKey) {
    serverPubKeyRegistry.Lock()
    defer serverPubKeyRegistry.Unlock()
    serverPubKeyRegistry.keys[name] = pubKey
}

The registry is protected by a sync.RWMutex, making it safe for concurrent access across multiple goroutines. To retrieve a key, the driver uses getServerPubKey (lines 78-85 in auth.go), which acquires a read-lock before accessing the map.

DSN Parsing and Key Resolution

When a DSN contains serverPubKey=<name>, the driver resolves this name during configuration initialization. In dsn.go (lines 23-30), the parser extracts the value and stores it in cfg.ServerPubKey. During the normalize() method (lines 23-28), the driver calls getServerPubKey(cfg.ServerPubKey) to fetch the actual RSA key and assigns it to cfg.pubKey:

if cfg.ServerPubKey != "" {
    cfg.pubKey = getServerPubKey(cfg.ServerPubKey)
}

This resolution happens before any network connection is established, ensuring the key is available immediately during authentication.

Bypassing the Network Request in Authentication

During authentication, both supported plugins check for the presence of cfg.pubKey before requesting a key from the server. For the sha256_password path in auth.go (lines 24-30), the driver uses the pre-cached key directly:

pubKey := mc.cfg.pubKey
if pubKey == nil {
    // Only request from server if not pre-registered
    pubKey, err = mc.requestServerPubKey()
}

Similarly, in the caching_sha2_password authentication block (lines 23-31), the driver checks cfg.pubKey first. If the key is present, it encrypts the password immediately using rsa.EncryptOAEP; otherwise, it falls back to the vulnerable request flow. By pre-registering the key, the driver never executes the request path, eliminating the MITM attack surface.

Complete Implementation Example

The following example demonstrates loading a PEM-encoded public key, registering it with the driver, and configuring the DSN to use serverPubKey:

package main

import (
	"crypto/rsa"
	"crypto/x509"
	"database/sql"
	"encoding/pem"
	"log"
	"os"

	"github.com/go-sql-driver/mysql"
)

func main() {
	// 1. Load the server's RSA public key from a trusted source (e.g., verified PEM file)
	data, err := os.ReadFile("myserver_pub.pem")
	if err != nil {
		log.Fatal(err)
	}
	block, _ := pem.Decode(data)
	if block == nil || block.Type != "PUBLIC KEY" {
		log.Fatal("invalid PEM block")
	}
	pubIfc, err := x509.ParsePKIXPublicKey(block.Bytes)
	if err != nil {
		log.Fatal(err)
	}
	pubKey, ok := pubIfc.(*rsa.PublicKey)
	if !ok {
		log.Fatal("not an RSA public key")
	}

	// 2. Register the key under a logical name
	mysql.RegisterServerPubKey("myserver", pubKey)

	// 3. Configure DSN to use the pre-registered key
	//    The driver will use the verified key and never request one from the server
	dsn := "user:pass@tcp(localhost:3306)/dbname?serverPubKey=myserver"

	db, err := sql.Open("mysql", dsn)
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	// Connection established with MITM-resistant password encryption
}

This pattern guarantees that only a key verified out-of-band is used for encryption, protecting against malicious key substitution during the handshake.

Summary

  • The default RSA key exchange mechanism requests the server's public key over the network, creating a MITM vulnerability where an attacker can substitute a malicious key.
  • The serverPubKey parameter enables pre-registration of trusted RSA keys via RegisterServerPubKey, storing them in a thread-safe registry (serverPubKeyRegistry) in auth.go.
  • During DSN parsing in dsn.go, the driver resolves the key name to an actual RSA key and stores it in cfg.pubKey before connection.
  • Both sha256_password and caching_sha2_password authentication paths use the pre-cached key from cfg.pubKey, bypassing the network request and eliminating the MITM attack window.

Frequently Asked Questions

How does serverPubKey prevent man-in-the-middle attacks compared to the default mechanism?

The default mechanism requests the RSA public key from the server during authentication, allowing an attacker to intercept the request and substitute a malicious key that would decrypt the password. The serverPubKey parameter uses a key registered out-of-band via RegisterServerPubKey, eliminating the network request and ensuring the client only encrypts data with a verified key.

Which MySQL authentication plugins utilize the serverPubKey parameter?

According to the implementation in auth.go, both the sha256_password and caching_sha2_password authentication paths check for the presence of cfg.pubKey. If the key is present (resolved from the serverPubKey DSN parameter), both plugins use it directly via rsa.EncryptOAEP instead of requesting a key from the server.

Is the public key registry thread-safe for concurrent access?

Yes. The serverPubKeyRegistry accessed via RegisterServerPubKey and getServerPubKey in auth.go is protected by a sync.RWMutex. The registry uses Lock() for writes during registration and RLock() for reads during key retrieval, making it safe for concurrent use across multiple goroutines.

When does the driver resolve the serverPubKey DSN name to an actual RSA key?

During the configuration normalization phase in dsn.go (specifically within the normalize() method at lines 23-28), the driver calls getServerPubKey() to resolve the name specified in the DSN to the registered RSA key. It stores the result in cfg.pubKey before any network connection is attempted, ensuring the key is available at authentication time.

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 →