Search code examples
c#restwcfself-hostingwebhttpbinding

SSL-protected RESTFul WCF Self-hosting service


Summary

I am trying to implement an SSL-protected RESTFul WCF service, but following error occurred and communication failed.

making the HTTP request to ‘https://123.123.123.123:5000/TestService/PostMsg’. This could be due to the fact that the server certificate is not configured properly with HTTP.SYS in the HTTPS case. This could also be caused by a mismatch of the security binding between the client and the server. on some customers machines.

Can anyone help me? Best regards

Tried things :

I have succeeded to communicate without SSL-protection.

Code

Service Implementation

using System;
using System.Security.Cryptography.X509Certificates;
using System.ServiceModel;
using System.ServiceModel.Description;
using System.ServiceModel.Security;
using System.ServiceModel.Web;

namespace TestService
{
    class Program
    {
        static WebServiceHost host;

        static void Main()
        {
            WebHttpBinding binding = new WebHttpBinding();
            binding.Security.Mode = WebHttpSecurityMode.Transport;
            binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Certificate;
            binding.Security.Transport.ProxyCredentialType = HttpProxyCredentialType.None;

            Uri uri = new Uri("https://localhost:5000/TestService");

            host = new WebServiceHost(typeof(TestService));
            ServiceEndpoint se = host.AddServiceEndpoint(typeof(ITestService), binding, uri);

            var behavior = new WebHttpBehavior();
            behavior.FaultExceptionEnabled = false;
            behavior.HelpEnabled = true;
            behavior.DefaultOutgoingRequestFormat = WebMessageFormat.Json;
            behavior.DefaultOutgoingResponseFormat = WebMessageFormat.Json;
            se.EndpointBehaviors.Add(behavior);

            ServiceDebugBehavior debug = host.Description.Behaviors.Find<ServiceDebugBehavior>();
            debug.IncludeExceptionDetailInFaults = true;

            ServiceMetadataBehavior metad = new ServiceMetadataBehavior();
            metad.HttpGetEnabled = true;
            metad.HttpsGetEnabled = true;
            host.Description.Behaviors.Add(metad);

            var certificate = new X509Certificate2(@"D:\Work\TestService\ServerCert1.pfx", "paswd", X509KeyStorageFlags.UserKeySet);
            host.Credentials.ServiceCertificate.Certificate = certificate;
            host.Credentials.ClientCertificate.Authentication.CertificateValidationMode = X509CertificateValidationMode.None;

            host.Open();
            Console.WriteLine(string.Format(null, "URL : {0}", uri.ToString()));
            Console.WriteLine("Press <ENTER> to terminate");
            Console.ReadLine();
            host.Close();

        }
    }
}

Interface

using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Web;

namespace TestService
{
    [ServiceContract]
    interface ITestService
    {
        [OperationContract]
        [WebInvoke(Method = "POST"
                            , RequestFormat = WebMessageFormat.Json
                            , UriTemplate = "/PostMsg"
            )]
        MessageData PostMsg(MessageData msg);
    }

    [DataContract]
    public class MessageData
    {
        [DataMember]
        public string Name { get; set; }

        [DataMember]
        public int Gender { get; set; }

        [DataMember]
        public int Age { get; set; }

    }

}
using System;

namespace TestService
{
    class TestService : ITestService
    {
        public MessageData PostMsg(MessageData msg)
        {
            Console.WriteLine(string.Format(null, "Recieved Name: {0}, Gender:{1}, Age:{2}", msg.Name , msg.Gender , msg.Age));

            return new MessageData()
            {
                Name = msg.Name,
                Gender = msg.Gender,
                Age = msg.Age + 1
            };
        }

    }
}

Client Implementation

using System;
using System.Windows.Forms;
using System.Security.Cryptography.X509Certificates;
using System.ServiceModel;
using System.ServiceModel.Description;
using System.ServiceModel.Security;
using System.ServiceModel.Web;
using TestService;

namespace TestClient
{
    public partial class Form1 : Form
    {
        WebChannelFactory<ITestService> cf = null;
        ITestService channel = null;

        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            listBox1.HorizontalScrollbar = true;

            Uri uri = new Uri("https://123.123.123.123:5000/TestService");
            EndpointAddress endpointAddress = new EndpointAddress(uri);

            cf = new WebChannelFactory<ITestService>(uri);
            WebHttpBinding binding = cf.Endpoint.Binding as WebHttpBinding;
            binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Certificate;
            binding.Security.Mode = WebHttpSecurityMode.Transport;

            var behavior = new WebHttpBehavior();
            behavior.FaultExceptionEnabled = false;
            behavior.HelpEnabled = true;
            behavior.DefaultOutgoingRequestFormat = WebMessageFormat.Json;
            behavior.DefaultOutgoingResponseFormat = WebMessageFormat.Json;
            cf.Endpoint.Behaviors.Add(behavior);

            var clientCertificate = new X509Certificate2(@"D:\Work\TestService\ServerCert1.pfx", "pswd", X509KeyStorageFlags.UserKeySet);
            cf.Credentials.ClientCertificate.Certificate = clientCertificate;
            cf.Credentials.ServiceCertificate.Authentication.CertificateValidationMode = X509CertificateValidationMode.None;

            channel = cf.CreateChannel();

        }

        private void button1_Click(object sender, EventArgs e)
        {
            MessageData msg = new MessageData()
            {
                Name = "Taro",
                Gender = 1,
                Age = 3
            };
            MessageData rtn = channel.PostMsg(msg);
            listBox1.Items.Insert(0, string.Format("Name:{0}, Gender:{1}, Age{2} ", rtn.Name, rtn.Gender, rtn.Age));

        }
    }
}

Script for creating pfx file.

@echo ----------------------------------------------
@echo     Script for creating self certificate
@echo ----------------------------------------------
@set "TOOL_DIR=E:\Windows Kits\10\bin\10.0.18362.0\x86"
@if not exist "%TOOL_DIR%" (
    @echo Tools do not exists. %TOOL_DIR%
    @goto ERROR_EXIT
)
@set "PATH=%TOOL_DIR%;%PATH%"

@set "WORK_DIR=D:\Work\TestService"
@if not exist %WORK_DIR% ( 
    @echo Work folder does not exist. %WORK_DIR%
    @goto ERROR_EXIT
)
cd  /d %WORK_DIR%

@openfiles > NUL 2>&1 
@if NOT %ERRORLEVEL% EQU 0 (
    @echo It is not being executed as an administor.
    goto ERROR_EXIT
)

@SET /P ANS="Create a certificate file. Are you sure (Y / N)?"
@if /i %ANS% NEQ y if /i %ANS% NEQ Y goto ERROR_EXIT

del %WORK_DIR%\*.*
@echo;

@echo Create Self-Signed Certificate
makecert -n "CN=ServerCN1" -a sha1 -eku 1.3.6.1.5.5.7.3.3 -r -sv ServerCert1.pvk ServerCert1.cer -cy authority -b 11/06/2019 -e 12/31/2019
@echo;

@echo Create Software Publisher Certificate File
cert2spc ServerCert1.cer ServerCert1.spc
@echo;

@echo Create Personal Information Exchange File
pvk2pfx -pvk ServerCert1.pvk -spc ServerCert1.spc -po pswd -pfx ServerCert1.pfx -f
@echo;

:ERROR_EXIT
@SET /P ANS="Finished."

Requisite

  • I don't want to use IIS because I think there are a lot of settings with it. Instead, I am using X509Certificate2 method of X509Certificate2 class in "System.Security.Cryptography.X509Certificates" Name space. (.NETFramework\v4.7.2\System.dll)

  • Server and client are on the same computer at this moment.

  • Ports of Windows defender firewall are opened.

Some Images for understanding what I am doing.

https://i.sstatic.net/MCFl9.png

https://i.sstatic.net/DgNid.png


Solution

  • Using a certificate to secure the communication requires that we bind the certificate to the particular computer port with the below command.

    Netsh http add sslcert ipport=0.0.0.0:5000 certhash= 0102030405060708090A0B0C0D0E0F1011121314 appid= appid={00112233-4455-6677-8899-AABBCCDDEEFF}

    https://learn.microsoft.com/en-us/windows/win32/http/add-sslcert
    https://learn.microsoft.com/en-us/dotnet/framework/wcf/feature-details/how-to-configure-a-port-with-an-ssl-certificate

    This feature is auto-completed in IIS when we set up the site binding. enter image description here

      WebHttpBinding binding = new WebHttpBinding();
                binding.Security.Mode = WebHttpSecurityMode.Transport;
                binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Cert
    

    About authenticating the client with a certificate, please refer to the below official document. https://learn.microsoft.com/en-us/dotnet/framework/wcf/feature-details/transport-security-with-certificate-authentication
    There are usually two points we need to pay attention to.

      1. We should ensure that both the service certificate and the client certificate should have client authentication and server authentication purpose.

    Server Authentication (1.3.6.1.5.5.7.3.1)
    Client Authentication (1.3.6.1.5.5.7.3.2)

      2. We had better build the project over the Dotnet framwork4.6.2 since there is something wrong with reading the private key of the certificate created by using PowerShell certificate.

    Feel free to let me know if there is anything I can help with.