在当今的网络世界中,单点登录(SSO)技术已经成为一种常见的身份验证方式。用户只需通过一个身份验证服务,就可以访问多个不同的服务和应用。这种技术在第三方网站中尤为常见,例如,用户可以通过自己的Facebook账户登录其他网站。然而,实现这一功能并非易事,尤其是在涉及到跨平台的交互时。本文将探讨在实现单点登录过程中遇到的一些技术挑战,特别是XML签名验证的问题。
团队目前负责维护一个为第三方网站提供单点登录服务的项目。这个项目的目标是让用户能够通过公司的平台进行身份验证,然后安全地访问第三方网站。为了实现这一目标,尝试使用PHP和SAML(安全断言标记语言)来构建一个示例的第三方网站。选择PHP的原因是家用网站使用PHP,而且服务器端实现是基于.NET的,因此使用PHP可以证明跨平台的互操作性。
SAML请求部分的实现相对简单。它涉及到一个大型的GET请求,其中XML数据被压缩并使用第三方的私钥进行签名。请求和签名作为单独的请求参数提供。虽然URL包含许多长命名空间和URL编码的内容,但这些都是相对无害的。
https://sso.platform.com/AuthService/Saml?SAMLRequest=base64blob&SigAlg=http%3A%2F%2Fwww.w3.org%2F2000%2F09%2Fxmldsig%23rsa-sha1&Signature=anotherBase64Blob
为了验证SAML请求是否来自授权的合作伙伴,SSO平台需要从请求中查找Issuer字段,找到对应的合作伙伴公钥,并使用标准库验证加密签名。
SAML响应告诉合作伙伴用户是否通过了身份验证。在特定情况下,它是一个包含XML文档的POST请求。文档中信息的真实性通过XML签名进行验证。
创建XML签名的步骤比简单地对一组字节进行数字签名要复杂得多。XML签名有很多变体和选项,如果没有库的支持,几乎不可能正确实现,而且编写这样的库也不是一件容易的事情。
XML签名允许同时对多个“引用”进行签名。理论上,引用可以是任何URI。在实践中,它要么是整个当前文档(空URI),要么是当前文档的片段("#fragmentID")。每个引用都会被规范化,然后计算其“摘要”(加密哈希)。摘要是一个常规的SHA1或SHA256哈希,这一步骤不使用任何密钥。
在第二步中,所有的摘要被收集到一个<SignedInfo>
结构中。此外,它还可能包含发行者的公钥RSA,甚至是一个完整的X509证书。除了RSA密钥,证书还包含发行者信息,如位置和名称,并且可能由受信任的证书颁发机构签名。
<SignedInfo>
然后被规范化,并使用发行者的私钥进行数字签名。带有摘要和签名的<SignedInfo>
被添加到原始XML文档中,使其成为已签名的文档。
需要注意的是,<SignedInfo>
可能根本不包含任何密钥信息,在这种情况下,验证者必须从其他来源找到发行者的公钥,例如,从已知发行者数据库中。即使<SignedInfo>
包含密钥,验证者也必须确保这个密钥确实来自发行者,而不是来自某个恶意的中间人。
要验证XML签名,必须执行以下步骤:
<SignedInfo>
节点。<SignedInfo>
节点的数字签名是否有效。<SignedInfo>
中提供的摘要匹配。如果上述所有检查都通过,那么文档就是真实的,并且没有被篡改。
需要注意的是,文档文本可能会(并且很可能)以非规范化的形式发送。因此,有很大的可能性,数字签名的内容与在文档中看到的内容不同。为了使签名验证成功,发行者和验证者都必须执行规范化过程,并且他们必须以完全相同的方式执行,否则摘要将不匹配。
规范化算法,说得委婉一点,是非平凡的。为了简化任务,“独占规范化”被定义为“规范化XML”加上一些“例外”。规范化XML本身就是一个庞大的算法:它将自闭合标签(<tag />
)转换为开闭标签对(<tag></tag>
),处理空格,属性顺序,传播命名空间等。独占规范化在简单条件下增加了一两段细化,例如“如果前缀尚未被任何输出祖先渲染,或者其父元素的最近输出祖先没有在节点集中具有与N相同的命名空间前缀和值的命名空间节点。”
标准中提供的“非规范实现”在“许多简单情况下”都能工作,但根据标准作者自己的承认,它是“受限的”。
发现了两个声称实现了XML签名验证的PHP库:
robrichards/xmlseclibs
:文档中的用例展示了如何创建XML签名,但没有展示如何验证它。有一个verify()
方法,但不清楚如何使用它。php-XMLDigitalSignature
。它也有一个verify()
方法,但示例只验证刚刚创建的签名。即使在查看了源代码一段时间后,也不清楚(甚至在查看了源代码一段时间后)如何验证来自其他人的签名。还有FR3D/XmlDSig
,它是xmlseclibs
的包装器。它的代码有一个直接的verify()
方法,它使用xmlseclib
的verify()
。
然而,即使有了这个库,也无法验证签名,因为服务器没有正确地执行规范化。
为了对文档进行理智检查,使用.NET框架编写了一个控制台验证程序(以后可能会详细介绍)。
ReferenceUri=""
)工作得很好,但如果签名只应用于文档的一部分(ReferenceUri="#mySignedElement"
)则失败。离线.NET版本可以很好地处理这两种情况。