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:
- The client sends a request for the server's public key.
- The server replies with a PEM-encoded RSA key.
- The driver encrypts the password using
rsa.EncryptOAEP(via theencryptPasswordhelper).
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
serverPubKeyparameter enables pre-registration of trusted RSA keys viaRegisterServerPubKey, storing them in a thread-safe registry (serverPubKeyRegistry) inauth.go. - During DSN parsing in
dsn.go, the driver resolves the key name to an actual RSA key and stores it incfg.pubKeybefore connection. - Both
sha256_passwordandcaching_sha2_passwordauthentication paths use the pre-cached key fromcfg.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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →