12

I'm trying to consume Java Web Service using C# in desktop application.
My first attempt was using WebServicesClientProtocol, but I'm not able to add necessary attribute that is required by WSSE Username and Token Security Spec 1.1

I need to create request that has this structure:

<soap:Envelope xmlns:dz="http://dom.query.api.com" xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:xsd="http://dz.api.swd.zbp.pl/xsd">
    <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:UsernameToken wsu:Id="UsernameToken-E94CEB6F4708FB7C23148611494797612">
                <wsse:Username>my_login</wsse:Username>
                <wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">XqEwZ/CxaBfFvh487TjvN8qD63c=</wsse:Password>
                <wsse:Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">JzURe0CxvzRjmEcH/ndldw==</wsse:Nonce>
                <wsu:Created>2017-02-09T09:42:27.976Z</wsu:Created>
            </wsse:UsernameToken>
            <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-E94CEB6F4708FB7C2314861149479517">MIIKnDCCB.........nmIngeg6d6TNI=</wsse:BinarySecurityToken>
            <ds:Signature Id="SIG-E94CEB6F4708FB7C23148611494795311" 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="dz soap xsd" xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#"/>
                    </ds:CanonicalizationMethod>
                    <ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
                    <ds:Reference URI="#id-E94CEB6F4708FB7C23148611494795310">
                        <ds:Transforms>
                            <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#">
                                <ec:InclusiveNamespaces PrefixList="dz xsd" 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>mlABQuNUFOmLqsDswxXxQ6XnjpQ=</ds:DigestValue>
                    </ds:Reference>
                </ds:SignedInfo>
                <ds:SignatureValue>lYhBHSQ/L...XL1HEbMQjJ/Q2Rvg==</ds:SignatureValue>
                <ds:KeyInfo Id="KI-E94CEB6F4708FB7C2314861149479518">
                    <wsse:SecurityTokenReference wsse11:TokenType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509PKIPathv1" wsu:Id="STR-E94CEB6F4708FB7C2314861149479519" xmlns:wsse11="http://docs.oasis-open.org/wss/oasis-wss-wssecurity-secext-1.1.xsd">
                        <wsse:Reference URI="#X509-E94CEB6F4708FB7C2314861149479517" ValueType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509PKIPathv1"/>
                    </wsse:SecurityTokenReference>
                </ds:KeyInfo>
            </ds:Signature>
        </wsse:Security>
    </soap:Header>
    <soap:Body wsu:Id="id-E94CEB6F4708FB7C23148611494795310" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
        <dz:query>
            <dz:param>
                <xsd:userQueryId>27467</xsd:userQueryId>
            </dz:param>
        </dz:query>
    </soap:Body>
</soap:Envelope>

I've managed to create custom classes using IEndpointBehavior and IClientMessageInspector, but with them I'm only able to add UsernameToken

public class InspectorBehavior : IEndpointBehavior
{
    /// <summary>
    /// Gets or sets the custom ClientInspector.
    /// </summary>
    public ClientInspector ClientInspector { get; set; }

    /// <summary>
    /// Constructs a new InspectorBehavior
    /// </summary>
    /// <param name="clientInspector"><see cref="ClientInspector"/></param>
    public InspectorBehavior(ClientInspector clientInspector)
    {
        ClientInspector = clientInspector;
    }

    /// <summary>
    /// Implement to confirm that the endpoint meets some intended criteria.
    /// </summary>
    /// <param name="endpoint"><see cref="ServiceEndpoint"/></param>
    public void Validate(ServiceEndpoint endpoint)
    {
        // not calling the base implementation
    }

    /// <summary>
    /// Implement to pass data at runtime to bindings to support custom behavior.
    /// </summary>
    /// <param name="endpoint"><see cref="ServiceEndpoint"/></param>
    /// <param name="bindingParameters"><see cref="BindingParameterCollection"/></param>
    public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
    {
        // not calling the base implementation
    }

    /// <summary>
    /// Implements a modification or extension of the service across an endpoint.
    /// </summary>
    /// <param name="endponit"><see cref="ServiceEndpoint"/></param>
    /// <param name="endpointDispatcher"><see cref="EndpointDispatcher"/></param>
    public void ApplyDispatchBehavior(ServiceEndpoint endponit, EndpointDispatcher endpointDispatcher)
    {
        // not calling the base implementation
    }

    /// <summary>
    /// Implements the custom modification of the WCF client across an endpoint.
    /// </summary>
    /// <param name="endpoint"><see cref="ServiceEndpoint"/></param>
    /// <param name="clientRuntime"><see cref="ClientRuntime"/></param>
    public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
    {
        if (this.ClientInspector == null)
            throw new InvalidOperationException("Caller must supply ClientInspector.");

        clientRuntime.ClientMessageInspectors.Add(ClientInspector);
    }
}

public class ClientInspector : IClientMessageInspector
{
    /// <summary>
    /// Gets or sets the custom MessageHeader.
    /// </summary>
    public MessageHeader[] Headers
    {
        get;
        set;
    }

    /// <summary>
    /// Constructs a new ClientInspector
    /// </summary>
    /// <param name="headers"><see cref="MessageHeader"/></param>
    public ClientInspector(params MessageHeader[] headers)
    {
        Headers = headers;
    }

    /// <summary>
    /// Enables inspection or modification of a message before a request message is sent to a service.
    /// </summary>
    /// <param name="request"><see cref="Message"/></param>
    /// <param name="channel"><see cref="IClientChannel"/></param>
    /// <returns></returns>
    public object BeforeSendRequest(ref Message request, IClientChannel channel)
    {
        if (Headers != null)
        {
            for (int i = Headers.Length - 1; i >= 0; i--)
                request.Headers.Insert(0, Headers[i]);
        }

        return request;
    }

    /// <summary>
    /// Enables inspection or modification of a message after a reply message is received but 
    /// prior to passing it back to the client.
    /// </summary>
    /// <param name="reply"><see cref="Message"/></param>
    /// <param name="correlationState">object</param>
    public void AfterReceiveReply(ref Message reply, object correlationState)
    {
        // not calling the base implementation
    }
}

public class SecurityHeader : MessageHeader
{
    private readonly APIConfig config;

    /// <summary>
    /// Constructors a new SecurityHeader
    /// </summary>
    /// <param name="config"><see cref="APIConfig"/></param>
    public SecurityHeader(APIConfig config)
    {
        this.config = config;
    }

    /// <summary>
    /// Gets or sets a value that indicates whether the header must be understood, according to SOAP 1.1/1.2 specification.
    /// </summary>
    public override bool MustUnderstand
    {
        get
        {
            return true;
        }
    }

    /// <summary>
    /// Gets the name of the message header.
    /// </summary>
    public override string Name
    {
        get
        {
            return "Security";
        }
    }

    /// <summary>
    /// Gets the namespace of the message header.
    /// </summary>
    public override string Namespace
    {
        get
        {
            return "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd";
        }
    }

    protected override void OnWriteStartHeader(XmlDictionaryWriter writer, MessageVersion messageVersion)
    {
        writer.WriteStartElement("wsse", Name, Namespace);
        writer.WriteXmlnsAttribute("wsse", Namespace);
    }

    /// <summary>
    /// Called when the header content is serialized using the specified XML writer.
    /// </summary>
    /// <param name="writer"><see cref="XmlDictionaryWriter"/></param>
    /// <param name="messageVersion"><see cref="MessageVersion"/></param>
    protected override void OnWriteHeaderContents(XmlDictionaryWriter writer, MessageVersion messageVersion)
    {
        WriteHeader(writer);
    }

    /// <summary>
    /// Overwrites the default SOAP Security Header values generated by WCF with
    /// those required by the UserService which implements WSE 2.0.  This is required
    /// for interoperability between a WCF Client and a WSE 2.0 Service.
    /// </summary>
    /// <param name="writer"><see cref="XmlDictionaryWriter"/></param>
    private void WriteHeader(XmlDictionaryWriter writer)
    {
        // Create the Nonce
        byte[] nonce = GenerateNonce();

        // Create the Created Date
        string created = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ");

        // Create the WSSE Security Header, starting with the Username Element
        writer.WriteStartElement("wsse", "UsernameToken", Namespace);
        writer.WriteXmlnsAttribute("wsu", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd");
        writer.WriteStartElement("wsse", "Username", null);
        writer.WriteString(config.Username);
        writer.WriteEndElement();

        // Add the Password Element
        writer.WriteStartElement("wsse", "Password", null);
        writer.WriteAttributeString("Type", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest");
        writer.WriteString(GeneratePasswordDigest(nonce, created, config.Password));
        writer.WriteEndElement();

        // Add the Nonce Element
        writer.WriteStartElement("wsse", "Nonce", null);
        writer.WriteAttributeString("EncodingType", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary");
        writer.WriteBase64(nonce, 0, nonce.Length);
        writer.WriteEndElement();

        // Lastly, add the Created Element
        writer.WriteStartElement("wsu", "Created", null);
        writer.WriteString(created);
        writer.WriteEndElement();
        writer.WriteEndElement();
        writer.Flush();
    }

    /// <summary>
    /// Generates a random Nonce for encryption purposes
    /// </summary>
    /// <returns>byte[]</returns>
    private byte[] GenerateNonce()
    {
        RNGCryptoServiceProvider rand = new RNGCryptoServiceProvider();
        byte[] buf = new byte[0x10];
        rand.GetBytes(buf);
        return buf;
    }

    /// <summary>
    /// Generates the PasswordDigest using a SHA1 Hash
    /// </summary>
    /// <param name="nonceBytes">byte[]</param>
    /// <param name="created">string</param>
    /// <param name="password">string</param>
    /// <returns>string</returns>
    private string GeneratePasswordDigest(byte[] nonceBytes, string created, string password)
    {
        // Convert the values to be hashed to bytes
        byte[] createdBytes = Encoding.UTF8.GetBytes(created);
        byte[] passwordBytes = Encoding.UTF8.GetBytes(password);
        byte[] msgBytes = new byte[nonceBytes.Length + createdBytes.Length + passwordBytes.Length];

        // Combine the values into one byte array
        Array.Copy(nonceBytes, msgBytes, nonceBytes.Length);
        Array.Copy(createdBytes, 0, msgBytes, nonceBytes.Length, createdBytes.Length);
        Array.Copy(passwordBytes, 0, msgBytes, (nonceBytes.Length + createdBytes.Length), passwordBytes.Length);

        // Generate the hash
        SHA1CryptoServiceProvider sha1 = new SHA1CryptoServiceProvider();
        byte[] hashBytes = sha1.ComputeHash(msgBytes);
        return Convert.ToBase64String(hashBytes);
    }
}

public class APIConfig
{
    /// <summary>
    /// Gets or Sets the Password property
    /// </summary>
    public string Password
    {
        get;
        set;
    }

    /// <summary>
    /// Gets or Sets the Username property
    /// </summary>
    public string Username
    {
        get;
        set;
    }
}

With above code I'm able to create this request:

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
    <s:Header>
        <wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
            <wsse:UsernameToken xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
                <wsse:Username>Demo</wsse:Username>
                <wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">1TiCoKWfNF3EdEH3qdU4inKklaw=</wsse:Password>
                <wsse:Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">mAyz3SywR8sR9IkhDGJRIw==</wsse:Nonce>
                <wsu:Created>2017-02-09T23:29:14.371Z</wsu:Created>
            </wsse:UsernameToken>
        </wsse:Security>
    </s:Header>
    <s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
        <query xmlns="http://dom.query.api.com">
            <param>
                <userQueryId xsi:nil="true" xmlns="http://dom.query.api.com/xsd"/>
            </param>
        </query>
    </s:Body>
</s:Envelope>

As You can see I'm missing BinarySecurityToken and Signature elements in my Security element. I've tried using Microsoft.Web.Services3 but without luck.
For example constructor of BinarySecurityToken is protected.

I have my client cert imported inside my cert store. I need to sign only the body of my request.

How can I add those two elements to Security element inside Header? i know I must use Microsoft.Web.Services3 but i don't know how.

I've searched over the internet for similar questions, but all I found was tutorials on how to add username and passwords, questions about adding Signature and BinarySecurityToken remains unanswered - How to sign xml with X509 cert, add digest value and signature to xml template

Community
  • 1
  • 1
Misiu
  • 4,738
  • 21
  • 94
  • 198

1 Answers1

4

this coded binding should produce a similar message:

var b = new CustomBinding();

            var sec = (AsymmetricSecurityBindingElement)SecurityBindingElement.CreateMutualCertificateBindingElement(MessageSecurityVersion.WSSecurity10WSTrust13WSSecureConversation13WSSecurityPolicy12BasicSecurityProfile10);
            sec.EndpointSupportingTokenParameters.Signed.Add(new UserNameSecurityTokenParameters());
            sec.MessageSecurityVersion =
                MessageSecurityVersion.
                    WSSecurity10WSTrust13WSSecureConversation13WSSecurityPolicy12BasicSecurityProfile10;
            sec.IncludeTimestamp = false;
            sec.MessageProtectionOrder = System.ServiceModel.Security.MessageProtectionOrder.EncryptBeforeSign;

            b.Elements.Add(sec);
            b.Elements.Add(new TextMessageEncodingBindingElement(MessageVersion.Soap11, Encoding.UTF8));
            b.Elements.Add(new HttpsTransportBindingElement());


            var c =
                new ServiceReference1.SimpleServiceSoapClient(b, new EndpointAddress(new Uri("https://www.bankhapoalim.co.il/"), new DnsEndpointIdentity("WSE2QuickStartServer"), new AddressHeaderCollection()));

            c.ClientCredentials.UserName.UserName = "yaron";
            c.ClientCredentials.UserName.Password = "1234";

            c.ClientCredentials.ServiceCertificate.Authentication.CertificateValidationMode =
                System.ServiceModel.Security.X509CertificateValidationMode.None;
            c.ClientCredentials.ServiceCertificate.DefaultCertificate = new X509Certificate2(@"C:\Program Files\Microsoft WSE\v2.0\Samples\Sample Test Certificates\Server Public.cer");

            c.ClientCredentials.ClientCertificate.Certificate = new X509Certificate2(@"C:\Program Files\Microsoft WSE\v2.0\Samples\Sample Test Certificates\Client Private.pfx", "wse2qs");

            c.Endpoint.Contract.ProtectionLevel = System.Net.Security.ProtectionLevel.Sign;

The path you chose would require you to implement message signing by yourself which is harder.

Yaron Naveh
  • 23,560
  • 32
  • 103
  • 158
  • Thank You so much for reply. Request created by Your code looks almost fine, one thing missing is `Nonce` in UsernameToken. Should I use custom `WSSecurityTokenSerializer`? as shown here: https://weblog.west-wind.com/posts/2012/nov/24/wcf-wssecurity-and-wse-nonce-authentication. I need both `Nonce` and signed body in security header. – Misiu Feb 14 '17 at 07:39
  • not sure if that link will work, you should try. otehrwise there should be other ways to create custom username token in wcf like https://msdn.microsoft.com/en-us/library/ms731872(v=vs.110).aspx . also just maybe if you create the username yourself in a message inspector it will still wor (in this case remove it from the code config). Another option is to implement a custom encoder which adds the nonce to the username (use https://gist.github.com/yaronn/44bb89cd14c54b24240d152e64ab37bb as a base) but carefully with white space so not to invalidate the signature. – Yaron Naveh Feb 15 '17 at 06:12
  • I've tried using my `MessageInspector` but it creates two `o:Security` tags in `s:Header`, one that contains `o:UsernameToken` with `Nonce` and second without it, but with `BinarySecurityToken` and `Signature` – Misiu Feb 15 '17 at 07:57
  • `WSSecurityTokenSerializer` looks promising(it serializes `UsernameToken` in way I want),but I noticed some differences in BinarySecurityToken. Using Your code I have only `ValueType` attribute with value `http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3` but in sample request I got from service owner this attribute should have value `http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509PKIPathv1` and also `EncodingType` with value `http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary` – Misiu Feb 15 '17 at 08:53
  • Also in `SignedInfo` I can see that there are two references - one to `Body` and second to `UsernameToken`. Can I disable `UsernameToken` signing? I only need `Body` signed – Misiu Feb 15 '17 at 08:58
  • I think for that you would need to switch to another approach, e.g. add the token vai custom encoder. most servers would not care to get an extra signed token though so not sure you need. also maybe this approache here but its hacky http://stackoverflow.com/questions/10393541/how-can-i-control-which-elements-are-signed-in-a-wcf-soap-request – Yaron Naveh Feb 15 '17 at 11:16
  • Thanks for suggestion. I've tried creating custom `IClientMessageInspector` (again) but I must do everything manually. I'll try custom Encoder. I must create custom `MessageEncoderFactory ` and replace `TextMessageEncodingBindingElement` from Your code? – Misiu Feb 15 '17 at 18:53
  • this is an example for a custom encoder https://gist.github.com/yaronn/44bb89cd14c54b24240d152e64ab37bb . your encoder should just push the username token into the header. be carefull not to change anything else (including whitespace) since some parts of the xml are already signed. use preserveWhiteSpace etc. there is always manuall work in such approach, iits unavoidable. but I would try to send the message with the signed token, the server might get it. if it does not maybe it has a different issue. – Yaron Naveh Feb 16 '17 at 02:39
  • 1
    Thank You for help with this. I'm still have problems but I'm much closer thanks to Your answer. I'll check certificates and all thing I can. If it won't work I'll post my code in separate question. Thanks again! – Misiu Feb 21 '17 at 06:56
  • I've posted same question on MSDN - https://social.msdn.microsoft.com/Forums/vstudio/en-US/6d071462-d37e-4653-9d70-cf67b780700d/wcf-c-client-java-backend-with-cxf-241-unable-to-generate-correct-request-with?forum=wcf#b84b4aa4-7918-4c0b-8ccf-c950cfffc348 could You please take a look? – Misiu Mar 01 '17 at 16:08
  • I got some things working. I found https://github.com/dotnet/wcf/blob/master/src/System.Private.ServiceModel/tools/IISHostedWcfService/App_code/testhosts/CustomTextMessageBindingElement.cs and tried that. but unfortunately now I get this error from server: `Transport level information does not match with SOAP Message namespace URI`. Inside trace I saw that with custom message encoder `...` is added at start of my message – Misiu Mar 02 '17 at 09:30
  • maybe the SOAPAction HTTP header has a different value than the WS-Addressing SOAPAction element inside the soap. Can you get this working in SoapUI? then you can compare differences – Yaron Naveh Mar 02 '17 at 13:07
  • Sorry for such late reply. I waited for service owner response. I managed to get this working in SoapUI. I have one big difference. In SoapUI I load cert from pfx file, but in C# I load it from cert store. I've tried preventing whitespace's in XMLDocument when adding security header, but nothing helped. In SoapUI I must specify cert alias, do I must do same thing in C#? – Misiu Mar 20 '17 at 15:08
  • the cert source is just an api, as long as c# loads it its fine. you should look at xml level differences between soapui and c#. – Yaron Naveh Mar 20 '17 at 21:17
  • Hi again, I've created CustomTextMessageEncoder and I've created new solution based on Your code, but all the time I get error `org.apache.ws.security.WSSecurityException: The signature or decryption was invalid`. Inside SoapUI I've noticed that `BinarySecurityToken` is send with ValueType=`X509PKIPathv1`. Unfortunately in .NET this is not supported and I'm using `X509v3`. Right now I need `MessageEncoder` to add two attributes that are required by service. Problem is that when I modify my message in `WriteMessage` then it is serialized differently and signature is invalid. – Misiu Mar 24 '17 at 13:28
  • I've put my code at: https://gist.github.com/Misiu/e84a6adfd2891265f7ecb88bc6201130 One thing that I find weird it that in `WriteMessage` I log old message and new message after adding attributes. First one doesn't have security header. It is visible only in second message. – Misiu Mar 24 '17 at 13:40
  • I've posted another question related to altering message in MessageEncoder: http://stackoverflow.com/questions/43002290/custom-messageencoder-add-attributes-to-message-without-modifying-structure could You please take a look at it? – Misiu Mar 24 '17 at 14:38
  • How have you handled the signature issue? – mskuratowski May 07 '20 at 06:33