我有一个Java(基于JAX-WS)的SOAP客户端,我正试图与(第三方)基于WCF的服务器通信。我发现sentiment expressed here非常准确。但目标仍然存在。
很长一段时间,我可以从服务器中哄骗一个有效的“安全上下文令牌”,但我对信息签名问题感到困惑(我相信)。
服务器似乎希望使用客户端/服务器密钥(hmac-sha1
算法)使用PSHA1
身份验证代码对邮件进行签名。很公平。但是,JAX-WS似乎希望使用rsa-sha1
和X509证书来签署出站消息(服务器不喜欢),并且只有hmac-sha1
才会使用UsernameToken
提供(服务器也不喜欢)。
所以我试图在SOAPHandler
实现中手动签署出站SOAP消息。客户端为获取安全上下文令牌而发送的请求如下所示:
<t:RequestSecurityToken xmlns:t="http://schemas.xmlsoap.org/ws/2005/02/trust">
<t:TokenType>http://schemas.xmlsoap.org/ws/2005/02/sc/sct</t:TokenType>
<t:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</t:RequestType>
<t:Entropy>
<t:BinarySecret Type="http://schemas.xmlsoap.org/ws/2005/02/trust/Nonce">NzM1MDZjYWVkMTEzNDlkNGEyODY0ZDBlMjlkODEyMTM=</t:BinarySecret>
</t:Entropy>
<t:KeySize>256</t:KeySize>
</t:RequestSecurityToken>
服务器发回的令牌如下所示:
<t:RequestSecurityTokenResponse xmlns:t="http://schemas.xmlsoap.org/ws/2005/02/trust">
<t:TokenType>http://schemas.xmlsoap.org/ws/2005/02/sc/sct</t:TokenType>
<t:RequestedSecurityToken>
<c:SecurityContextToken xmlns:c="http://schemas.xmlsoap.org/ws/2005/02/sc" u:Id="uuid-106bdbae-76e5-4195-b5d0-cc1c1a7a813e-13">
<c:Identifier>urn:uuid:c0be4929-da8d-4955-8e13-b25aa7a37217</c:Identifier>
</c:SecurityContextToken>
</t:RequestedSecurityToken>
<t:RequestedAttachedReference>
<o:SecurityTokenReference xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
<o:Reference ValueType="http://schemas.xmlsoap.org/ws/2005/02/sc/sct" URI="#uuid-106bdbae-76e5-4195-b5d0-cc1c1a7a813e-13" />
</o:SecurityTokenReference>
</t:RequestedAttachedReference>
<t:RequestedUnattachedReference>
<o:SecurityTokenReference xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
<o:Reference URI="urn:uuid:c0be4929-da8d-4955-8e13-b25aa7a37217" ValueType="http://schemas.xmlsoap.org/ws/2005/02/sc/sct" />
</o:SecurityTokenReference>
</t:RequestedUnattachedReference>
<t:RequestedProofToken>
<t:ComputedKey>http://schemas.xmlsoap.org/ws/2005/02/trust/CK/PSHA1</t:ComputedKey>
</t:RequestedProofToken>
<t:Entropy>
<t:BinarySecret u:Id="uuid-106bdbae-76e5-4195-b5d0-cc1c1a7a813e-14" Type="http://schemas.xmlsoap.org/ws/2005/02/trust/Nonce">dssunihZGy2dnnDHV9PMe3vU3lg/kKKZQkFohvGvCAk=</t:BinarySecret>
</t:Entropy>
<t:Lifetime>
<u:Created>2016-04-08T04:11:54.392Z</u:Created>
<u:Expires>2016-04-08T19:11:54.392Z</u:Expires>
</t:Lifetime>
<t:KeySize>256</t:KeySize>
</t:RequestSecurityTokenResponse>
我正在使用BinarySecret
将客户端和服务器PSHA1
组合在一起,如下所示:
private byte[] getSharedKey() {
try {
//FIXME: client key first, or server key first?
P_SHA1 algo = new P_SHA1();
return algo.createKey(getBinaryClientEntropy(), getBinaryServerEntropy(), 0, getSharedKeySize() / 8);
}
catch (Throwable e) {
LOG.error("Unable to compute shared key!", e);
}
return null;
}
然后我使用该密钥计算消息的MAC,例如:
Mac mac = Mac.getInstance("HmacSHA1");
SecretKeySpec key = new SecretKeySpec(getSharedKey(), "HmacSHA1");
mac.init(key);
byte[] signatureBytes = mac.doFinal(content);
String signature = Base64.encodeBytes(signatureBytes);
然后进入出站请求(以及大量其他样板事件),作为SignatureValue
。最终我最终得到了类似的东西:
<S:Envelope xmlns:S="http://www.w3.org/2003/05/soap-envelope">
<S:Header xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:scon="http://schemas.xmlsoap.org/ws/2005/02/sc" xmlns:sec="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
<sec:Security xmlns:env="http://www.w3.org/2003/05/soap-envelope" env:mustUnderstand="true">
<scon:SecurityContextToken xmlns:util="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" util:Id="uuid-106bdbae-76e5-4195-b5d0-cc1c1a7a813e-55">
<scon:Identifier>urn:uuid:3ab0f3fb-edd4-4880-af77-d700dda371bb</scon:Identifier>
</scon:SecurityContextToken>
<sig:Signature xmlns:sig="http://www.w3.org/2000/09/xmldsig#">
<sig:SignedInfo>
<sig:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
<sig:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#hmac-sha1" />
</sig:SignedInfo>
<sig:SignatureValue>ohqViTbUYBG2E3hLldUA1AsPBJM=</sig:SignatureValue>
<sig:KeyInfo>
<sec:SecurityTokenReference>
<sec:Reference URI="#uuid-106bdbae-76e5-4195-b5d0-cc1c1a7a813e-55" ValueType="http://schemas.xmlsoap.org/ws/2005/02/sc/sct" />
</sec:SecurityTokenReference>
</sig:KeyInfo>
</sig:Signature>
</sec:Security>
</S:Header>
<S:Body>
<ns2:HelloWorld xmlns:ns2="http://tempuri.org/" xmlns:ns3="http://schemas.microsoft.com/2003/10/Serialization/">
<ns2:name>Test</ns2:name>
</ns2:HelloWorld>
</S:Body>
</S:Envelope>
这导致“验证消息安全性时发生错误”从服务器返回的响应。
使用wcf-storm触发请求和Fiddler2来检查传出的数据包,我知道我应该关闭。以下请求可正常运行:
<S:Envelope xmlns:S="http://www.w3.org/2003/05/soap-envelope">
<S:Header xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:scon="http://schemas.xmlsoap.org/ws/2005/02/sc" xmlns:sec="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
<o:Security xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" s:mustUnderstand="1">
<u:Timestamp u:Id="_0">
<u:Created>2016-04-05T23:48:06.110Z</u:Created>
<u:Expires>2016-04-05T23:53:06.110Z</u:Expires>
</u:Timestamp>
<c:SecurityContextToken xmlns:c="http://schemas.xmlsoap.org/ws/2005/02/sc" u:Id="uuid-8085da33-b25c-4f09-b5a9-110635a3ae39-2005">
<c:Identifier>urn:uuid:91349027-cb32-4c46-9f16-74a6bcb11126</c:Identifier>
</c:SecurityContextToken>
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
<SignedInfo>
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
<SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#hmac-sha1" />
<Reference URI="#_0">
<Transforms>
<Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
</Transforms>
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
<DigestValue>AvRXi7pyjulsfdg9afInSFMM+5k=</DigestValue>
</Reference>
</SignedInfo>
<SignatureValue>TQup7BBN43b8CefrdSRd+X8MBgg=</SignatureValue>
<KeyInfo>
<o:SecurityTokenReference>
<o:Reference ValueType="http://schemas.xmlsoap.org/ws/2005/02/sc/sct" URI="#uuid-8085da33-b25c-4f09-b5a9-110635a3ae39-2005" />
</o:SecurityTokenReference>
</KeyInfo>
</Signature>
</o:Security>
</S:Header>
<S:Body>
<ns2:HelloWorld xmlns:ns2="http://tempuri.org/" xmlns:ns3="http://schemas.microsoft.com/2003/10/Serialization/">
<ns2:name>Test</ns2:name>
</ns2:HelloWorld>
</S:Body>
</S:Envelope>
主要区别是:
Timestamp
元素(虽然我尝试过包含它,但似乎没有任何区别)。SignedInfo/Reference
元素,因为我不确定它的DigestValue
是如何计算的。所以,经过所有这些,我想主要的问题是:
签署出站邮件的实际算法是什么?如果我有:
<Envelope>
<Header>
HHH...
</Header>
<Body>
BBB...
</Body>
</Envelope>
...我的意思是计算<Envelope>...</Envelope>
(所以整个事物),或仅<Body>...</Body>
,甚至只是BBB...
部分的签名值?如果我打算使用整个内容,我如何协调这一事实,即在标题中添加签名信息会改变计算签名时用作输入的内容?
是否有更直接的方法让JAX-WS使用我忽略的必要签名约定生成请求?
然后还有一些小问题:
在使用BinarySecret
合并客户端和服务器PSHA1
值时,是否存在关于我传递哪个订单的既定标准?
Timestamp
和SignedInfo/Reference
条目是否重要,如果是,那么计算DigestValue
的正确方法是什么?
答案 0 :(得分:5)
经过一些研究和相当多的反复试验,我设法找到了一个有效的解决方案。我先从奖金问题开始:
我没有找到任何正式文档,我遇到的每个参考实现和代码示例总是先传递客户端密钥,这也是服务器(Microsoft IIS v8.5)所期望的。所以这似乎是标准,即使它不是正式标准。
是的,Timestamp
和Reference
值具有重要意义,并且与主要问题密切相关。
那么在Java中使用JAX-WS手动执行此操作的实际算法是什么?
This reference是一个有用的开始,并且应该让您非常了解SOAP世界中的事物是如何变得过分的。其中一些是非常晦涩的描述,以引导。例如:
3.2.2签名验证
- 从
KeyInfo
或从外部来源获取密钥信息。- 使用。获取
醇>SignatureMethod
的规范形式CanonicalizationMethod
并使用结果(以前获得的结果)KeyInfo
)确认SignatureValue
元素上的SignedInfo
。
如果您的KeyInfo
是SecurityTokenReference
到SecurityContextToken
且实际上并未包含任何关键数据,并且SignatureMethod
为Algorithm="http://www.w3.org/2000/09/xmldsig#hmac-sha1"
,它很明显是任何CanonicalizationMethod
与之相关的问题,或者你应该从中得到什么,知道你需要结合服务器和客户端BinarySecret
值并采取结果成为你的关键。但我离题了。
要应用的算法或多或少地在Signature
块中描述。例如,如果您正在与之交谈的服务器需要以下内容:
<o:Security xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" s:mustUnderstand="1">
<u:Timestamp u:Id="_0">
<u:Created>2016-04-11T00:53:44.050Z</u:Created>
<u:Expires>2016-04-11T00:58:44.050Z</u:Expires>
</u:Timestamp>
<c:SecurityContextToken xmlns:c="http://schemas.xmlsoap.org/ws/2005/02/sc" u:Id="uuid-41b0578e-dc47-4467-9b65-b0cebde98309-1">
<c:Identifier>urn:uuid:9eba64a2-5cf8-4ea9-85e9-359b2edbb13c</c:Identifier>
</c:SecurityContextToken>
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
<SignedInfo>
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
<SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#hmac-sha1" />
<Reference URI="#_0">
<Transforms>
<Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
</Transforms>
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
<DigestValue>CwJgqLNOoHJpuiqIOylvVvFli1E=</DigestValue>
</Reference>
</SignedInfo>
<SignatureValue>fJxof0blfd6abX0V4EmPYZ/NGJI=</SignatureValue>
<KeyInfo>
<o:SecurityTokenReference>
<o:Reference ValueType="http://schemas.xmlsoap.org/ws/2005/02/sc/sct" URI="#uuid-41b0578e-dc47-4467-9b65-b0cebde98309-1" />
</o:SecurityTokenReference>
</KeyInfo>
</Signature>
</o:Security>
...您想要从Reference
元素开始,该元素指向具有id
&#34; _0&#34;的元素。 (在这种情况下是Timestamp
元素)。然后,您可以根据指定的Transform
算法规范化引用的元素。使用Apache XML Security最容易做到这一点,大概如下:
SOAPElement timestamp = secHeader.addChildElement(soapFactory.createName("Timestamp", "u", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"));
//[add 'Created' and 'Expires' values, as required]
//once you're done adding stuff, you can canonicalize the element
Canonicalizer canonizer = Canonicalizer.getInstance(Canonicalizer.ALGO_ID_C14N_EXCL_OMIT_COMMENTS);
byte[] canonTimestamp = canonizer.canonicalizeSubtree(timestamp);
这会给你这样的东西(新行不是规范,抱歉):
<u:Timestamp xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" u:Id="_0"><u:Created>2016-04-11T00:53:44.050Z</u:Created><u:Expires>2016-04-11T00:58:44.050Z</u:Expires></u:Timestamp>
现在您需要计算该字符串的DigestValue
。我们DigestMethod
元素中的Reference
元素告诉我们这应该是SHA1哈希(base64编码)。简单地说:
MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
String canonDigestValue = Base64.encodeBytes(sha1.digest(canonTimestamp));
您获得的值将进入Reference/DigestValue
元素(假设您正在构建出站请求)。完成后,Reference
已完成,并且由于没有任何其他Reference
元素,因此SignedInfo
块也是如此。
现在要获取SignatureValue
,您可以将SignedInfo
元素标准化,与以前相同:
SOAPElement sigInfo = sigElem.addChildElement(new QName("SignedInfo"));
SOAPElement canon = sigInfo.addChildElement(new QName("CanonicalizationMethod"));
canon.addAttribute(soapFactory.createName("Algorithm"), Canonicalizer.ALGO_ID_C14N_EXCL_OMIT_COMMENTS);
//[continue adding the other elements...]
//canonicalize the entire, completed 'SignedInfo' block
byte[] bytesToSign = canonizer.canonicalizeSubtree(sigInfo);
应该为您提供以下内容:
<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#"><CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"></CanonicalizationMethod><SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#hmac-sha1"></SignatureMethod><Reference URI="#_0"><Transforms><Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"></Transform></Transforms><DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"></DigestMethod><DigestValue>CwJgqLNOoHJpuiqIOylvVvFli1E=</DigestValue></Reference></SignedInfo>
...然后根据指定的SignatureMethod
算法签署整个内容,在我们的例子中为HmacSHA1
:
Mac mac = Mac.getInstance("HmacSHA1");
SecretKeySpec key = new SecretKeySpec(getSharedKey(), "HmacSHA1");
mac.init(key);
String signature = Base64.encodeBytes(mac.doFinal(bytesToSign));
...其中getSharedKey()
返回一个密钥,该密钥是使用客户端和服务器在初始BinarySecret
交换期间发送的RequestSecurityToken
值导出的。如:
private byte[] getSharedKey() {
try {
//XXX: doesn't seem to be formally specified anywhere, but convention appears to be that the client key always goes first
P_SHA1 algo = new P_SHA1();
return algo.createKey(getBinaryClientEntropy(), //the 'BinarySecret' value that the client sent to the server, decoded to raw binary
getBinaryServerEntropy(), //the 'BinarySecret' value that the server sent to the client, decoded to raw binary
0, //offset, '0' is what we want here
getSharedKeySize() / 8); //'KeySize' is 256 bits in this case (specified by server), divide by '8' to convert to bytes
}
catch (Throwable e) {
LOG.error("Unable to compute shared key!", e);
}
return null;
}
无论如何,此时您应该有一个签名值,它可以附加到出站消息中的Security
标题中,例如:
SOAPElement sigValue = sigElem.addChildElement(new QName("SignatureValue"));
sigValue.addTextNode(signature);
如果一切顺利,消息现在已成功签名并且服务器质量可接受。
虽然我注意到有一个最后的警告,即需要在服务器的时区(在本例中为UTC)中生成Timestamp
值,否则它将拒绝由于时间戳来自未来或已经过期的请求。一个简单的问题,可以通过标准化UNIX纪元时间戳来解决。但出于某种原因,他们选择了&#34; yyyy-mm-dd&#39; hh:mm:ss.msec&#39; Z&#39;&#34;代替。去图。
我希望这对下一个不幸的灵魂有帮助,他必须尝试让Java使用SOAP / XML与.NET对话。
如果您正在使用Apache XML Security,那么最后需要注意。在尝试使用org.apache.xml.security.Init.init()
之前,您需要致电Canonicalizer
,例如在static
初始化程序块中。如果你不这样做,当你尝试规范化时,你会得到一个例外(我认为是NPE)。