代码之家  ›  专栏  ›  技术社区  ›  aroth

SOAP、WCF和消息签名

  •  7
  • aroth  · 技术社区  · 8 年前

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

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

    服务器似乎希望使用 hmac-sha1 使用客户端/服务器密钥的身份验证代码( PSHA1 算法)。很公平。然而,JAX-WS似乎想使用 rsa-sha1 以及一个X509证书,用于对出站消息进行签名(服务器不喜欢),并且似乎只使用 hmac-sha1型 如果 UsernameToken (服务器也不喜欢)。

    因此,我正在尝试从 SOAPHandler 实施客户端为获取安全上下文令牌而发送的请求如下所示:

    <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风暴发送请求,使用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使用我忽略的所需签名约定生成请求?

    还有一些小的奖金问题:

    1. 我传递客户机和服务器的顺序是否有既定标准 二进制机密 使用组合它们时的值 PSHA1型 ?

    2. 时间戳 签名信息/参考 如果是,正确的计算方法是什么 摘要值 ?

    1 回复  |  直到 7 年前
        1
  •  13
  •   aroth    8 年前

    经过一些研究和一段时间的反复尝试,我终于找到了一个可行的解决方案。我先从奖金问题开始:

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

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

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

    This reference 这是一个很有帮助的起点,应该能让您很好地了解SOAP世界中的总体架构。其中的一些描述非常愚蠢。例如:

    3.2.2签名验证

    1. 从获取密钥信息 KeyInfo 或来自外部源。
    2. 获取 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,我认为是这样)。