经过一些研究和一段时间的反复尝试,我终于找到了一个可行的解决方案。我先从奖金问题开始:
-
我没有找到任何正式的文档,我遇到的每个参考实现和代码示例总是首先传递客户端密钥,这也是服务器(Microsoft IIS v8.5)所期望的。因此,这似乎是标准,即使它不是正式的。
-
是的
Timestamp
和
Reference
价值观在很大程度上是重要的,并且与主要问题密切相关。
那么,如果必须在Java中使用JAX-WS手动对出站SOAP消息中的内容进行签名,那么实际的签名算法是什么呢?
This reference
这是一个很有帮助的起点,应该能让您很好地了解SOAP世界中的总体架构。其中的一些描述非常愚蠢。例如:
3.2.2签名验证
-
从获取密钥信息
KeyInfo
或来自外部源。
-
获取
SignatureMethod
使用
CanonicalizationMethod
并使用结果(和先前获得的
密钥信息
)确认
SignatureValue
在
SignedInfo
要素
如果您的
密钥信息
是一个
SecurityTokenReference
到
SecurityContextToken
它本身并不包含任何关键数据
签名方法
是
Algorithm="http://www.w3.org/2000/09/xmldsig#hmac-sha1"
很明显
规范化方法
与此相关,或者你应该如何从中了解你需要将服务器和客户端结合起来
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>
…你想从
参考
元素,它指向具有
id
“_0”(在本例中为
时间戳
元件)。然后根据指定的
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
我们的元素
参考
元素告诉我们这应该是一个SHA1哈希(base64编码)。简单地说:
MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
String canonDigestValue = Base64.encodeBytes(sha1.digest(canonTimestamp));
你得到的价值进入
Reference/DigestValue
元素(假设您正在构建出站请求)。完成后
参考
是完整的,因为没有任何额外的
参考
元素,因此
信息元素
块
现在要得到
签名值
,你将
信息元素
元素,与之前相同:
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>
…然后你根据提名的人在整件事上签字
签名方法
算法,在我们的例子中
HmacSHA1
:
Mac mac = Mac.getInstance("HmacSHA1");
SecretKeySpec key = new SecretKeySpec(getSharedKey(), "HmacSHA1");
mac.init(key);
String signature = Base64.encodeBytes(mac.doFinal(bytesToSign));
哪里
getSharedKey()
在本例中,返回使用
二进制机密
客户端和服务器在初始化期间发送的值
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)中生成,否则它将拒绝请求,因为时间戳来自未来或已经过期。一个简单的问题,可以通过标准化UNIX历元时间戳来解决。但出于某种原因,他们选择了“yyyy-mm-dd'T'hh:mm:ss.msec'Z'”。想想看吧
我希望这对下一个不幸的人有帮助,他必须尝试让Java使用SOAP/XML与.NET进行通信。
如果您使用的是ApacheXMLSecurity,请注意最后一点。你需要打电话
org.apache.xml.security.Init.init()
在尝试使用
Canonicalizer
,例如从
static
初始化块。如果你不这样做,当你试图规范化时,你会得到一个例外(NPE,我认为是这样)。