【问题标题】:SOAP, WCF, and message signaturesSOAP、WCF 和消息签名
【发布时间】:2016-07-29 08:48:30
【问题描述】:

我有一个 Java(基于 JAX-WS)SOAP 客户端,我正试图与基于 WCF 的(第三方)服务器通信。我发现sentiment expressed here 非常准确。但目标仍然存在。

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

服务器似乎希望使用客户端/服务器密钥(PSHA1 算法)使用hmac-sha1 身份验证代码对消息进行签名。很公平。然而 JAX-WS 似乎想要使用 rsa-sha1 和 X509 证书来签署出站消息(服务器不喜欢),并且似乎只在提供 UsernameToken 时才使用 hmac-sha1(服务器也不喜欢)。

所以我尝试从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>

我正在使用PSHA1 组合客户端和服务器BinarySecret 密钥,如下所示:

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>

...我的意思是计算&lt;Envelope&gt;...&lt;/Envelope&gt; 的签名值(所以整个事情),或者只是&lt;Body&gt;...&lt;/Body&gt;,甚至只是BBB... 部分?如果我打算使用整个东西,我该如何协调这与将签名信息添加到标头会改变计算签名时用作输入的内容这一事实?

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

还有一些小问题:

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

  2. Timestamp 和 SignedInfo/Reference 条目是否重要,如果是,计算 DigestValue 的正确方法是什么?

【问题讨论】:

    标签: java wcf soap soap-client


    【解决方案1】:

    经过一些研究和相当多的反复试验,我设法找到了一个可行的解决方案。我先从附赠问题开始:

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

    2. 是的,Timestamp 和 Reference 值非常重要,并且与主要问题密切相关。

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

    This reference 是一个很有帮助的起点,它应该能让您很好地了解 SOAP 世界中过度架构的事物已经变得多么。其中一些描述得非常晦涩,开机。例如:

    3.2.2 签名验证

    1. 从KeyInfo 或外部来源获取密钥信息。
    2. 使用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“_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);
    

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

    <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。 Reference 元素中的 DigestMethod 元素告诉我们这应该是一个 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() 返回使用客户端和服务器在初始RequestSecurityToken 交换期间发送的BinarySecret 值派生的密钥。如:

    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);
    

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

    虽然我注意到最后一个警告,那就是 Timestamp 值需要在服务器的时区(在本例中为 UTC)中生成,否则它将由于时间戳而拒绝请求要么来自未来,要么已经过期。一个简单的问题,可以通过标准化 UNIX 纪元时间戳来解决。但由于某种原因,他们改为使用“yyyy-mm-dd'T'hh:mm:ss.msec'Z'”。去图吧。

    我希望这对下一个不得不尝试使用 SOAP/XML 让 Java 与 .NET 对话的不幸者有所帮助。

    最后一点,如果您使用的是 Apache XML Security。在尝试使用Canonicalizer 之前,您需要调用org.apache.xml.security.Init.init(),例如在static 初始化程序块中。如果你不这样做,当你尝试规范化时,你会得到一个异常(我认为是 NPE)。

    【讨论】:

    • 实际上,作为P_SHA1 参数的熵的顺序由WS-Trust 正式指定。在您的特定情况下,ComputedKey URI 是 http://schemas.xmlsoap.org/ws/2005/02/trust/CK/PSHA1,它表示 WS-Trust 的 2005 年 2 月修订版 (specs.xmlsoap.org/ws/2005/02/trust/WS-Trust.pdf)。来自规范:The key is computed using P_SHA1 from the TLS specification to generate a bit stream using entropy from both sides. The exact form is: key = P_SHA1 (Ent_REQ, Ent_RES)
    • 我希望我能投票给这 1000 倍。您是在这里跟进您的发现的英雄。
    • 对于任何需要在 JavaScript 中执行此操作的人,以下是生成签名的方法:stackoverflow.com/a/55053741/5065447
    • 我非常感谢 stackoverflow 网站,帮助了我无数次。但是这个问题和这个答案都如此完整,这只是我这些年来在 SO 中得到的最惊人的帮助,正如我所说,这很多。 @aroth,正如乔纳森所说,我希望我能投票 1000 次,非常感谢。
    猜你喜欢
    • 2010-12-16
    • 2011-11-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-05-04
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多