你是谁?MySQL 和 MariaDB 认证协议从 1997 年到 2017 年的历史

MySQL 3.20 至 4.0

在过去美好的日子里,当 32MB 内存足以配得上 my-huge.cnf 这个名字时,当没人知道 Google、Facebook 甚至还没出现时,安全……怎么说呢……有点可爱。计算机病毒不会窃取数百万财产,也不会扰乱选举——它们会播放 Yankee Doodle 或告诉你不要玩电脑。人们使用 telnet 和 ftp,尽管一些有安全意识的管理员已经知道 ssh 了。

大约在这个时候,前后几年,MySQL 诞生了。它有了用户,这些用户的数据需要与他人隔离,但允许使用自己的数据。这就需要认证。

Michael Widenius(或 Monty)显然意识到有些用户对安全性偏执。即使在早期 MySQL 版本中也能找到证据

/* 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

这摘自 MySQL–3.21 的 global.h。毫不意外的是,MySQL 从未将密码从客户端明文发送到服务器。它发送的是从密码哈希生成的随机字节。从技术上讲,第一个 MySQL 认证协议(如 MySQL–3.20,1996 年)的工作方式如下

  • 服务器在 mysql.user 表中存储了密码哈希。不过,哈希函数相当简单
    for (; *password ; password++)
    {
      tmp1 = *password;
      hash ^= (((hash & 63) + tmp2) * tmp1) + (hash << 8);
      tmp2 += tmp1;
    }
    

    注意,哈希值只有 32 位!

  • 在认证过程中,服务器以一个由8 个随机字母组成的字符串(称为混淆字符串)开始握手。
  • 客户端计算了此混淆字符串的哈希(如上所述)以及密码的哈希。这两个数字的 XOR 运算产生了一个 32 位的种子,用于初始化一个伪随机数生成器。该生成器生成了“随机的”8 字节并发送到服务器。
  • 服务器基本上重复了同样的操作——它知道混淆字符串,并且从 mysql.user 表中获取了密码的哈希。因此,它也初始化了一个随机数生成器,生成了 8 个字节,并将其与客户端发送的进行比较。

这并不是一个糟糕的协议。它有明显的优点,例如,密码从未明文发送。也从未明文存储。但是,说真的,32 位?即使在 1996 年也不够。这就是为什么下一个主要的 MySQL 版本——3.21——使用了 64 位哈希。除此之外,协议保持不变。并且它仍然存在于(尽管不是默认的)MySQL–5.6 和 MariaDB–10.2 中。幸运的是,它已从 MySQL–5.7 中移除。我真心希望现在没有人使用它了。

MySQL 4.1 至 5.7

这个协议的主要缺陷,正如我们在 2000 年代初突然意识到的那样,是它将密码存储为明文。不开玩笑。它确实存储了哈希,但客户端进行认证时只需要哈希,而不是密码。也就是说,如果有人能够从 mysql.user 表中读取密码哈希,只需要对客户端库稍加修改,就可以直接使用这些哈希进行登录,冒充任何人。

此外,这个哈希函数也相当可疑。不是说我们知道如何反转它,但我们仍然不完全信任它。事实证明,它后来确实被破解了,但那时我们已经有了替代方案。

对于新协议,我们(即我、Konstantin Osipov、Peter Zaitsev 和其他几个人)设定了以下目标

  • 嗅探认证过程不应允许攻击者冒充任何人进行认证
  • 窃取整个 mysql.user 表不应允许攻击者冒充任何人进行认证
  • 额外目标:使用经过验证的知名加密哈希函数

这就是双 SHA1(或称为新协议mysql_native_password)协议的创建方式。它首次在 MySQL–4.1 中引入,至今仍是使用最广泛的 MySQL 认证协议。每个 MySQL 和 MariaDB 版本都支持它。其工作方式如下

  • 服务器在 mysql.user 表中存储 SHA1(SHA1(password))
  • 对于认证,服务器发送一个随机的 20 字母混淆字符串。
  • 客户端计算以下内容
    SHA1( scramble || SHA1( SHA1( password ) ) ) ⊕ SHA1( password )

    其中 是异或,|| 是字符串连接。并将其发送到服务器。

  • 服务器不知道 SHA1(password),但它知道混淆字符串和 SHA1(SHA1(password)),因此可以计算表达式的第一部分,并通过异或得到 SHA1(password)
  • 现在它只需要计算上一步得到的 SHA1(password) 的 SHA1 值,然后将其与存储在 mysql.user 表中的值进行比较。任务完成。

该协议实现了所有目标——嗅探认证握手或窃取 mysql.user 表都无法帮助攻击者冒充用户。尽管如此,它并非完美无缺。服务器收到了 SHA1(password)(因此它可能出现在核心转储中、从服务器内存中提取等),这足以冒充用户。此外,如果有人能够嗅探认证握手并且窃取 mysql.user 表,他将能够重复服务器所做的所有步骤,提取 SHA1(password) 并冒充合法用户。虽然这是一个缺陷,但并非主要问题——当一个人拥有密码哈希时,通常更容易直接暴力破解它们。而且我总觉得这个缺陷无论如何都无法修复,除非我们诉诸公钥加密技术。

MySQL 5.7 及更高版本

我们的用户依赖于双 SHA1 认证。生活继续。MySQL 被 Sun 收购,Sun 被 Oracle 收购。MariaDB 兴起并越来越受欢迎。而与这一切无关的是,SHA1 逐渐被认为越来越不安全。必须有所行动了。MySQL 是第一个提供替代方案的。实现新认证插件的任务被委托给了 Kristofer Pettersson。那时我不在 MySQL,所以我无从得知他与谁讨论以及他的目标是什么。但基本思路很清楚——摆脱 SHA1 并消除这最后一个缺陷。他正确地决定使用公钥加密技术。因此,新的 MySQL 5.7 认证协议(sha256_password 插件)使用了 SHA256(SHA2 的 256 位版本)和 RSA。它一起工作的方式如下

  • 服务器存储 SHA256(password),太棒了!
  • 认证期间,它向客户端发送一个 20 字母的混淆字符串,就像以前一样。
  • 客户端从提前分发到客户端的文件中读取服务器的 RSA 公钥。
  • 客户端计算密码与混淆字符串的异或(根据需要重复混淆字符串以覆盖整个密码),使用服务器的公钥对其进行加密,然后发送到服务器。
  • 服务器使用其秘密 RSA 密钥解密数据,将结果与混淆字符串进行异或以提取客户端的明文密码,计算其 SHA2 值,并将其与 mysql.user 表中存储的值进行比较。任务完成。

相当直接。只有一个麻烦之处。必须将服务器的公钥分发给所有客户端。而且每个客户端都需要拥有它希望连接的所有服务器的公钥,并根据需要进行管理。我能预见到这可能会变得相当烦人,这大概就是为什么如果客户端没有服务器的公钥,服务器会在认证期间“乐于”提供它,代价是一次往返。当然,任何有安全意识的人都不应该依赖这种方式,客户端如何知道公钥是真实的?中间人攻击者可以用他自己的公钥替换它,然后就能获取客户端的明文密码!再说一次,这倒也不是特别糟糕,但服务器仍然会获取明文密码。就像它在所有先前的认证协议中所做的那样。

MariaDB 10.1 及更高版本

但 MariaDB 甚至没有那个。我们仍然依赖于久经考验的双 SHA1 协议。银行虽然抱怨,但它仍然足够安全。尽管如此,鉴于最近的新闻,我们也开始寻找替代方案。请注意,即使考虑到最近的研究,双 SHA1 仍然是安全的。研究表明,现在可以生成 SHA1 碰撞,这意味着可以生成两个具有相同 SHA1 哈希的密码。这对 SHA1 本身来说是坏消息,但并不会真正削弱我们的认证。尽管如此……这表明 SHA1 的寿命不多了,最终可能会有人找到方法来破解它。

我也构建了使用公钥加密的新认证协议。目标是即使是流量嗅探、mysql.user 表,或者两者兼有,甚至是一个完全被攻陷的服务器,也无法恢复密码。而且其行为应该保持不变——无需分发文件,协议中也没有新增的往返。结果是使用了 ed25519 的协议——这是一种椭圆曲线数字签名算法,在有限域 order 2255–19 上使用 Edwards 曲线(这就是该算法被称为 Ed-255-19 的原因)。曲线和算法本身都由著名的 Daniel J. Bernstein(或简称 djb)发明。他和他的团队还创建了一些 ed25519 的公共领域参考实现,其中一个在 OpenSSH 中使用。新的 MariaDB 认证协议也使用了这些参考 ed25519 实现之一,其工作方式如下

  • 用户的密码是秘密密钥。我们计算 SHA512(password) 并应用一些数学“魔法”将其转换为公钥。这个公钥由服务器存储在 mysql.user 表中。
  • 对于认证,服务器向客户端发送一个随机的 32 字节混淆字符串。
  • 客户端使用其秘密密钥对其进行签名
  • 服务器使用用户的公钥验证签名

就这些!比双 SHA1 简单得多。而且它尽可能地安全,密码不存储在服务器上,不发送到任何地方,服务器甚至在任何时候都看不到它,也无法从其掌握的信息中恢复它。这里也没有中间人攻击的机会。无需文件。相同的往返次数。拥有 mysql.user 表的情况下仍然可以暴力破解密码,但对此我们无能为力。

这个新协议首先作为插件在 MariaDB–10.1.22 中引入,并将会在未来版本中更方便地集成到服务器中。