Delen via


Selfhosting a WCF service over Https

 

Something as simple as ad-hoc selfhosting a WCF service can be fairly difficult if you need to use https.  This is because of all the underlying pieces than need to be configured to properly host on a secure transport.  In this fairly lengthy posting, I will go through a walkthrough of creating a simple selfhosted HelloWorld WCF service, entirely by hand, and modify it to listen over https.  Along the way, I will run into several common errors and give the solutions to these.

 

Disclaimer: By no means is this intended for production.  The purpose of this is to give an example of test code which self-hosts a WCF service using https.

 

  • Write the simple WCF service you want to host. The following are the steps I took for this:
    • Create a new Console Application in Visual Studio.
    • Add a reference to System.ServiceModel.
    • Write a service contract and a service class which implements it.
    • In Main, create an address, binding, and serviceHost. Add the endpoint to the host and start the host.
    • So, the finished project looks like this:

 

using System;

using System.ServiceModel;

 

namespace HttpsSelfhost

{

    class Program

    {

        static void Main(string[] args)

        {

            string address = "https://localhost/HelloWorldService";

            BasicHttpBinding binding = new BasicHttpBinding();

           

            ServiceHost host = new ServiceHost(typeof(HelloWorldService));

            host.AddServiceEndpoint(typeof(IHelloWorld), binding, address);

            host.Open();

           

            Console.WriteLine("Host is {0}. Press enter to close.", host.State);

            Console.ReadLine();

            host.Close();

        }

    }

 

    [ServiceContract]

    public interface IHelloWorld

    {

        [OperationContract]

        void HelloWorld();

    }

 

    public class HelloWorldService : IHelloWorld

    {

        public void HelloWorld()

        {

            Console.WriteLine("Hello World!");

        }

    }

}

 

  • Now, I want a client to test it.
    • I can create a new console app for this. In short, it looks like this:

 

using System;

using System.ServiceModel;

 

namespace HelloWorldClient

{

    class Program

    {

        static void Main(string[] args)

        {

            Console.WriteLine("Press enter when the service is opened.");

            Console.ReadLine();

 

            string address = "https://localhost/HelloWorldService";

            BasicHttpBinding binding = new BasicHttpBinding();

 

            ChannelFactory<IHelloWorld> factory = new ChannelFactory<IHelloWorld>(binding, address);

            IHelloWorld client = factory.CreateChannel();

            Console.WriteLine("Invoking HelloWorld on the service.");

            client.HelloWorld();

            Console.WriteLine("Press enter to quit.");

            Console.ReadLine();

        }

    }

 

    [ServiceContract]

    public interface IHelloWorld

    {

        [OperationContract]

        void HelloWorld();

    }

}

 

  • In Visual Studio, under the Solution properties, I set the Startup Type to multiple and have both the service and client projects start. Hitting f5 brings me to the first error:
  • The reason for this error is that I am running VS as a non-admin. Also, I'm planning on hosting over https, so I'll want a different port anyway. So, I modify the address, for both the client and service, to be:

string address = "https://localhost:1234/HelloWorldService";

Then restart VS as an admin.  When I hit F5, I can start the client once the service indicates it's open. 

  • The advantage of this is that it's pretty well decoupled from the service. The disadvantage is that any time I change my service address, binding, or contract, I need to fix it on my client as well. But for quick ad-hoc testing, this is a great place to start. Now, I want to modify it to use https.

 

  • Now I want to start modifying this for https. I still want to use port 1234, but I want secure transport.
    • Blindly doing this, I simply change the address to use https instead of http:

 

string address = "https://localhost:1234/HelloWorldService";

 

This yields the following exception:

  • ArgumentException: The provided URI scheme 'https' is invalid; expected 'http'.

Parameter name: context.ListenUriBaseAddress

  • To fix this, we need to add the following to the binding:

 

binding.Security.Mode = BasicHttpSecurityMode.Transport;

 

  • Now I'm much closer: the ServiceHost actually opens. But when I try to invoke it with the client, I get the following:
    • CommunicationException: An error occurred while making the HTTP request to https://localhost:1234/HelloWorldService. 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.
  • That means I have to configure the port with an SSL certificate. I want a self-signed certificate for this. There are details about this here: https://msdn.microsoft.com/en-us/library/ms733791.aspx, but I'm going to give a quick and dirty summary of what needs to be done. It probably differs a bit from that article. Remember, this isn't intended for a production environment...
    • First, use netsh to add a namespace reservation for the port:

 

netsh http add urlacl url=https://+:1234/ user=EVERYONE

 

  • But that's not enough, we need a self-signed cert to bind to that port. Otherwise, you'll get the same exception as above. Create the cert using makecert.exe, which tends to reside in the windows sdk folder. Something like \Program Files\Microsoft SDKs\Windows\v7.0A\Bin. First, create the root certificate to use for signing the server certificate. <machineName> can be localhost, which is what I'm using.

 

makecert.exe -sk RootCA -sky signature -pe -n CN=<machineName> -r -sr LocalMachine -ss Root MyCA.cer

 

  • This creates a cert named MyCA.cer. You should see that file in the working directory from where you ran makecert.exe.
  • Next, create the server certificate. <certificate path> should be something to use to identify the cert. I'll use MyAdHocTestCert.cer.

 

makecert.exe -sk server -sky exchange -pe -n CN=<machineName> -ir LocalMachine -is Root -ic MyCA.cer -sr LocalMachine -ss My <certificate path>

 

  • Now, you have to bind the cert to the port. This gets tricky because if you execute it from command-line, you have to enter the certificate's thumbprint. Or, you can execute the command via code if you know the certificate path, which was MyAdHocTestCert.cer. To quickly execute this process via code, I'm using the following:

 

using System;

using System.Diagnostics;

using System.IO;

using System.Security.Cryptography.X509Certificates;

 

namespace BindCertToPort

{

    class Program

    {

        static void Main(string[] args)

        {

            int port = 1234;

            string certPath = "MyAdHocTestCert.cer";

            X509Certificate2 certificate = new X509Certificate2(certPath);

 

            // netsh http add sslcert ipport=0.0.0.0:<port> certhash={<thumbprint>} appid={<some GUID>}

            Process bindPortToCertificate = new Process();

            bindPortToCertificate.StartInfo.FileName = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.SystemX86), "netsh.exe");

            bindPortToCertificate.StartInfo.Arguments = string.Format("http add sslcert ipport=0.0.0.0:{0} certhash={1} appid={{{2}}}", port, certificate.Thumbprint, Guid.NewGuid());

            bindPortToCertificate.Start();

            bindPortToCertificate.WaitForExit();

        }

    }

}

 

  • Note that the above code expects that the actual MyAdHocTestCert.cer file is in the same location as the BindCertToPort executable.
  • And that's it! At this point, I can start the service and invoke the HelloWorld() operation over https.
  • Tips
    • To see the port reservations, use
      • netsh http show urlacl.
    • To delete the port reservation for port 1234, use
      • netsh http delete urlacl url=https://+:1234/

Note: If the certificate is still bound to the port, you still should be able to invoke the service over https.

  • To delete the certificate - port binding thingy, use
    • netsh http delete sslcert ipport=0.0.0.0:<port>
  • You can look at certificates using the certificates snapin in mmc. I use this tool for removing certs I set up. You can also delete a certificate via code, something like this:

 

X509Certificate2 cert = new X509Certificate2(certificatePath);

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

store.Open(OpenFlags.ReadWrite);

store.Remove(cert);

store.Close();

 

I want to reiterate that none of the above examples are intended for production; I'm only presenting them to provide one example of how to manually set up a self-hosted WCF service which uses https.

Comments

  • Anonymous
    December 10, 2010
    Instead of makecert, you could use SelfSignedCertificate class from NetFx: www.maciejaniserowicz.com/.../WCF-Auth-starter-zalazek-aplikacji-klient-serwer-z-uwierzytelnianiem-usernamepassword.aspx The comment from "26 lipca 2010 17:25"

  • Anonymous
    November 15, 2011
    Did not work until I defined a binding in IIS to use https over the (localhost) certificate created according to your example ... is this a must???

  • Anonymous
    November 15, 2011
    Another problem I've faced, only localhost worked for me, any other name did not work. any suggestions ( for sure from another computer it did not work at any cost!!

  • Anonymous
    November 20, 2011
    Ok, Great ... the secret is that any certificate will not be considered if it had "Issued to" field not matching the server name or domain name ... Excellent work :)

  • Anonymous
    August 12, 2012
    Better, use UWS - a free redistributable web server for ASP.NET that has full certificate management and can host WCF apps just fine. ultidev.com/.../UWS-Cassini-Pro

  • Anonymous
    May 06, 2013
    s

  • Anonymous
    May 06, 2013
    Exception Occurs on ClientSide "Could not establish trust relationship for the SSL/TLS secure channel with authority 'localhost:8086" Inner Exception The underlying connection was closed: Could not establish trust relationship for the SSL/TLS secure channel.

  • Anonymous
    April 05, 2014
    Awesome. It worked, but the code failed. becz the string certPath = "MyAdHocTestCert.cer"; this is something was throwing error, so I opened the certificate which was available under Persona->Certitficates->open the certificate look for thumbprint values and copy and paste with out spaces in it' . It got registered successfully. Thanks a ton. Good Post

  • Anonymous
    October 28, 2014
    Exception Occurs on ClientSide "Could not establish trust relationship for the SSL/TLS secure channel with authority 'localhost:8086" Inner Exception The underlying connection was closed: Could not establish trust relationship for the SSL/TLS secure channel.

  • Anonymous
    October 28, 2014
    To check if the certificate is bound to the port. Use netsh http show sslcert ipport=0.0.0.0:port Mine was not bound when i ran the above. Finally worked. :) Thank you

  • Anonymous
    May 21, 2015
    Client app is getting below exception: "Could not establish trust relationship for the SSL/TLS secure channel with authority 'localhost:8086" Inner Exception The underlying connection was closed: Could not establish trust relationship for the SSL/TLS secure channel." How did you fix it? I've isntalled MyCA.cer on the client PC, is that right?

  • Anonymous
    July 30, 2015
    Could not establish trust relationship for the SSL/TLS secure channel with authority 'localhost:8002