SOAP、WCF 和消息签名

SOAP, WCF, and message signatures

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

长话短说,我可以从服务器中哄出一个有效的 "security context token",但我被 message-signing 问题挂断了(我相信)。

服务器似乎希望使用 hmac-sha1 身份验证代码和 client/server 密钥(PSHA1 算法)对邮件进行签名。很公平。但是 JAX-WS 似乎想使用 rsa-sha1 和 X509 证书来签署出站消息(服务器不喜欢),并且似乎只在 [=21] 时才使用 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>

这导致 "An error occurred when verifying security for the message" 从服务器返回响应。

使用 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>

主要区别是:

所以在所有这些之后,我想主要的问题是:

签署出站消息的实际算法是什么? 如,如果我有:

<Envelope>
    <Header>
        HHH...
    </Header>
    <Body>
        BBB...
    </Body>   
</Envelope>

...我是要计算 <Envelope>...</Envelope> 的签名值(所以整个事情),还是只是 <Body>...</Body>,甚至只是 BBB... 部分?如果我打算使用整个东西,我该如何协调将签名信息添加到 header 会改变计算签名时用作输入的内容的事实?

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

然后是一些小的奖励问题:

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

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

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

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

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

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

This reference 是一个很有帮助的起点,应该可以让您很好地了解 SOAP 世界中过度架构的事物是如何发展的。并且其中一些描述非常迟钝,引导。例如:

3.2.2 Signature Validation

  1. Obtain the keying information from KeyInfo or from an external source.
  2. Obtain the canonical form of the SignatureMethod using the CanonicalizationMethod and use the result (and previously obtained KeyInfo) to confirm the SignatureValue over the SignedInfo element.

如果您的 KeyInfoSecurityTokenReferenceSecurityContextToken 本身实际上不包含任何关键数据,而您的 SignatureMethodAlgorithm="http://www.w3.org/2000/09/xmldsig#hmac-sha1" , 很清楚任何 CanonicalizationMethod 与那个有什么关系或者你应该如何从中得到知道你需要组合服务器和客户端 BinarySecret 值并将结果作为你的关键.但是我跑题了。

要应用的算法是 more-or-less 在 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>

现在您需要计算该字符串的 DigestValueReference 元素中的 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() returns,在这种情况下,使用客户端和服务器在初始 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 header,例如:

SOAPElement sigValue = sigElem.addChildElement(new QName("SignatureValue"));
sigValue.addTextNode(signature);

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

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

我希望这对下一个必须尝试使用​​ SOAP/XML.

与 .NET 通信的不幸的人有所帮助。

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