假设您想在客户端/服务器协议中实现密码身份验证方法。 您将如何做到这一点以及可能出现的问题是什么? 以下是 PostgreSQL 中如何完成此操作的故事。
password
一开始,PostgreSQL 只有 pg_hba.conf 中现在称为“password”的方法。 这是你能想象到的最简单的事情:
1.客户端对服务器说:“你好,我是 Peter,我想要连接。”
2.服务器回复:“你的密码是什么?”
3.客户端提示输入密码或从其他地方获取密码并响应:“这是‘123456’。”
4.现在服务器查找实际密码。 它存储在系统目录 pg_authid 列 rolpassword 中。 服务器基本上执行 strcmp(pg_authid.rolpassword, “123456”),如果相等,它会向客户端说“OK”,会话启动将继续。
这种方法有一些明显的问题:
•密码通过网络线路以明文形式传输。 有一些外部方法可以解决这个问题,例如使用 SSL 或其他加密封装。
•密码以明文形式存储在系统目录中,最终存储在磁盘上。 这很糟糕,因为它允许数据库和系统管理员看到其他用户的密码。 当然,人们不应该重复使用密码,但事实上人们可能会这样做。 它还可以允许管理员通过使用密码以其他用户身份登录来绕过审核。 一般来说,存在明文密码是不好的,因为它最终可能会被复制或意外看到。 最好不要这样做。
•更微妙的是,密码以明文形式存在于服务器进程的内存中。 为什么这么糟糕? 因为管理员也可能可以在那里访问它。 此外,如果服务器核心转储或交换,明文密码可能最终会出现在磁盘上的某个位置。 因此,这实际上几乎与在磁盘存储中以明文形式保存密码一样糟糕。
crypt
于是又尝试了另一种方法。这是在pg_hba.conf中不再支持的“crypt”认证方式。
1.客户端再次开始:“你好,我是 Peter,我想连接。”
2.已配置为使用 crypt 方法的服务器响应:“请告诉我使用盐(salt)'ab’对你的密码进行crypt()加密后的值?” 每次连接尝试都会随机选择salt干扰串。
3.客户端获取用户的输入内容并计算 crypt(“123456”, “ab”)然后回复:“这是‘ab01FAX.bQRSU’。”
4.服务器检查 crypt(pg_authid.rolpassword, “ab”) 是否等于“ab01FAX.bQRSU”,如果是则回复“OK”。
crypt() 是一个常见且随时可用的 Unix 函数,用于进行加密,因此它是在此处使用的明显选择。 它解决了以明文形式传输密码的问题,但仍然存在一些现有问题和新问题:
•密码仍以明文形式保存在系统目录和存储中。
•原始 crypt() 使用的加密方法现已过时。 同样,盐值长度salt(2 个字节)也已过时。
•因此,类 Unix 操作系统的不同供应商已扩展了 crypt() 的调用以使用不同的加密算法,但这是以不兼容的方式完成的。crypt() 只用于加密本地使用的密码(就像最初的用途一样)是可以的,但是当您想通过网络在不同系统之间进行通信时,它就会崩溃。
•crypt() 可能在非 Unix 系统上不可用。 虽然可以提供替代品,但如果需要这样做的话,就会对使用操作系统中现成的设施的原始前提产生疑问。
md5
到此时,PostgreSQL 已经支持 SSL,因此线传输过程中的明文问题不再那么重要。 真正让人烦恼的是系统目录中的明文密码。 因此设计了一个新系统,在pg_hba.conf中称为“md5”。 它的工作原理如下:
1.客户端:“你好,我是 Peter,我想连接。”
2.服务器:“请告诉我使用盐(salt)'abcd’对你的密码进行MD5哈希后的值?” 同样,每次连接尝试时都会随机选择盐。
3.客户端获取用户输入并计算:md5(md5(“123456” + “peter”) + “abcd”)。 (我在这里使用 + 进行字符串连接。)这里,“123456”是用户输入的密码,“peter”是用户名,“abcd”是盐值。 然后客户端回复:“这是‘301eddd34d997f72bd43ba678e36a5ba’。”
4.服务器检查 md5(pg_authid.rolpassword + “abcd”) 是否等于“301eddd34d997f72bd43ba678e36a5ba”,如果是则回复“OK”。
那么这方法有什么问题呢?
•现在读到这里,使用 MD5 显然是一个危险信号。 哈希方法已过时。
•盐值salt的长度(4 字节)也已过时。
•用户名被用作存储的哈希密码的盐值。 (这是为了确保两个恰好具有相同密码的用户不会具有相同的存储哈希值。)因此,重命名用户会使存储的哈希密码失效,并且需要分配新密码。 这可能并不常见,但发生时仍然很烦人。
•如果有人碰巧从系统目录(或者可能是备份转储)中获取了存储的哈希密码的副本,他们可以使用这些密码进行登录。您不需要实际的密码。 例如,在上面的步骤 3 中,客户端可以在不知道“123456”的情况下只发送 md5(“我找到的哈希值”+“abcd”)。 (你不能使用库存 libpq 来做到这一点,但制作一个可以做到这一点的自定义版本对于专门的攻击者来说并不难。)
这里的教训是:不要设计自己的加密方法。
scram
因此,所有这些问题都必须重新考虑,目前的解决方案是在 PostgreSQL 10引入的使用公共标准的:SASL(RFC 4422)和 SCRAM(RFC 5802 和 RFC 7677)。
SASL 是一个协议框架,允许客户端和服务器协商身份验证机制。 这在电子邮件中被广泛使用:SMTP 或 IMAP 服务器可能提供名称为 PLAIN、LOGIN、CRAM-MD5 或 DIGEST-MD5 的身份验证机制,或许还有 SCRAM,尽管这种情况似乎较少见。 PostgreSQL 使用 SASL 的原因主要是因为 SCRAM 是通过 SASL 定义的,因此遵循它是有意义的。 否则 SASL 功能不会向用户公开。
SCRAM 是一种身份验证机制。 它实际上是一组身份验证机制,具有不同的可能的哈希算法。 当最初考虑在 PostgreSQL 中实现 SCRAM 时,大多数以前的 SCRAM 使用都采用 SHA-1,但它已经过时了,并且在撰写本文时也已被弃用,就像 MD5 一样。 所以PostgreSQL目前使用的算法是SHA-256,认证方式的全称是SCRAM-SHA-256。
整个过程大致如下,传输过程中的数据格式如下:
1.客户端:“你好,我是 Peter,我想连接。”
2.服务器:“我们将进行 SASL 身份验证。 选择以下方法之一:SCRAM-SHA-256”。 (目前只有一种方法,除非提供通道绑定,但为了简单起见,我在这篇博文中忽略了这一点。)
3.客户端:“我选择 SCRAM-SHA-256。 这是第一个 SASL 数据:n,n=peter,r=rOprNGfwEbeRWgbNEkqO“。 该数据根据 SCRAM 规范进行组装并封装在 SASL 协议消息中。 这里,“n=”字段包含用户名,“r=”字段包含base64编码的随机字符串。 前面的内容与通道绑定有关,我们在这里忽略它。
4.服务器:“这是一些 SASL 数据:r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,s=W22ZaJ0SNY7soEsUEjb6gQ==, i=4096”。 “r=”字段包含客户端的随机数据以及服务器附加的附加随机数据。 此外,服务器还发送盐值 (s=) 和交互计数 (i=),这是从相关用户存储的密码中获取的。
5.客户端:“这是一些 SASL 数据:c=biws,r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0, p=dHzbZapWIk4jUhN+Ute9ytag9zjfMHgsqmmiz7AndVQ=”。 “r=”字段与之前相同。 客户端和服务器只需来回发送此消息以检查它们是否仍在与正确的对方进行通信。 “p=”字段是客户端用户提供的密码,然后使用提供的盐值和迭代计数以特定方式对其进行哈希处理。 “c=”字段用于通道绑定。
6.服务器现在会根据本地存储的数据检查此数据。 其详细内容在此省略。 如果满足,则发回:“这是最终的 SASL 数据:v=6rriTRBi23WpRR/wtup+mMhUZUn/dB5nLTJRsjl95G4=”。 这称为验证器,允许客户端检查服务器是否确实检查了密码,而不仅仅是让每个人都通过。
7.然后客户端检查验证器,如果满足条件,则会话可以继续进行。
这里涉及很多内容。这解决了我们之前讨论的所有问题,甚至包括我们还没有考虑到的一些问题:
•密码不会以明文形式传输到网络中。
•密码在系统目录或底层存储中不是明文形式。
•密码在服务器进程中永远不会以明文形式存在。 实际上,明文密码从未离开客户端。客户端以一定的方式对其进行哈希处理,服务器将其与已有的哈希信息进行比较,但服务器永远不会看到实际的密码。
•客户端发送的任何信息都不能被其他任何人用来登录,即使整个交换过程被捕获。 这是因为客户端和服务器在每次连接尝试中都使用不同的随机数据。
•每个存储的密码都使用不同的盐值进行哈希处理,因此实际上不存在不同用户存储的密码意外相同的风险。 此外,盐值与用户名或用户的其他属性无关。
•盐长度可以轻松改变。 用户只需使用不同的盐值创建新密码即可。
•可以以系统化的方式将算法添加到此设计中。 请注意,这仍然需要更改软件,并且不会完全轻松,但至少有一个明确的方法可以做到这一点。
•正如上面第7点提到的,客户端可以验证服务器是否确实检查了密码。 在客户端/服务器身份验证中,我们通常主要考虑服务器试图阻止未经授权的客户端连接。 这是因为服务器存储了未经授权的客户端可能想要获取的有价值的数据。 但相反的情况也可能发生:客户端连接到伪造的服务器并向其发送服务器不应该获得的有价值的数据。 这样的服务器会很高兴地让任何碰巧连接的客户端进入,而不实际检查密码。 SCRAM 可以防止这种情况发生。 显然,SSL/TLS 是一种更复杂、更完整的解决方案,用于检查网络中的节点是否值得信赖,SCRAM 并不意味着消除这种需要。
这就是 PostgreSQL 现在的情况。
LDAP认证和其他认证方法
PostgreSQL中还有另一组与密码相关的认证方法:
•ldap
•radius
•pam
•bsd
对于客户端和协议而言,这些方法与明文认证方式“password”是等效的。 唯一的区别是服务器不会将密码与 pg_authid 中存储的密码进行比较,而是与相应的外部服务进行比较。 例如,LDAP 身份验证的工作原理如下:
1.客户端:“你好,我是 Peter,我想连接。”
2.服务器:“您的密码是什么?”
3.客户端:“是‘123456’。”
4.现在服务器向 LDAP 服务器检查密码“123456”。 这本身就可能涉及许多细节。 如果检查成功,服务器会说“OK”。
因此,这避免了将密码以明文形式存储在数据库中,但仍然存在与此方法相关的所有其他问题。 使用 SSL 进行 PostgreSQL 连接并安全地配置 LDAP 服务器和连接可以缓解许多此类问题,但它不会像 SCRAM 那样安全。 (另一条建议是,如果目标是在组织范围内建立集中式密码存储,则可以考虑使用 Kerberos 而不是 LDAP,但那是另一个话题了。)
总结
安全和密码学是很复杂的。通过使用SCRAM,PostgreSQL采用了公认的公共标准,现在处于一个良好的状态,并且可以在未来进行适应。
PostgreSQL考试认证中心(简称:PGCCC)