Who are you? The history of MySQL and MariaDB authentication protocols from 1997 to 2017
MySQL 3.20 to 4.0
In the good old days, when 32MB of RAM justified the name my-huge.cnf
, when nobody knew Google and Facebook didn’t even exist, security was… how do I put it… kind of cute. Computer viruses didn’t steal millions and didn’t disrupt elections — they played Yankee Doodle or told you not to play with the PC. People used telnet and ftp, although some security conscious admins already knew ssh.
Somewhere around this time, give or take a few years, MySQL was born. And it had users, who had to be kept away from seeing others’ data, but allowed to use their own. This called for authentication.
Michael Widenius (or Monty) apparently was aware that some users are paranoid about security. The proof can be found even in early MySQL versions:
/* Paranoid settings. Define I_AM_PARANOID if you are paranoid */ #ifdef I_AM_PARANOID #define DONT_ALLOW_USER_CHANGE 1 #define DONT_USE_MYSQL_PWD 1 #endif
This is from the global.h
of MySQL–3.21. It should come as no surprise that MySQL never sent passwords from the client to the server in clear text. It sent random bytes that were generated from hashes of passwords. Technically, the first MySQL authentication protocol (as in MySQL–3.20, 1996) worked as follows:
- Server has stored the password hash in the
mysql.user
table. The hash function was rather simple, though:for (; *password ; password++) { tmp1 = *password; hash ^= (((hash & 63) + tmp2) * tmp1) + (hash << 8); tmp2 += tmp1; }
Note, that the hash value was only 32 bits!
- During the authentication, the server started the handshake with a string of 8 random letters (called scramble).
- The client calculated the hash (as above) of this scramble, and the hash of the password. XOR of these two numbers produced one 32-bit seed that was used to initialize a pseudo-random number generator. That generator generated “random” 8 bytes and they were sent to the server.
- The server, basically, repeated the same — it knew the scramble, it had the hash of a password from the
mysql.user
table. So it also initialized a random number generator, generated 8 bytes, and compared them with what the client had sent.
This wasn’t a bad protocol. It had obvious strengths, for example, the password was never sent in clear. And was never stored in clear either. But, seriously, 32-bit? That wasn’t enough even in 1996. Which is why the next major MySQL release — 3.21 — used 64-bit hashes. Otherwise the protocol stayed the same. And it is still present (although not the default) in MySQL–5.6 and MariaDB–10.2. Luckily, it was removed from MySQL–5.7. I really hope nobody uses it nowadays.
MySQL 4.1 to 5.7
The main flaw of this protocol, as we suddenly realized in the early 2000, was that it stored the password in plain text. No kidding. It kind of stored the hash, but the client only needed the hash for authentication, not the password. That is, if someone could read password hashes from the mysql.user
table, it would only take a small modification of the client library to use these hashes directly and to login as just anyone.
Also this hash function was rather suspicious. Not that we knew how to reverse it, but we still didn’t fully trust it. And indeed, it was cracked later, but by that time we already had a replacement.
For the new protocol we (that is, me, Konstantin Osipov, Peter Zaitsev, and a few others) set the following goals:
- Sniffing the authentication should not allow the attacker to authenticate as anyone
- Stealing the whole
mysql.user
table should not allow the attacker to authenticate as anyone - Bonus point: use a proven well-known cryptographic hash function
And this is how the double-SHA1 (or new or mysql_native_password) protocol was created. It was first introduced in MySQL–4.1 and is still the most widely used MySQL authentication protocol. Every MySQL and MariaDB version supports it. It works as follows:
- The server stores
SHA1(SHA1(password))
in themysql.user
table. - For authentication the server sends a random 20-letter scramble.
- The client computes the following:
SHA1( scramble || SHA1( SHA1( password ) ) ) ⊕ SHA1( password )
where
⊕
is XOR and||
is string concatenation. And sends it to the server. - The server doesn’t know
SHA1(password)
, but it knows the scramble andSHA1(SHA1(password))
, so it can calculate the first part of the expression, and with a XOR it can getSHA1(password)
. - Now all it needs to do is to calculate the SHA1 of
SHA1(password)
from the previous step, and it can compare the result with what is stored in themysql.user
table. Mission accomplished.
This protocol achieved all goals — sniffing the authentication handshake or stealing the mysql.user
table would not help the attacker to impersonate users. Still, it wasn’t perfect. The server received SHA1(password)
(so it could show up in core dumps, be extracted from the server memory, etc) and that was sufficient to impersonate a user. Furthermore, if someone was able to sniff the authentication handshake and steal the mysql.user
table, he would be able to repeat all steps that server did, extract SHA1(password)
and impersonate a legitimate user too. While it was a flaw, it was not a major issue — when one has password hashes it’s usually easier just to brute-force them. And I had a feeling that this flaw was impossible to fix anyway, unless we resort to public key cryptography.
MySQL 5.7 and up
Our users relied on double-SHA1 authentication. Life went on. MySQL was acquired by Sun, Sun was acquired by Oracle. MariaDB took off and grew in popularity. And, unrelated to all that, SHA1 was gradually considered less and less secure. Something had to be done. MySQL was the first to offer a replacement. A task of implementing a new authentication plugin was delegated to Kristofer Pettersson. I wasn’t in MySQL at that time, so I cannot know who he was discussing it with and what his goals were. But the basics are clear — move away from SHA1 and remove this last flaw. He correctly decided to use public key cryptography. So, the new MySQL 5.7 authentication protocol (sha256_password plugin) uses SHA256 (the 256-bit version of SHA2) and RSA. Together it works like this:
- The server stores
SHA256(password)
, yay! - During authentication it sends to the client a 20-letter scramble, just as before.
- Client reads the server’s public RSA key from a file that was distributed to the client in advance.
- Client calculates XOR of the password and the scramble (repeated as necessary to cover the whole password), encrypts it using the server’s public key, and sends to the server.
- Server decrypts the data using its secret RSA key, XORs the result with a scramble to extract the client’s plain-text password, calculates SHA2 of it, and compares that with the stored value from the
mysql.user
table.
Quite straightforward. There is just one nuisance. One has to distribute the server’s public key to all clients. And every client needs to have public keys for all servers it wants to connect to and juggle them as needed. I could see where it could become rather annoying, and probably that’s why if the client does not have the server’s public key, the server helpfully provides it, during authentication, at the cost of one round-trip. Of course, no security-conscious person should ever rely on that, how would the client know that the public key is authentic? A man-in-the-middle can replace it with his own public key and he’ll be able to obtain the client password in plain-text! And again, not that it’s particularly bad, but the server still gets the password in plain-text. Just like it did in all previous authentication protocols.
MariaDB 10.1 and up
But MariaDB didn’t have even that. We were still relying on the old proven double-SHA1 protocol. Banks were grumbling, but it was still secure enough. Nevertheless, in the light of recent news we’ve also started looking for alternatives. Double-SHA1 is still safe, mind you, even after taking recent research into account. The research shows than one can generate SHA1 collisions now, meaning one can generate two passwords that will have the same SHA1 hash. That’s bad for SHA1, but doesn’t really weaken our authentication. Still… It shows that SHA1 doesn’t have that much time left, eventually someone might find some way to break it.
I’ve also built the new authentication protocol using public key cryptography. With the goal that not the traffic sniffing, not the mysql.user
table, not both together, not even a fully compromised server would be able to recover the password. And the behavior should stay as before — no files to distribute, no new round-trips in the protocol. The result is a protocol that uses ed25519 — elliptic curve digital signature algorithm that uses Edwards curve over the finite field of order 2255–19 (that’s why the algorithm is called Ed-255-19). Both the curve and the algorithm itself were invented by the well-known Daniel J. Bernstein (or, simply, djb). He and his team also created a few reference public domain implementations of ed25519, one of which is used in OpenSSH. The new MariaDB authentication protocol also uses one of these reference ed25519 implementations, and works like this:
- The user’s password is the secret key. We calculate
SHA512(password)
and applying some math magic convert it into a public key. This public key is stored by the server in themysql.user
table. - For authentication the server sends a random 32-byte scramble to the client.
- The client signs it with its secret key.
- The server verifies the signature with the user’s public key.
That’s all! Much simpler than with double-SHA1. And it’s as secure as it can be, the password is not stored on the server, not sent anywhere, the server doesn’t even see it at any point in time and cannot restore it from the information it has. Nothing for the man-in-the-middle here either. No files. Same number of round-trips. One can still brute-force passwords, given the mysql.user
table, but there’s nothing we can do about that.
This new protocol was first introduced in MariaDB–10.1.22 as a plugin, and it will be more conveniently integrated into the server in future versions.
“And it had users, who had to be kept away from seeing others’ data, but allowed to use their own. This called for authentication.” Technically authorization.
Yes and no. This is authorization, you’re right.
But the blog says that it *requires* authentication, not that it *is* authentication.
Will MariaDB’s client library be compatible with MySQL’s sha256-based authentication?
Up until now MariaDB’s client library could log in to MySQL with no issue. But with SHA1 going away, my team is wondering if we’re about to run into compatibility problems.
Yes. https://jira.mariadb.org/browse/CONC-229 is about adding SHA256 support to Connector/C — it’s closed, so the feature must’ve been already implemented. https://jira.mariadb.org/browse/CONJ-327 is about adding SHA256 support to Connector/J — it’s not closed yet, so this is planned, but not yet implemented.
‘public key(sha512(guess))’ is still very cheap to compute, allowing easy brute forcing of human-generated passwords. Why not use some parameterized hash function and pass those parameters along with the challenge (assuming the client defends against trivial parameters)? e.g. PBKDF2 or Argon2?
Correct. I’ve listed the goals of the new authentication in the blog, and strong protection against brute forcing wasn’t there.
For two reasons, one can easily generate a complex password (and as it’s stored on the client, not manually typed it, it does not matter that it’s impossible to remember).
And because we’ve tried to use salted passwords in MySQL 4.0 and it was a huge pain and we had to revert that in the next minor release — users couldn’t cope that PASSWORD(“foo”) became non-deterministic, returning different hashes when invoked many times. Although I hope that times have changed and now we give salted passwords another try.
Thanks!
Is there any form of downgrade protection? I see –secure-auth, but that seems for only very old versions. Does the default client support restricting permitted auth to ed25519? (maybe a custom plugin directory with only populated the desired plugins)
No. And mysql_native_password is built-in, one cannot really delete “mysql_native_password.so” to prevent downgrades.
This is a good question, for which we don’t have a good answer at the moment…
Please, feel free to suggest a new feature at https://jira.mariadb.org
Done: https://jira.mariadb.org/browse/MDEV-16256