使用WCF对SOAP请求进行数字签名时指定#Body的引用URI

时间:2012-04-30 02:20:25

标签: c# wcf soap

我正在使用WCF客户端与非WCF Web服务进行通信。

此Web服务要求对SOAP消息的正文进行签名,但是,我在生成有效的SOAP请求时遇到问题。

我已经实现了一个继承自IClientMessageInspector的ClientMessageInspector,我在其中修改了BeforeSendRequest方法中的消息以添加XML数字签名。我使用SignedXML类来执行此操作。

我正在使用WSDL和SOAP的IBM Web服务验证工具来检查我的数字签名是否经过验证。

我的问题是,当我在Reference URI中指定一个完整的命名空间引用时,我正在使用的IBM工具说我有一个有效的签名。根据XML数字签名规范,我应该能够引用该属性,而不是命名空间,但是,当我这样做时,我没有获得有效的数字签名。

这是我正在生成的SOAP请求,我的工具说它具有有效的签名,但Web服务不喜欢它:

<?xml version="1.0" encoding="utf-8"?>
<s:Envelope xmlns:a="http://www.w3.org/2005/08/addressing" 
            xmlns:s="http://www.w3.org/2003/05/soap-envelope" 
            xmlns:soapsec="http://schemas.xmlsoap.org/soap/security/2000-12" 
            xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <s:Header>
    <soapsec:Signature>
      <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
        <SignedInfo>
          <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315" />
          <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" />
          <Reference URI="http://schemas.xmlsoap.org/soap/security/2000-12#Body">
            <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
            <DigestValue>4mt5wluUTu5tpR2d5UemVSLvqTs=</DigestValue>
          </Reference>
        </SignedInfo>
        <SignatureValue>UZ7HzfE3GxIY9hg...</SignatureValue>
        <KeyInfo>
          <X509Data>
            <X509Certificate>MIIEkTCCA3mgAwIBAgIQCu...</X509Certificate>
          </X509Data>
          <KeyValue>
            <RSAKeyValue>
              <Modulus>0C3e9HDx5Yq6FLUxIgjJ...</Modulus>
              <Exponent>AQAB</Exponent>
            </RSAKeyValue>
          </KeyValue>
        </KeyInfo>
      </Signature>
    </soapsec:Signature>
  </s:Header>
  <s:Body soapsec:id="Body">
    .... SOAP Body Here ...
  </s:Body>
</s:Envelope>

这是我想要生成的SOAP请求,但是我的工具说这个签名无效,而且Web服务也告诉我签名无效:

<?xml version="1.0" encoding="utf-8"?>
<s:Envelope xmlns:a="http://www.w3.org/2005/08/addressing" 
            xmlns:s="http://www.w3.org/2003/05/soap-envelope" 
            xmlns:soapsec="http://schemas.xmlsoap.org/soap/security/2000-12" 
            xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <s:Header>
    <soapsec:Signature>
      <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
        <SignedInfo>
          <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315" />
          <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" />
          <Reference URI="#Body">
            <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
            <DigestValue>4mt5wluUTu5tpR2d5UemVSLvqTs=</DigestValue>
          </Reference>
        </SignedInfo>
        <SignatureValue>UZ7HzfE3GxIY9hg...</SignatureValue>
        <KeyInfo>
          <X509Data>
            <X509Certificate>MIIEkTCCA3mgAwIBAgIQCu...</X509Certificate>
          </X509Data>
          <KeyValue>
            <RSAKeyValue>
              <Modulus>0C3e9HDx5Yq6FLUxIgjJ...</Modulus>
              <Exponent>AQAB</Exponent>
            </RSAKeyValue>
          </KeyValue>
        </KeyInfo>
      </Signature>
    </soapsec:Signature>
  </s:Header>
  <s:Body soapsec:id="Body">
    .... SOAP Body Here ...
  </s:Body>
</s:Envelope>

这是我在BeforeSendRequest中创建签名的代码,并相应地修改消息:

    public object BeforeSendRequest(ref System.ServiceModel.Channels.Message request, System.ServiceModel.IClientChannel channel)
    {
        XmlDocument doc = new XmlDocument();
        doc.PreserveWhitespace = true;
        doc.LoadXml(request.ToString());

        // Add the required namespaces to the SOAP Envelope element, if I don't do this, the web service I'm calling returns an error
        string soapSecNS = "http://schemas.xmlsoap.org/soap/security/2000-12";
        string soapEnvNS = "http://www.w3.org/2003/05/soap-envelope";

        //Get the header element, so that we can add the digital signature to it
        XmlNode headerNode = doc.GetElementsByTagName("Header", soapEnvNS)[0];

        // Set the ID attribute on the body element, so that we can reference it later
        XmlNode bodyNode = doc.GetElementsByTagName("Body", soapEnvNS)[0];

        ((XmlElement)bodyNode).RemoveAllAttributes();
        ((XmlElement)bodyNode).SetAttribute("id", soapSecNS, "Body");

        XmlWriterSettings settings2 = new XmlWriterSettings();
        settings2.Encoding = new System.Text.UTF8Encoding(false);

        // Load the certificate we want to use for signing
        SignedXmlWithId signedXml = new SignedXmlWithId(doc);
        X509Certificate2 cert = new X509Certificate2("C:\\myCertificate.pfx", "myPassword");

        signedXml.SigningKey = cert.PrivateKey;

        //Populate the KeyInfo element correctly, with the public cert and public key
        Signature sigElement = signedXml.Signature;
        KeyInfoX509Data x509Data = new KeyInfoX509Data(cert);
        sigElement.KeyInfo.AddClause(x509Data);

        RSAKeyValue rsaKeyValue = new RSAKeyValue((RSA)cert.PublicKey.Key);
        sigElement.KeyInfo.AddClause(rsaKeyValue);

        // Create a reference to be signed, only sign the body of the SOAP request, which we have given an 
        // ID attribute to, in order to reference it correctly here
        Reference reference = new Reference();
        reference.Uri = soapSecNS + "#Body";

        // Add the reference to the SignedXml object.
        signedXml.AddReference(reference);

        // Compute the signature.
        signedXml.ComputeSignature();

        // Get the XML representation of the signature and save
        // it to an XmlElement object.
        XmlElement xmlDigitalSignature = signedXml.GetXml();

        XmlElement soapSignature = doc.CreateElement("Signature", soapSecNS);
        soapSignature.Prefix = "soapsec";
        soapSignature.AppendChild(xmlDigitalSignature);

        headerNode.AppendChild(soapSignature);

        // Make sure the byte order mark doesn't get written out
        XmlDictionaryReaderQuotas quotas = new XmlDictionaryReaderQuotas();
        Encoding encoderWithoutBOM = new System.Text.UTF8Encoding(false);

        System.IO.MemoryStream ms = new System.IO.MemoryStream(encoderWithoutBOM.GetBytes(doc.InnerXml));

        XmlDictionaryReader xdr = XmlDictionaryReader.CreateTextReader(ms, encoderWithoutBOM, quotas, null);

        //Create the new message, that has the digital signature in the header
        Message newMessage = Message.CreateMessage(xdr, System.Int32.MaxValue, request.Version);
        request = newMessage;

        return null;
    }

有人知道如何将参考URI设置为#Body,还有一个有效的XML签名吗?

1 个答案:

答案 0 :(得分:2)

因为我试图为SOAP请求生成签名,所以我通过继承SignedXml来覆盖它,重写GetIdElement,这样我就可以返回我正在寻找的任何元素。在这种情况下,id元素将属于名称空间http://schemas.xmlsoap.org/soap/security/2000-12

public class SignedXmlWithId : SignedXml
{
    public SignedXmlWithId(XmlDocument xml)
        : base(xml)
    {
    }

    public SignedXmlWithId(XmlElement xmlElement)
        : base(xmlElement)
    {
    }

    public override XmlElement GetIdElement(XmlDocument doc, string id)
    {
        // check to see if it's a standard ID reference
        XmlElement idElem = base.GetIdElement(doc, id);

        if (idElem == null)
        {
            // I've just hardcoded it for the time being, but should be using an XPath expression here, and the id that is passed in
            idElem = (XmlElement)doc.GetElementsByTagName("Body", "http://schemas.xmlsoap.org/soap/security/2000-12")[0];
        }

        return idElem;
    }
}

我还意识到使用诸如用于WSDL和SOAP的IBM Web服务验证工具之类的工具来验证SOAP请求的数字签名是行不通的。相反,我使用以下方法来验证签名:

    public static bool verifyDigitalSignatureForString(string msgAsString)
    {
        XmlDocument verifyDoc = new XmlDocument();
        verifyDoc.PreserveWhitespace = true;
        verifyDoc.LoadXml(msgAsString);

        SignedXmlWithId verifyXml = new SignedXmlWithId(verifyDoc);
        // Find the "Signature" node and create a new
        // XmlNodeList object.
        XmlNodeList nodeList = verifyDoc.GetElementsByTagName("Signature");

        // Load the signature node.
        verifyXml.LoadXml((XmlElement)nodeList[0]);

        if (verifyXml.CheckSignature())
        {
            Console.WriteLine("Digital signature is valid");
            return true;
        }
        else
        {
            Console.WriteLine("Digital signature is not valid");
            return false;
        }
    }