使用用户名和密码来验证用户的身份是最普通也最常见的方法,虽然在安全性方面也比较弱,由于其运用的广泛性还是成为了WS-Security目前所支持的Security Token之一。其原理非常简单,用户在发送请求的时候,在Soap head中加入自己的用户名以及密码,接受请求的Service通过之前与Client建立的共享密码来验证密码的合法性从而实现鉴别用户的功能。
不过实际运用起来就不能考虑的那么简单了,该方法主要存在两个问题:
1.在SOAP包中传输密码怎么保证密码的安全性?
2.怎么从用户名密码中获得签名和加密所需要的密钥?
针对第一个问题有三种解决方案:
1.使用运输层的安全协议(如SSL)来保证明文密码的安全性。
2.对明文密码做摘要后再传送给Service。
3.利用从密码派生出来的密钥来代替直接使用密码来实现身份鉴别。
第一种方法采用了WS-Security结合运输层的安全协议(SSL)来保证密码的安全,在本系列一开始的文章已经描述过运输层安全协议的缺点,所以在此不对该方法做详细介绍。
第二种方法类似于HTTP Digest Authentication,先来看一段使用该方法的示例:
<wsse:UsernameToken>
<wsse:Username>NNK</wsse:Username>
<wsse:Password Type="...#PasswordDigest">
weYI3nXd8LjMNVksCKFV8t3rgHh3Rw==
</wsse:Password>
<wsse:Nonce>WScqanjCEAC4mQoBE07sAQ==</wsse:Nonce>
<wsu:Created>2003-07-16T01:24:32Z</wsu:Created>
</wsse:UsernameToken>
从中看出这里使用了PasswordDigest类型的Password,从Password的内容也可以看出这里没有使用明文密码的形式。另外还多出了wsse:Nonce和wsu:Created两个元素。
其中Password的内容的计算公式如下:
Password_Digest = Base64 ( SHA-1 ( nonce + created + password ) )
读者可能比较奇怪wsse:Nonce和wsu:Created这两个元素的作用。为什么不直接SHA-1(password) ? 这样做是为了避免重放(Replay)攻击。假设Alice以摘要的形式向Service发送了密码,如果Bob此时截获了Alice发送的密码摘要,然后再用它向Service发送请求,那么Service将误认为Bob也是合法用户。当我们加入Nonce和Created元素之后,Service可以检查收到的消息中的Nonce是否已经收到过了,或者在一段时间(5min)内是否收到了相同用户名密码,从而避免重复攻击的危险。不过使用PasswordDigest方式要求Service必须拥有密码的明文形式,也就是说Service可以看到每个用户的密码,这对用户来说增加了风险。因为通常情况下用户的密码是以hash的形式保存在Service端的,从而保证用户的信息不被泄漏。
尽管通过PasswordDigest可以避免密码的明文传播,而且通过引入wsse:Nonce和wsu:Created可以避免重放攻击的危险。但是如果Bob能够把传送中的密码摘要完全的拦截下来(使它无法传送到Service),然后利用拦截下来的密码去冒充Alice去请求Service,那么Service将束手无策。因此引入了第三种方法的介绍。
第三种方法和Kerberos协议中KDC向Client传送TGT的方式类似。
我们可以看出前两种方式用户都将自己的密码发送给Service用于身份鉴别,难道为了证明自己的身份就必须把密钥(这里是密码)直接告诉别人吗?其实问题的关键在于Client能向Service证明它拥有只有C与S知道的密钥。而证明拥有的最直接方法就是告诉对方这个密钥,然后由Service比较这个密钥是否和它所知道的密钥一致,从而鉴别用户的身份。但是这种方法如前所述有各种缺陷。既然仅仅需要Client证明它知道这个密钥,那么Client可以用这个密钥对一段消息做一个签名,然后将消息和签名同时发送给Service,Service用它所知道的Client的密钥也对同样的消息做一次签名,通过比较两个签名是否一致就可以确认Client是否真的拥有它的密钥。同样通过加密的方法Client也可以向Service证明自己是否真的拥有密钥(因为只有C与S密钥一致Service才能解密出用C密钥加密的消息)。这样一旦Client在消息中加入自己的一些特有信息(比如IP),即便Bob截获了消息但是由于他并不知道真正的密钥,看不到那些特有信息,也就无法冒充Alice。
通过这种间接证明拥有密钥的方法,我们同时解决了文章一开始提出的第二个问题:
怎么从用户名密码中获得签名和加密所需要的密钥?
只要对密码做一些处理就可以从中派生出密钥。当然为了安全起见我们希望每次派生出来的密钥都不一样,这样就可以避免多次使用同一密钥而导致密钥被破解。下面就是WS-Security对密钥派生的元素定义:
<wsse:UsernameToken wsse:Id=”…”>
<wsse:Username>…</wsse:Username>
<wsse11:Salt>…</wsse11:Salt>
<wsse11:Iteration>…</wsse11:Iteration>
</wsse:UsernameToken>
其中Salt是导致密钥变化的因子,Iteration是密钥派生时Hash的次数。
密码的派生公式如下:
K1 = SHA1( password + Salt)
K2 = SHA1( K1 )
…
Kn = SHA1 ( Kn-1)
可以看到此时在UsernameToken已经不再包含Password元素,因为Client将通过使用从Password派生出密钥做签名做加密的方式来证明它拥有密钥,从而证明自己的身份。
由此看出第三种办法相对来说安全性大大提高了,但是在实际应用中以上介绍的三种的方法都不被推荐使用。
第三种方法仍旧有两个缺陷:
1.直接使用密码派生密钥,同以往临时产生的会话密钥相比,密码一旦破解,所有由改密码派生的密钥也被破解。由于密码长期不变, 那么随后所有使用该密码加密的消息都没有安全性可言。而且该密码可能还被用于Client与其他Service的交互,那么被破解后带来的损失就大多了。
2.用户密码必须以明文形式保存在Service端。
因此,在微软的WSE对安全的默认支持方式中采用了UsernameToken和Service端Certification的组合的方式来表示Security Token。下图就是WSE中已经实现的UsernameForCertificate对SOAP Envelop的扩展结构。
从中可以看到SOAP Head中的wsse:UsernameToken已经被加密,被xenc:EncryptedData所替代,查看其Token Reference发现加密使用的Key来自xenc:EncryptedKey。如果你完整阅读了本系列的文章,你将不会对它太陌生,在XML Encryption中曾经对它的来由做了详细介绍。
Note由于使用对称密钥加密效率高,所以通常会使用对称密钥来加密数据,但是如何让消息的接受也获得对称密钥则成了一个问题。消息发送方不可能将对称密钥也随消息传递给消息接收方,此时利用非对称密钥来实现加密所用的对称密钥的传递成为了一个比较好的选择。EncrptedKey就是实现此种功能的扩展元素。
Client随机产生的一个对称密钥并用它来加密和签名SOAP Envelop中的其他元素(如UsernameToken),并通过使用Service(消息接受方)的公钥(由于是公钥可以方便获取)来加密该对称密钥,以保证只有Service能够获得Client随机产生的对称密钥,从而达到验证消息完整性,解密数据并鉴别用户身份的目的。以下是采用这种方式保证安全的SOAP Envelop的示例:
<?xml version="1.0" encoding="utf-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://www.w3.org/2001/12/soap-envelope"
xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
xmlns:xenc="http://www.w3.org/2001/04/xmlenc"
xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility">
<SOAP-ENV:Header>
<wsse:Security
xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/secext">
<wsse:UsernameToken>
<wsse:Username>HotelService</wsse:Username>
<wsse:Password>myword</wsse:Password>
</wsse:UsernameToken>
<xenc:EncryptedKey wsu:id="userSysmetricKey">
<xenc:EncryptionMethod
Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5"/>
<ds:KeyInfo>
<wsse:SecurityTokenReference>
<wsse:KeyIdentifier
ValueType="...oasis-wss-soap-message-security-1.1#ThumbPrintSHA1">
LKiQ/CmFrJDJqCLFcjlhIsmZ/+0=
</wsse:KeyIdentifier>
</wsse:SecurityTokenReference>
</ds:KeyInfo>
<xenc:CipherData>
<xenc:CipherValue>G2wDCq24FsgBGerE...</xenc:CipherValue>
</xenc:CipherData>
<xenc:ReferenceList>
<xenc:DataReference URI="#DiscountResponse"/>
</xenc:ReferenceList>
</xenc:EncryptedKey>
<ds:Signature>
<ds:SignedInfo>
<ds:CanonicalizationMethod
Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<ds:SignatureMethod
Algorithm="http://www.w3.org/2000/09/xmldsig#hmac-sha1"/>
<ds:Reference URI="#DiscountedBookingForPartnersResponse">
<ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#hmac-sha1"/>
<ds:DigestValue>JwFsd3eQc0iXlJm5PkLh7...</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>BSxlJbSiFdm5Plhk...</ds:SignatureValue>
<ds:KeyInfo>
<wsse:SecurityTokenReference>
<wsse:Reference URI="#userSysmetricKey"
ValueType="...oasis-wss-soap-message-security-1.1#EncryptedKey"/>
</wsse:SecurityTokenReference>
</ds:KeyInfo>
</ds:Signature>
</wsse:Security>
</SOAP-ENV:Header>
<SOAP-ENV:Body wsu:Id="DiscountedBookingForPartnersResponse">
<s:GetSpecialDiscountedBookingForPartnersResponse
xmlns:s="http://www.MyHotel.com/partnerservice ">
<xenc:EncryptedData
wsu:Id="DiscountResponse"
type="http://www.w3.org/2001/04/xmlenc#Element">
<xenc:EncryptionMethod
Algorithm="http://www.w3.org/2001/04/xmlenc#aes256-cbc "/>
<CipherData>
<CipherValue>XD6sFa0DrWsHdehrHdhcW0x...</CipherValue>
</CipherData>
</xenc:EncryptedData>
</s:GetSpecialDiscountedBookingForPartnersResponse>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
Q: 怎么从用户名密码中获得签名和加密所需要的密钥?
A: 随机产生密钥,并通过接受方的公钥加密,保证密钥不被别人所知。
Q: 如何避免重放攻击?
A: 由于其他人无法获得密钥,所以即便截获消息也无法冒充。
Q: 直接使用密码派生密钥被破解了怎么办?
A: 密钥不再从密码派生,而是每次随机产生,即便被破解也不会影响其他的消息和其他的服务。
Q: 用户密码必须以明文形式保存在Service端?
A: 由于密码被加密而不是做摘要所以不需要Service拥有明文密码。
应用场景:
B2C网上购物,每个用户都有各自的用户名密码,而且可以方便的获得Server端的Certification。(比如Amazon)
参考资料:
OASIS Kerberos Token Profile 1.1
Protect Your Web Services Through The Extensible Policy Framework In WSE 3.0