Creating Signed SOAP Message as a String with C#

2020-07-18 11:35发布

问题:

I need to call web service that I have to send such soap request below by using C#. SoapBody and TimeStamp must be signed.

 <soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:web="http://xyzt.com/">
   <soap:Header>
      <wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
         <wsse:BinarySecurityToken EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary" ValueType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509PKIPathv1" wsu:Id="X509-F4AF9673207AC5E0B614180667985061">MIIFsDCCBawwggSUoAMCAQICBgCaWhnEajANBgkqhkiG9w0BAQsFADBcMQswCQYDVQQGEwJUUjFNMEsGA1UEAwxETWFsaSBNw7xow7xyIEVsZWt0cm9uaWsgU2VydGlmaWthIEhpem1ldCBTYcSfbGF5xLFjxLFzxLEgLSBTw7xyw7xtIDEwHhcNMT</wsse:BinarySecurityToken>
         <ds:Signature Id="SIG-3" xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
            <ds:SignedInfo>
               <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#">
                  <ec:InclusiveNamespaces PrefixList="soap web" xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#"/>
               </ds:CanonicalizationMethod>
               <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
               <ds:Reference URI="#id-2">
                  <ds:Transforms>
                     <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#">
                        <ec:InclusiveNamespaces PrefixList="web" xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#"/>
                     </ds:Transform>
                  </ds:Transforms>
                  <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
                  <ds:DigestValue>IZVrIpPCxiPcvyVOVv/d4nRPZWM=</ds:DigestValue>
               </ds:Reference>
               <ds:Reference URI="#TS-1">
                  <ds:Transforms>
                     <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#">
                        <ec:InclusiveNamespaces PrefixList="wsse soap web" xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#"/>
                     </ds:Transform>
                  </ds:Transforms>
                  <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
                  <ds:DigestValue>fltghgDztDtuVQX7y4t0ZJxAnxE=</ds:DigestValue>
               </ds:Reference>
            </ds:SignedInfo>
            <ds:SignatureValue>IOVXxBTp053aNJMbQj+VTiBblZ63peyJ1vWazKmEWNxN7RaeFfKELoxede8xQEqzSaB/u8exC7LLGYiEdChboVCf9liLMN4MmNj5JR6gfDrsL3azThf5hxLQ+WIE20PRoU6ozpp20zC1IaO3IU4ZaRLw</ds:SignatureValue>
            <ds:KeyInfo Id="KI-F4AF9673207AC5E0B614180667986422">
               <wsse:SecurityTokenReference wsse11:TokenType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509PKIPathv1" wsu:Id="STR-F4AF9673207AC5E0B614180667986643" xmlns:wsse11="http://docs.oasis-open.org/wss/oasis-wss-wssecurity-secext-1.1.xsd">
                  <wsse:Reference URI="#X509-F4AF9673207AC5E0B614180667985061" ValueType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509PKIPathv1"/>
               </wsse:SecurityTokenReference>
            </ds:KeyInfo>
         </ds:Signature>
         <wsu:Timestamp wsu:Id="TS-1">
            <wsu:Created>2014-12-08T21:26:36.191Z</wsu:Created>
            <wsu:Expires>2014-12-08T21:36:36.191Z</wsu:Expires>
         </wsu:Timestamp>
      </wsse:Security>
   </soap:Header>
   <soap:Body wsu:Id="id-2" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
      <web:getStatus>
         <itemID>1234567</itemID>
      </web:getStatus>
   </soap:Body>
</soap:Envelope>

I created this soap request and get good response by using WCF Client CustomBinding and .pfx file that has certificate with private key.

Most of examples about signing soap messages use certificate from certificate store or pfx file. But in my scenario user's certificate (with private key) stored on smart card that certificate private key can't be exported. So in this case I can not use WCF CustomBinding or I can not use SignedXml class to sign SOAP message because when i try to get certificate programatically then private key is missing. (Also I got private key by using NCryptoki - PKCS wrapper but this private key type is different then RSA that i can't set for WCF client or SignedXmlClass private key.)

So, I try to create this SOAP message as a string, and manually create DigestValues, BinarySecurityToken and SignatureValue by using smart card.

I can calculate BinarySecurityToken value as :

var certificate = GetX5092Certificate(); // X5092 certificate on smart card without private key
string binarySecToken= Convert.ToBase64String(certificate.RawData);

Also i have some code to calculate digest value as:

byte[] dataToHashTS = Encoding.UTF8.GetBytes(TimeStampReference.OuterXml);
XmlDsigExcC14NTransform transformDataTS = new XmlDsigExcC14NTransform("wsse soap web");
transformDataTS.LoadInput(new MemoryStream(dataToHashTS));
byte[] bDigestDataTS = transformDataTS.GetDigestedOutput(SHA1Managed.Create());
string sDigestDataTS = Convert.ToBase64String(bDigestDataTS); //timestamp digest
  • I am not sure that If I calculate digest values right or not?

  • To calculate SignatureValue, I think I need to get hash of SignedInfo part. I have method that sign content (byte array) by using smart card. So how I can send SignedInfo content to this method? I mean is it enough to get hash of SignedInfo block as a string? OR I have get SignedInfo as XmlElement then transform + hash as i did to calculate digest values?

Any help would be much appreciated. Thanks.

回答1:

  1. For the DigestValue you need to you need to canonicalize a string like this:
   <u:Timestamp u:Id="_0" xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
              <u:Created>2016-06-14T22:56:10.896Z</u:Created>
              <u:Expires>2016-06-14T23:01:10.896Z</u:Expires>
          </u:Timestamp>

So you can just put that string as a parameter here:

private string CanonicalizeDsig(string input)
{
    XmlDocument doc = new XmlDocument();
    doc.PreserveWhitespace = false;
    try
    {
        doc.LoadXml(input);
        XmlDsigC14NTransform trans = new XmlDsigC14NTransform();
        trans.LoadInput(doc);
        String c14NInput = new StreamReader((Stream)trans.GetOutput(typeof(Stream))).ReadToEnd();

        return c14NInput;


    }
    catch (Exception ex)
    {
        return String.Empty;
    }

}

After canonicalization you can now compute for the Hash: (mine is a SHA1 example). So put the return value of the above method on the parameter of this one. to get something like JCMdwz5g8iq05Lj6tjfDOxKqT4k=

private string ComputeHashSHA1(string input)
{
    try
    {
        SHA1CryptoServiceProvider sha1Hasher = new SHA1CryptoServiceProvider();
        byte[] hashedDataBytes = sha1Hasher.ComputeHash(Encoding.UTF8.GetBytes(input));
        string digestValue = Convert.ToBase64String(hashedDataBytes);
        return digestValue;

    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message);
        return String.Empty;
    }

}
  1. The Signature value is a tricky one and I can only cover a specific example. Check the service's WSDL it if has a policy that looks something like this.
<sp:Trust10> <wsp:Policy> <sp:MustSupportIssuedTokens/> <sp:RequireClientEntropy/> <sp:RequireServerEntropy/> </wsp:Policy>

This means you need to combine the Client Entropy (which is your key - any string that is based 64 that you sent to the server on get token request) and the Server Entropy (the return base 64 key)

You can combine them using the Microsoft.IdentityModel dll where there is a KeyGenerator object.

Your input will be something like this and it also needs canonicalization with DsigExcC14N:

              <SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
              <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>JCMdwz5g8iq05Lj6tjfDOxKqT4k=</DigestValue>
              </Reference>
          </SignedInfo>

Here is the canonicalization:

private string CanonicalizeExc(string input)
{
    XmlDocument doc = new XmlDocument();
    doc.PreserveWhitespace = false;
    try
    {
        doc.LoadXml(input);
        XmlDsigExcC14NTransform trans = new XmlDsigExcC14NTransform();
        trans.LoadInput(doc);
        String c14NInput = new StreamReader((Stream)trans.GetOutput(typeof(Stream))).ReadToEnd();

        return c14NInput;


    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.ToString());
        return String.Empty;
    }

}

Then here is how you get the signature value:

        private string ComputeHMACSHA1_PSHA(string input, string serversecret, string clientsecret)
    {
        try
        {
            byte[] signedInfoBytes = Encoding.UTF8.GetBytes(input);

            byte[] binarySecretBytesServer = Convert.FromBase64String(serversecret);
            byte[] binarySecretBytesClient = Convert.FromBase64String(clientsecret);

            byte[] key = KeyGenerator.ComputeCombinedKey(binarySecretBytesClient, binarySecretBytesServer, 256);


            HMACSHA1 hmac = new HMACSHA1(key);
            hmac.Initialize();

            byte[] hmacHash = hmac.ComputeHash(signedInfoBytes);
            string signatureValue = Convert.ToBase64String(hmacHash);
            return signatureValue;
        }
        catch (Exception ex)
        {
            return string.Empty;
        }
    }

It will give you something like this. kykmlowWIW4TXRcCi46OfZPUBKQ=