简介
Web 服务开发人员不太善于和人打交道,当然,我说的不是谈情说爱方面——如果您向某个人颁发一份证书,第二天他就会重新设置自己的计算机,然后把自己的私钥丢掉;如果您给他一张智能卡,他会把它丢在家里;如果您让他记住一个密码,他会选择一个常用词汇作为密码;如果您让他记住一个更好的密码,他会把密码写在一张标签上,然后贴在显示器上——想把事情做圆满,好比登天。但是,往往我们都不得不知难而上,生成对付这种不完美物种的系统。
涉及到人的时候,您通常都会看到用于对服务器进行身份验证的Username标记。使用得当时,Username标记可以成为能够切实保障安全性的系统的一部分——但是,您真的应当提前计划。本文中,我将提供一些关于以合理的方式处理Username标记的指南。我将首先讨论两种针对使用Username标记的典型系统的攻击,然后介绍一些缓解技巧以支持您的系统防御这些攻击。
攻击加密(或已签名)数据
知道已加密(或者使用密钥签名)的数据的那些攻击者,只需尝试所有可能的密钥来解密数据(或者复制签名),就可以实现强力攻击。请注意,这是一种脱机攻击——Offline Attack。因此,标准的服务器端技术(如在特定次数的失败尝试后锁定帐户或者延迟登录)都不能保护您的数据安全。攻击者可以将其所有的计算资源集中起来以尝试猜出密码,而且,由于这是脱机时发生的,您甚至可能意识不到自己的系统正在遭受攻击。对于较长的会话密钥(通常是由基于高质量熵的强加密随机数字生成器生成的 128-256 位密钥),这种攻击一般并不可行,这是因为密钥空间非常巨大。例如,SSL使用强会话密钥,不容易受到这种类型攻击的侵害。
但是,当密码是加密或签名密钥的源时,此类攻击就变得非常高效,这是因为密钥空间大为缩小。作为一个极端的例子,如果您的密码策略允许一个字符的 ASCII密码,那么攻击者只需猜测大约100个不同的密钥就可以得到正确的密码。如果您的密码策略要求最少六个字符,密钥空间增大了,但攻击者这时会使用一个常用密码词典(或者就是服务器所支持的主要语言中所有词汇的列表)来提高这种攻击的效率。他最终会找到那些根据词典中的一或多个词汇来组成自己的密码的用户。
如果您强制具有标准复杂性要求的密码策略,甚至自己对系统中注册的所有新密码执行词典攻击,则可以降低这种特定的风险,但这时密码的长度又成了一个问题。人们就是记不住很长的密码。这个时候,您可能思忖,这都跟Username标记有什么关系吗?您应当想到Username标记常用于对消息签名,而且稍做处理,甚至可用于加密消息。如果这样做了,并且您让攻击者看到了这些签名或加密文本,您就将自己暴露在上述可怕的脱机强力和词典攻击之下。
Web服务增强(Web Services Enhancements,WSE)团队非常关注Username标记的误用,因此从SP2开始,WSE 2.0标记颁发框架将拒绝任何包含未加密 Username标记的请求(一种可接受的加密形式是直接使用SSL)。并且,没有任何可更改这种行为的配置选项。如果您确实需要放松这种限制,需要编写代码才能实现。
底线是:基于密码的密钥始终是脆弱的。既然存在脱机词典或者强力攻击的威胁,那么公开基于密码的加密文本或签名就实在有些不明智了。这也包括WSE中 PasswordOption.SendHashed所产生的摘要在内!
攻击服务器帐户数据库
如果攻击者攻破了保存密码数据库的服务器,而且这些密码是以原始的明文形式保存的,该攻击者将可以立即掌控极其宝贵的材料。人们倾向于重用密码,或者直接重用,或者在某个主题上稍做修改。通常,通过劫掠一个站点的帐户数据库,攻击者可以立即获得对许多其他站点的访问,使用户遭受标识被窃或者更糟糕的损失。
负责的架构师不会设计存储明文密码,甚至是加密密码的系统。他们知道加密并不能消灭秘密,而只是转移了秘密。需要解密密码的服务器需要用密钥来进行解密,而如果服务器可以得到密钥,那么攻破该服务器的攻击者也可以获得该密钥。负责的架构师会生成存储密码派生物的系统。例如,通过用一种单向的哈希派生密码“验证程序”,服务器就可以验证密码,而无需实际存储原始的明文密码。并且,由于操作是单向的,因此不需要密钥。
但是,请不要搞错了:用最好的单向salt化的、迭代哈希保护的帐户数据库,也无法抵挡窃取数据库的攻击者。这位攻击者可以按部就班慢条斯理地将她所能支配的所有计算能力集中到一起,对这些哈希发起词典和强力攻击。这时候,只有将强密码策略和好的检测应对措施结合起来,才足以形成唯一的可以限制损失的方法。您的密码策略可以减缓攻击,而检测应对措施则可以在系统被攻破时发出警报,此时您就可以开始通知客户。在发生这样的泄密事件后会不会有客户留下来,完全是另外一个问题。这是因为更改在许多不同的站点使用的密码对于大多数用户来说,最起码很不方便,最糟糕的是根本不可能。
强密码策略还有另外一个优点:它可以强制那些到处都使用同一个密码的用户为您的站点选择一个唯一的密码,这是因为您在第一步就会拒绝他的标准密码。
密码策略
很明显,使用强密码策略是明智之举。但是什么使得一种强密码策略好过另一种?您应当要求多长的密码?对于大多数安全应对措施而言,防守的强度应当与您所保卫的资产价值成正比。极端情况是,如果您没有任何有价值的资产(而您的声誉碰巧也是这些资产的一份),根本不需要密码策略。已经没什么好丢的了,当然也就不需要难为自己的用户,而拒绝他们首选的密码。实际上,为什么还要费心来要求密码呢?在另一种极端情况下,密码可能根本就不成其为一个选项:相反,您可能更愿意采用公钥基础结构,对客户端使用智能卡,从而得到多因子的身份验证。
如果您在这两种极端情况间摇摆,则可以使用一个公式来计算强力攻破密码所需的步骤数:
steps = (charChoices ^ minLength) / 2
其中,charChoices是通常为密码选择的唯一字符的数目。当然,这个公式没有将对选择密码的人的了解或者词典攻击的可能性考虑在内,因此步骤数常常更少。假定您要求了六字符的密码,结合使用大写和小写字母(也就是每个字符有 26×2种可能性),此时强力攻击大约需要234步。您邻居家的小孩就可以在他的 GameBoy上实现对这种密码的强力攻击。如果是八字符的密码,附加要求数字和标点符号,也就是为典型的用户增加了大约25种排列组合,又怎么样呢?这将门槛提升到了大约250步,在今天常见的PC上,可能需要一年,但是,如果使用专业硬件,可能会把所需时间降到大约一周,甚至几毫秒,具体取决于资金支持的程度。
强密码策略会对客户造成不便。但是,密码策略越弱,您的密码数据库就越脆弱,而要降低风险,就需要应用更强的应对措施来保护数据库和检测对数据库的威胁。
缓解技巧
首先,我将对有助于降低Username标记所带来风险的应对措施进行概述;然后,我会把它们穿插到若干示例中,提供一些关于如何在您自己的系统中应用它们的指南。
保护Username标记的最佳方式是在将其传送到网络上之前,使用强密钥对它进行加密。例如,如果您使用SSL来确保客户端和服务器之间简单的点到点连接的安全性,可以对Username标记使用PasswordOption.SendPlainText,并让安全的信道来保护密码。从安全角度讲,这相当于使用SSL上的基本身份验证。现在,唯一的问题是,一旦插在服务器端的HTTP侦听器解密了负载,用户的明文密码就可以被服务器端管线中的所有代码得到,这可能是我们所无法接受的。
您可以让客户端在通过SSL信道发送密码之前对密码进行哈希运算,从而进一步保护用户的密码。这可以防止服务器端管线被频繁地暴露给明文用户密码(请记住,大多数人都在所有地方使用同一个密码,因此这里我们所担心的不光是您的服务器)。客户端哈希和更通用的密码等效物的概念,是您在本文中通篇都会看到的重要主题。
顺便提一句,SSL不是唯一一种加密Username标记的方法。如果您使用WS-Security(或者WS-SecureConversation)来保护您的消息,可以使用来自服务器的X.509证书的公钥来加密一个强大、随机和对称的密钥,然后使用该密钥来加密标记。您还需要加密任何使用Username标记加密的签名。请记住,我们不希望让攻击者看到任何基于密码的加密文本或签名。如果您可以使用SSL,请使用它。SSL经过了大量公众审查和密码分析,您无需担心什么会被加密,什么没有被加密——整个消息都会被加密。
有时,服务器需要验证客户端的密码,因此需要某种帐户数据库。如前所述,这种帐户数据库是宝贵的资产,需要进行保护。一种您可以用来保护密码数据的技巧是存储salt化的迭代哈希,而不是真正的密码。这么做的目的是降低对被窃数据库进行脱机强力或词典攻击的速度,从而使您有时间检测泄密事件并通知客户采取应对措施。您可能会考虑的另一种技巧是使用一次性密码( One-time Password,OTP),您可以在RFC 2289中读到有关它的详细信息,但是,OTP超出了本文的范围。
使用“密码等效物”
“wsse:PasswordText和wsse:PasswordDigest类型的密码并不限于实际的密码,虽然通常是这样的。任何密码等效物(如派生密码或 S/KEY[一次性密码])都可以使用……本规范的目的并不在于要求所有的实现都能够访问明文密码。”
——摘自 OASIS UsernameToken Profile 1.0
正如您所看到的,允许客户端对简单密码进行预处理,然后通过Username标记将它们发送给服务器。一种方法是直接哈希密码,然后再通过安全的信道(如SSL)将它发送给服务器。在这种情况下,服务器通过要求客户端证明它知道该密码哈希来验证客户端的身份,这意味着服务器也知道密码。另一种方式是对密码salt化和哈希,然后将其发送给服务器,从而创建一种一次性的哈希,服务器可以使用该哈希来验证密码是否正确,这种方式有时会在没有其他信道安全(如SSL)的情况下使用。这种相当复杂的技巧仍不能避免脱机词典和强力攻击,因此不推荐使用,但是考虑到准备坚持走这条独木桥的朋友,本文仍将对其进行讨论。
使用Username标记的五步骤推荐解决方案
Step 1:使用X.509证书验证服务器的身份。
首先,必须使用强大的身份验证机制向客户端验证服务器的身份。您绝对不希望自己的客户将他们的密码发送给冒充的服务器。结合使用SSL或WS-Security与服务器端X.509证书。SSL握手要求服务器证明其私钥的正确性。如果无法使用SSL,您可以使用WS-Security来建立相同的证明。
Step 2:加密 Username 标记。
您必须确保使用在前面提到的身份验证交换派生的强密钥加密了所有的Username标记。这有助于确保只有可信的服务器能够解密标记。如果您使用SSL,整个负载都会被加密,能够满足这项要求。如果您使用 WS-Security外加服务器证书,应当确保使用强会话密钥加密了所有的 Username标记(和任何由这些标记生成的签名),而该会话密钥本身又使用服务器的公钥进行了加密。
Step 3:避免公开使用 Username 标记生成的签名或加密文本。
永远都不要使用Username标记加密任何数据。您应当首先选择在服务器身份验证握手过程中建立的强会话密钥。SSL或外加服务器证书的 WS-Security都可以实现这一点。如果使用Username标记对任何数据进行签名,请确保签名本身使用同一个强会话密钥进行了加密。请记住,我们的目标是不让攻击者看到任何基于密码的加密文本或签名,否则将无法免受脱机词典攻击的侵害。
Step 4:对Username标记密码等效物进行哈希运算。
在Username标记中使用密码等效物,而不是简单的明文密码。如果您使用的不是一次性密码或者类似的方案,请使用针对 wsse:PasswordDigest进行说明的简单的SHA-1哈希。这可以降低在 Username标记通过服务器端管线的过程中暴露客户端密码的可能性。
Step 5:通过为每个项使用唯一的salt来保护帐户数据库。
不要在服务器端帐户数据库中存储明文或者可逆加密的密码。相反,存储加slat化的,迭代哈希的密码验证程序。对验证过程的输入,将是由客户端发送的哈希密码以及从帐户数据库中查找的salt。如果您搞不懂我在说什么,请稍安勿躁,很快我就会进行详细介绍。
这是我建议您遵循的指南。下面,我们来看看如果您试图在不使用强加密的情况下发送Username标记,事情会变得多复杂。
不能解决问题的方案:以明文形式发送Username标记
首先必须说明,这不是推荐的方法,但是很多人都想这么做,因此需要进行讨论。希望这里的讨论足以说服您重新考虑前面推荐的更为简单的方法。
OASIS介绍了通过Username标记发送密码的三种方式。第一种方式是原样发送密码(或密码等效物);第二种方式是只发送密码的哈希;第三种是发送包括 nonce和时间戳的一次性哈希。如果您不打算加密Username标记,前两种技巧就容易遭受重播攻击(以明文形式发送原始密码纯粹是疯狂之举)。第三种技巧(与服务器端重播缓存结合使用时)可以缓解这一问题,但是仍易受到脱机词典和强力攻击,这也是我不能推荐它的原因。很不幸,OASIS 没能做到足够仔细,在他们对一次性哈希的讨论中,并没有指出这一点。一次性哈希方法的另一个问题是,它鼓励了在服务器上存储明文密码的服务器端管线。考虑一下在发送了nonce、时间戳和明文密码后,服务器需要如何作出反应。它必须查找用户的明文密码,然后将之与消息中发送的nonce和时间戳相结合,以计算哈希值。
本文中很早已经提及,在服务器端帐户数据库中存储明文密码是非常危险的做法。如果您要使用一次性哈希方法来发送未加密的Username标记(虽然已经警告过您这么做有多危险),您至少应当找到一种能够强化服务器端帐户数据库的方法。您可以考虑的第一种方法是选择使用密码等效物,这可以是让客户端管线哈希密码,然后运行生成Username标记的一次性哈希算法。这种情况下,共享的秘密是SHA-1(password),而服务器端的帐户数据库现在可以存储 SHA-1(password),而非明文密码。这是向正确的方向迈出的一步,但是窃取了服务器端帐户数据库的攻击者可以通过哈希每个可能的密码并与帐户数据库中的所有哈希进行比较来发起可伸缩的强力攻击。如果您的密码策略并不很强,他将可以使用词典来使攻击的效率更高。如果有几百或者几千个帐户,他的攻击伸缩效果会很好,因为他是在同时攻击所有密码。
一个更为严重的问题是,如果多个站点使用相同的技巧,攻击者甚至不需要猜测原始的密码,因为他已经具有密码等效物SHA-1(password)。他可以立即创建所有用户的Username标记,而这将在所有使用此技巧的其他站点有效。通过修改创建密码等效物的方式,我们可以同时改进两方面的效果。排除了 SHA-1(password),我们将使用 SHAd-1(password + userName + scopeUri)作为密码等效物。
userName和scopeUri为salt值。通过包括用户名作为salt,我们通过强制敌人一次攻击一个密码(因为每个帐户都有不同的salt),限制了脱机攻击的伸缩性。
scopeUri只是一个对于您的密码数据库唯一的字符串。这确保了您所使用的密码等效物与其他站点所使用的密码等效物不同。这些salt都不是秘密。发现它们的值并不能使攻击者获得其他好处。您必须将scopeUri发布给任何要使用您的服务的人。您还必须记录一个用户名的规范化表单,否则不区分大小写这类低级问题可能会导致一些用户身份验证失败。最后一点是,我们使用的不再是SHA-1,而是SHAd-1。这是一个双哈希:
SHAd-1(x) = SHA-1(SHA-1(x))
通过两次哈希数据,我们进一步降低了脱机攻击的伸缩性,这是因为我们确保了哈希的内部状态不能被根据公共的salt(如scopeUri)进行部分预计算(双哈希的原理在《Practical Cryptography》中得到了详细介绍,作者为Niels Ferguson和Bruce Schneier)。
要创建Username标记,请对客户端计算出的密码等效物进行base64编码,然后执行OASIS过程,以形成一次性哈希:
Password_Digest = Base64 ( SHA-1 ( nonce + created + password ) )
当服务器接收到Username标记后,他将检查他的重播缓存,然后在帐户数据库中查找密码等效物,并使用客户端发送的nonce和时间戳计算一次性哈希。如果它所计算出的哈希与客户端发送的摘要匹配,它将假定客户端是可信的,并将timestape/nonce对添加到自己的重播缓存,供以后使用。如果这看上去有些复杂,实际情况是,它的确很复杂。并且,除非您在SSL上进行传输,否则仍会受到脱机强力和词典攻击的侵害!我前面介绍的更为简单的、推荐的方法要好很多。您可以回头再看一遍,试着说服自己重新考虑使用它。
实现加salt化的、迭代哈希的帐户数据库
如果您要发送一次性哈希密码,不应选择这种技巧,因为您需要查找密码等效物才能计算一次性哈希。但如果您使用了推荐的方法——在客户端加密Username标记,在服务器上解密该标记,那么服务器将在解密标记后得到密码等效物。然后,您可以通过对它salt化并进行迭代哈希来将其转换为验证程序。这可以显著强化您的帐户数据库。
验证程序只是一个哈希值,给定了密码等效物,要计算出它仍需要相当巨大的工作量。这被称为“salt和拉伸(salting and stretching)”密码。我们混入了一个唯一的salt值并进行迭代哈希,以降低对被窃的帐户数据库进行脱机攻击的伸缩性。在 .NET Framework 1.0中,有一个实现此机制的类。它称为PasswordDeriveBytes。如果使用Framework 2.0,您应当以Rfc2898DeriveBytes为首选,它符合基于密码的加密的PKCS#5标准。无论是哪种情况,您都要通过传入密码、salt和要该对象使用的迭代次数来构造该对象。然后,调用GetBytes()来获得表示验证程序的字节数组。这就是相对于明文密码来说,我们所要存储到帐户数据库中的内容。
当新的客户端建立帐户后,您要获取它们的明文密码(或密码等效物),生成一个随机的salt值,然后计算验证程序,最后,在自己的帐户数据库中存储salt和验证程序。当现有的客户端向您发送Username标记时,您需要取出密码等效物,从帐户数据库中查找salt值和验证程序,并使用salt和所提供的密码等效物来计算调用方的验证程序,然后将其与帐户数据库中的验证程序进行比较。我随文附带了一些示例代码(参见光盘),它们演示如何使用PasswordDeriveBytes来实现这种方案。这应该会对您迅速上手有所帮助。
最后一件事:如果您采用了这种技术(或者您只是存储了密码的 SHA-1 哈希)而用户忘了他的密码,您将无法通过电子邮件将密码发送给他——这其实是件好事情。通过电子邮件向用户发送密码,在最愚蠢做法排行榜上,仅次于在服务器上存储明文密码。您需要的是另一种验证用户身份的方法,因此,您可以通过电话来确定这位要求您重置密码的用户是否可信。直接问该用户一系列可能在以后用于对他进行身份验证的问题,通常就可以达到目的。有人对这种技巧进行了详细介绍。无论选择哪种策略,您都需要提前进行规划。
小结
Username标记很容易被误用。强烈建议您使用通过强服务器身份验证方案(如 SSL)建立的强密钥对它们和任何使用它们创建的签名进行加密。对于那些不打算采纳我的建议并在不加密的情况下发送Username标记的朋友们,为此我还花了点时间探讨了一些自己的看法。
密码是一种无可奈何的负累,但是,通过了解面临的威胁和使用正确的技巧来缓解这些威胁,您仍旧可以以它们为基础生成安全的系统。您只需记住:“无论您做什么,如果密码策略不够水准,那您玩的都是危险的游戏。”