SOAP,WCF和消息签名

aro*_*oth 5 java wcf soap soap-client

我有一个Java(基于JAX-WS)的SOAP客户端,试图与基于(第三方)WCF的服务器进行通讯。我发现这里表达观点非常准确。但是目标仍然存在。

长话短说,我可以从服务器中哄骗有效的“安全上下文令牌”,但是却挂在消息签名问题上(我相信)。

服务器似乎期望hmac-sha1使用客户端/服务器秘密密钥(PSHA1算法)使用身份验证代码对消息进行签名。很公平。但是,JAX-WS似乎要使用rsa-sha1X509证书来签名出站消息(服务器不喜欢),并且似乎仅在提供hmac-sha1a 时才使用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>
Run Code Online (Sandbox Code Playgroud)

服务器发回的令牌如下所示:

<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>
Run Code Online (Sandbox Code Playgroud)

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;

}
Run Code Online (Sandbox Code Playgroud)

然后,我使用该密钥为消息计算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);
Run Code Online (Sandbox Code Playgroud)

然后,该请求就进入了出站请求(以及大量其他样板文件),例如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>
Run Code Online (Sandbox Code Playgroud)

这导致从服务器返回“响应消息的安全性时发生错误”响应。

使用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>
Run Code Online (Sandbox Code Playgroud)

主要区别在于:

  • 我省略了该Timestamp元素(尽管我尝试将其包括在内,但似乎没有任何区别)。
  • 我已经省略了该SignedInfo/Reference元素,因为我不确定该如何DigestValue计算。

所以毕竟,我想主要的问题是:

签署出站邮件的实际算法是什么? 如图所示,如果我有:

<Envelope>
    <Header>
        HHH...
    </Header>
    <Body>
        BBB...
    </Body>   
</Envelope>
Run Code Online (Sandbox Code Playgroud)

...我是要计算签名值<Envelope>...</Envelope>(是整个东西),还是仅仅是<Body>...</Body>,甚至只是BBB...一部分?并且,如果我打算使用整个内容,该如何与将签名信息添加到标头会更改计算签名时用作输入内容的事实相协调呢?

是否有更直接的方法让JAX-WS使用我忽略的必需签名约定来生成请求?

然后还有一些次要的奖金问题:

  1. 是否有关于BinarySecret使用组合客户端和服务器值时传递客户端和服务器值的顺序的既定标准PSHA1

  2. TimestampSignedInfo/Reference项是否重要?如果是,那么计算的正确方法是DigestValue什么?

aro*_*oth 6

经过一些研究和反复试验,我设法找到了可行的解决方案。我将从奖金问题开始:

  1. 我没有找到任何正式文档,遇到的每个参考实现和代码示例始终总是首先传递客户端密钥,这也是服务器(Microsoft IIS v8.5)所期望的。因此,即使它不是正式的标准,也似乎是标准。

  2. 是的,TimestampReference值在很大程度上具有重要意义,并且与主要问题密切相关。

那么,如果必须使用Java使用JAX-WS手动签名出站SOAP消息中的内容,那么实际的算法是什么?

该参考资料是一个有用的起点,应该使您对SOAP世界中已变得过于架构化的事情有了一个很好的了解。引导它的过程有些晦涩难懂。例如:

3.2.2签名验证

  1. KeyInfo从外部来源获取密钥信息。
  2. 获得的规范形式SignatureMethod使用 CanonicalizationMethod,并使用结果(和先前获得的 KeyInfo),以确认SignatureValueSignedInfo元件。

如果你KeyInfoSecurityTokenReference一个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>
Run Code Online (Sandbox Code Playgroud)

...您想从Reference元素开始,该元素指向带有id“ _0” 的元素(在本例中为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);
Run Code Online (Sandbox Code Playgroud)

那会给你这样的东西(换行符不是规范的,抱歉):

<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>
Run Code Online (Sandbox Code Playgroud)

现在,您需要计算该DigestValue字符串的。在DigestMethod我们的元Reference元告诉我们,这应该是一个SHA1哈希(base64编码)。如此简单:

MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
String canonDigestValue = Base64.encodeBytes(sha1.digest(canonTimestamp));
Run Code Online (Sandbox Code Playgroud)

您获得的值将包含在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);
Run Code Online (Sandbox Code Playgroud)

哪个应该使您网购:

<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>
Run Code Online (Sandbox Code Playgroud)

...然后您根据指定的SignatureMethod算法对整个事物进行签名,在我们的例子中是HmacSHA1

Mac mac = Mac.getInstance("HmacSHA1");
SecretKeySpec key = new SecretKeySpec(getSharedKey(), "HmacSHA1");
mac.init(key);

String signature = Base64.encodeBytes(mac.doFinal(bytesToSign)); 
Run Code Online (Sandbox Code Playgroud)

getSharedKey()在这种情况下,... where 返回使用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;
}
Run Code Online (Sandbox Code Playgroud)

无论如何,这时您应该具有一个签名值,并且可以将其附加到Security出站消息的标头中,例如:

SOAPElement sigValue = sigElem.addChildElement(new QName("SignatureValue"));
sigValue.addTextNode(signature);
Run Code Online (Sandbox Code Playgroud)

如果一切顺利,则消息现在已经成功签名并且服务器的质量可以接受。

尽管我最后指出了一个警告,那就是该Timestamp值需要在服务器的时区中生成(在本例中为UTC),否则它将由于时戳或将来的时间戳而拒绝该请求。已经过期了。一个简单的问题可以通过对UNIX纪元时间戳进行标准化来解决。但是出于某种原因,他们改用“ yyyy-mm-dd'T'hh:mm:ss.msec'Z'”。去搞清楚。

我希望这对下一个不幸的灵魂有所帮助,他不得不尝试使Java使用SOAP / XML与.NET进行通信。

最后一点,如果您使用的是Apache XML Security。您需要先调用,org.apache.xml.security.Init.init()然后再尝试使用Canonicalizer,例如从static初始化程序块中进行调用。如果不这样做,则在尝试规范化时会出现异常(我认为是NPE)。

  • 实际上,作为 P_SHA1 参数的熵的顺序由 WS-Trust 正式指定。在您的特定情况下,“ComputedKey”URI 是“http://schemas.xmlsoap.org/ws/2005/02/trust/CK/PSHA1”,表示 WS-Trust 的 2005 年 2 月修订版(http://specs .xmlsoap.org/ws/2005/02/trust/WS-Trust.pdf)。来自规范:`密钥是使用 TLS 规范中的 P_SHA1 计算的,以使用来自双方的熵生成位流。确切的形式是:key = P_SHA1 (Ent_REQ, Ent_RES)` (3认同)
  • 我希望我能投票赞成这个1000倍。您是在这里跟进您的发现的英雄。 (2认同)
  • 我非常感谢 stackoverflow 网站,帮助了我无数次。但是这个问题和这个答案,两者都如此完整,只是我这些年来在 SO 中得到的最惊人的帮助,正如我所说的,这很多。@aroth,正如乔纳森所说,我希望我能投票 1000 次,非常感谢你。 (2认同)