Signing SOAP messages using X.509 certificate from WCF service to Java webservice

29,632

OK. After few tries and errors here is the solution using SignedXml and IClientMessageInspector/BeforeSendRequest pattern. Thanks a lot to Yaron Naveh for his relevant suggestions.

// Sign an XML request and return it
public static string SignRequest(string request, string SubjectName, string Signature, string keyInfoRefId)
{
    if (string.IsNullOrEmpty(request))
        throw new ArgumentNullException("request");
    if (string.IsNullOrEmpty(SubjectName))
        throw new ArgumentNullException("SubjectName");

    // Load the certificate from the certificate store.
    X509Certificate2 cert = GetCertificateBySubject(SubjectName);

    // Create a new XML document.
    XmlDocument doc = new XmlDocument();

    // Format the document to ignore white spaces.
    doc.PreserveWhitespace = false;

    // Load the passed XML 
    doc.LoadXml(request);

    // Add the declaration as per Entrust sample provided -don't think it's necessary though
    if (!(doc.FirstChild is XmlDeclaration))
    {
        XmlDeclaration declaration = doc.CreateXmlDeclaration("1.0", "UTF-8", string.Empty);
        doc.InsertBefore(declaration, doc.FirstChild);
    }

    // Remove the Action (MustUnderstand). 
    // TODO: Need to find a more elegant way to do so
    XmlNode headerNode = null;
    XmlNodeList nodeList = doc.GetElementsByTagName("Action");
    if (nodeList.Count > 0)
    {
        headerNode = nodeList[0].ParentNode;
        headerNode.RemoveChild(nodeList[0]);
    }

    // Set the body id - not in used but could be useful at a later stage of this project
    XmlNamespaceManager ns = new XmlNamespaceManager(doc.NameTable);    
    ns.AddNamespace("s", "http://schemas.xmlsoap.org/soap/envelope/");
    XmlElement body = doc.DocumentElement.SelectSingleNode(@"//s:Body", ns) as XmlElement;    
    if (body == null)    
        throw new ApplicationException("No body tag found");
    body.RemoveAllAttributes();  // no need to have namespace
    body.SetAttribute("Id", "ABC"); // Body Id could be passed as a param

    // Create a custom SignedXml object so that we could sign the keyinfo
    CustomSignedXml signedXml = new CustomSignedXml(doc);

    // Add the key to the SignedXml document. 
    signedXml.SigningKey = cert.PrivateKey;

    // Create a new KeyInfo object.
    KeyInfo keyInfo = new KeyInfo();
    keyInfo.Id = keyInfoRefId;

    // Load the certificate into a KeyInfoX509Data object
    // and add it to the KeyInfo object.
    KeyInfoX509Data keyInfoData = new KeyInfoX509Data();
    keyInfoData.AddCertificate(cert);
    keyInfo.AddClause(keyInfoData);

    // Add the KeyInfo object to the SignedXml object.
    signedXml.KeyInfo = keyInfo;

    // Create a reference to be signed.
    Reference reference = new Reference();
    reference.Uri = "";

    // Add an enveloped transformation to the reference.
    XmlDsigEnvelopedSignatureTransform env = new XmlDsigEnvelopedSignatureTransform();
    reference.AddTransform(env);

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

    Reference reference2 = new Reference();
    reference2.Uri = "#" + keyInfoRefId;
    signedXml.AddReference(reference2);

    // Add the Signature Id
    signedXml.Signature.Id = Signature;

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

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

    // Append the Signature element to the XML document.
    if (headerNode != null)
    {                
        headerNode.AppendChild(doc.ImportNode(xmlDigitalSignature, true));
    }

    return doc.InnerXml;
}

public static X509Certificate2 GetCertificateBySubject(string CertificateSubject)
{
    // Check the args.
    if (string.IsNullOrEmpty(CertificateSubject))
        throw new ArgumentNullException("CertificateSubject");

    // Load the certificate from the certificate store.
    X509Certificate2 cert = null;

    X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser);

    try
    {
        // Open the store.
        store.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly);

        // Find the certificate with the specified subject.
        cert = store.Certificates.Find(X509FindType.FindBySubjectName, CertificateSubject, false)[0];

        // Throw an exception of the certificate was not found.
        if (cert == null)
        {
            throw new CryptographicException("The certificate could not be found.");
        }
    }
    finally
    {
        // Close the store even if an exception was thrown.
        store.Close();
    }

    return cert;
}

and the CustomSignedXml class:

public class CustomSignedXml : SignedXml
{
    public CustomSignedXml(XmlDocument doc) : base(doc)
    {
        return;
    }
    public override XmlElement GetIdElement(XmlDocument doc, string id)
    {
        // see if this is the key info being referenced, otherwise fall back to default behavior
        if (String.Compare(id, this.KeyInfo.Id, StringComparison.OrdinalIgnoreCase) == 0)
            return this.KeyInfo.GetXml();
        else
            return base.GetIdElement(doc, id);
    }
}
Share:
29,632
Nomadefv
Author by

Nomadefv

Updated on July 16, 2022

Comments

  • Nomadefv
    Nomadefv almost 2 years

    It's my first question over the web. Hope it will make sense.

    I have seen several blogs related to this issue over the Web, and I have tried few of the ideas presented in them with no success. Here's my situation:

    I have a web App calling a WCF web service which then call a Java web service. They are all on different servers. The call between the WCF web service to the java web service is not over https as the certificate will be enough to identify the caller (Message security therefore).

    • Java web service (black box)

    The Java web service requires to received a signed message and works as per below:
    Before each request is processed a handler intercepts all incoming messages and performs the following validation rules:
    1. Does the message contain a security header
    2. Does the message contain the correct security header ID
    3. Is the message been signed correctly
    4. Does the message contain a KeyInfo x.509 certificate
    5. Is the certificate issued from a trusted CA – configuration based
    6. Is the certificate valid (not expired, revoked)
    7. Does the certificate contain the correct policy OID

    Once all of these steps have been confirmed then the message can be processed, if any step fails then a soap message exception will be returned.

    The SOAP security header should validate against xxx...w3.org/TR/SOAP-dsig/ digital signature specification.

    The most complete description can be found here xxx...ibm.com/developerworks/webservices/library/ws-security.html this IBM article lists the details of each WS-Security header, additionally a sample signed SOAP message has been provided.

    When signing the SOAP message you must also add they x.509 certificate into the message KeyInfo this is required for the certificate validation.

    SOAP Request should like this:

    <?xml version="1.0" encoding="UTF-8"?>
    <S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/">
    <S:Header>
    <ds:Signature xmlns:ds="xxx...w3.org/2000/09/xmldsig#" Id="Signature001">
    <ds:SignedInfo>
    <ds:CanonicalizationMethod Algorithm="xxx...w3.org/TR/2001/REC-xml-c14n-20010315"/>
    <ds:SignatureMethod Algorithm="xxx...w3.org/2000/09/xmldsig#rsa-sha1"/>
    <ds:Reference URI="">
    <ds:Transforms>
    <ds:Transform Algorithm="xxx...w3.org/2000/09/xmldsig#enveloped-signature"/>
    </ds:Transforms>
    <ds:DigestMethod Algorithm="xxx...w3.org/2000/09/xmldsig#sha1"/>
    <ds:DigestValue>soe1PnaGXVGrsauC61JSHD+uqGw=</ds:DigestValue>
    </ds:Reference>
    <ds:Reference URI="#KeyInfo001">
    <ds:DigestMethod Algorithm="xxx...w3.org/2000/09/xmldsig#sha1"/>
    <ds:DigestValue>Y9SRPQ9TcDu+GazO3LFwodEdhaA=</ds:DigestValue>
    </ds:Reference>
    </ds:SignedInfo>
    <ds:SignatureValue>jBX/8XkY2aCte7qgXEp1sbNWmQcK/90iVL58sAvwYAEcBABGzOk2agxR0HvWrNa6ixkocAQ205lggwOxnxZJvoVozVYAAjcLtayPBOUYrnSEBFrwKWP/vxgvUDRIdXeIuw5GLY87NrTQMm1Ehf/HvMX9hTBJn4Nm8RdDiUmPcIo=</ds:SignatureValue>
    <ds:KeyInfo Id="KeyInfo001">
    <ds:X509Data>
    <ds:X509Certificate>MIIEbZCCA1WgAwIBAgIES1XpMjANBgkqhkiG9w0BAQUFADBYMRUwEwYKCZImiZPyLGQBGRYFbG9jYWwxFzAVBgoJkiaJk/IsZAEZFgdlbnRydXN0MRIwEAYDVQQDEwllbnRydXN0U00xEjAQBgNVBAMTCWVudHJ1c3RDQTAeFw0xMDA0MjIxMDQ4MDBaFw0xMzA0MjIxMTE4MDBaMGoxFTATBgoJkiaJk/IsZAEZFgVsb2NhbDEXMBUGCgmSJomT8ixkARkWB2VudHJ1c3QxEjAQBgNVBAMTCWVudHJ1c3RTTTESMBAGA1UEAxMJZW50cnVzdENBMRAwDgYDVQQDEwdSYnMgUmJzMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDMf88L2JjLPG1hNmTA/KBiC53WVwS2WU9Jh3lC1Rob6RMzOojomZ/dNrvSRB6nzWeXJpZXwik4XFrsAq24By2SZpLTO4p8Vcq71mTAfDu33cnO49Au2pwNvcMn5qIKBk1Xx+oVb4fzK9ncTRu7bW46HsIYth+qkGhbI2JEHwr/zwIDAQABo4IBrzCCAaswCwYDVR0PBAQDAgeAMCsGA1UdEAQkMCKADzIwMTAwNDIyMTA0ODAwWoEPMjAxMjA1MjgxNTE4MDBaMCMGA1UdIAQcMBowCwYJYIZIAYb6awoEMAsGCSqGSIb2fQdLAzAbBgNVHQkEFDASMBAGCSqGSIb2fQdEHTEDAgEBMIHGBgNVHR8Egb4wgbswb6BtoGukaTBnMRUwEwYKCZImiZPyLGQBGRYFbG9jYWwxFzAVBgoJkiaJk/IsZAEZFgdlbnRydXN0MRIwEAYDVQQDEwllbnRydXN0U00xEjAQBgNVBAMTCWVudHJ1c3RDQTENMAsGA1UEAxMEQ1JMMTBIoEagRIZCZmlsZTovLy8vTVNJREhVLTQ0NUE0RkVFL0NSTC9lbnRydXN0Y2FfZW50cnVzdHNtX2xvY2FsX2NybGZpbGUuY3JsMB8GA1UdIwQYMBaAFBvSL6cPz8L5shubV58yf0pczKzuMB0GA1UdDgQWBBT1/j6OSS8FTjwqluvew16sv7h+VzAJBgNVHRMEAjAAMBkGCSqGSIb2fQdBAAQMMAobBFY4LjADAgSwMA0GCSqGSIb3DQEBBQUAA4IBAQBXxRIA4HUvGSw4L+4uaR51pY4ISjUQWo2Fh7FYBMt29NsKCTdur1OWVVdndt1yjXP4yWXxoAhHtvZL+XNALUFlR2HAWiXuL1nRcxHkB98N5gPqQzW/lJk9cLtL4hVp28EiEpgmKT3I3NP2Pdb2G5MMOdvQ/GFb2y6OwblR8ViPQ8B2aHWzXMrH+0qadPAuBhXyAohwb+mMuYT/ms6xpGi1NMYuYMf6XONz9GkZgnGnMwa+9CCQws1HNz8WYHtmFIxLsVuEWc/0a1vg4IYX1Ds/ttyhJGTVXOSJSkBz8kRyj1pNBDdc1KeG8M++O8m8VgRTJvYaPc7NMiclISukGpea</ds:X509Certificate> </ds:X509Data>
    </ds:KeyInfo>
    </ds:Signature>
    </S:Header>
    <S:Body Id="ABC">
    <ns2:createUser xmlns:ns2="http://webservice.rbs.emea.ps.entrust.com/" xmlns:ns3="http://webservice.rbs.emea.ps.entrust.com/types/CertificateException" xmlns:ns4="http://webservice.rbs.emea.ps.entrust.com/types/UserException">
    <userID>0061020051</userID>
    </ns2:createUser>
    </S:Body>
    </S:Envelope>
    
    • WCF web service

    I have one server certificate (p7b format from a trusted CA) that I installed where my WCF web service workstation (dev) is by using the mmc Certificate Snap-in (at the moment cert is in the Trusted Publishers). I don't think I need another cert on the Java server as the response should be in clear (neither signed or encrypted). I am still a bit confused on this certificate -and certificates in general- as it seems to hold only a public key.

    Here is the app.config of my test project:

    <client>
      <endpoint address="http://entrust-user-certification-uat.fm.rbsgrp.net/rbs/WebAS"
        behaviorConfiguration="endpointCredentialsBehavior" binding="wsHttpBinding"
        bindingConfiguration="WebAsServicePortTypeBinding" contract="IWebAsServicePortType"
        name="WebAsServicePortType">
        <!--<identity>
          <dns value="entrust-user-certification-uat.fm.rbsgrp.net" />
        </identity>-->
      </endpoint>
    </client>
    <bindings>
      <wsHttpBinding>
        <binding name="WebAsServicePortTypeBinding" closeTimeout="00:01:00"
          openTimeout="00:01:00" receiveTimeout="00:10:00" sendTimeout="00:01:00"
          bypassProxyOnLocal="false" hostNameComparisonMode="StrongWildcard"
          maxBufferPoolSize="524288" maxReceivedMessageSize="65536" messageEncoding="Text"
          textEncoding="utf-8" useDefaultWebProxy="true" allowCookies="false">
          <readerQuotas maxDepth="32" maxStringContentLength="8192" maxArrayLength="16384"
            maxBytesPerRead="4096" maxNameTableCharCount="16384" />
          <security mode="Message">
            <message clientCredentialType="Certificate" negotiateServiceCredential="false"
              establishSecurityContext="false" />
          </security>
        </binding>
      </wsHttpBinding>
    </bindings>
    
    <behaviors>
      <endpointBehaviors>
        <behavior name="endpointCredentialsBehavior">
          <clientCredentials>
            <clientCertificate findValue="entrust-user-certification-uat.fm.rbsgrp.net"
               storeLocation="LocalMachine" storeName="TrustedPublisher"
              x509FindType="FindBySubjectName"></clientCertificate>
            <serviceCertificate>
              <!--   
              Setting the certificateValidationMode to PeerOrChainTrust means that if the certificate 
              is in the user's Trusted People store, then it will be trusted without performing a
              validation of the certificate's issuer chain. This setting is used here for convenience so that the 
              sample can be run without having to have certificates issued by a certificate authority (CA).
              This setting is less secure than the default, ChainTrust. The security implications of this 
              setting should be carefully considered before using PeerOrChainTrust in production code. 
              -->
              <authentication certificateValidationMode="None" revocationMode="NoCheck" trustedStoreLocation="LocalMachine"/>
            </serviceCertificate>
          </clientCredentials>
        </behavior>
      </endpointBehaviors>
    </behaviors>
    

    When I ran a simple test:
         WebAS entrustService = new WebAS();
         ActivationCodes certCodes = entrustService.createUser("testNomad");
    I've got the error:
         failed: System.Web.Services.Protocols.SoapException: javax.xml.soap.SOAPException: No Signature element found in soap message

    How could I force the signing process for each message? I was thinking I could do it through WCF configuration quite easily. Any help would be greatly appreciated !

  • Nomadefv
    Nomadefv over 13 years
    Thanks a lot for your comments! After an intense day looking again at this issue and speaking with the Java developer, we should really need only one cert on the client (wcf) and just sign all outgoing messages with it. I looked a bit more and implementing IClientMessageInspector could be the solution. I am still digging...
  • Nomadefv
    Nomadefv over 13 years
    Java wev service message security is not described in WSDL. Looking now at IClientMessageInspector and BeforeSendRequest.
  • Mr. Robot
    Mr. Robot over 7 years
    Hi, It's not very clear to me how to use this in BeforeSendRequest method. Can u explain?