jscheppers
jscheppers

Reputation: 233

WCF uniquely identify clients with certificates issue

I've got this WCF webservice which provides a pretty basic lookup-service (so no data is changed on the server-side). The information provided by this service is confidential so I have to take the approriate security measures.

The standard things to do is providing transport and message security by demanding a valid client certificate in the service binding. I create and issue these certificates so I control who can use this service. But (according to me, please prove me wrong here) nothing prevents the clients from exchanging their client certificates with other clients (whom I have not given access to my service). As long as the provided client certificate is valid, the service accepts the connection. An added username/password mechanism does nothing to change this, as these credentials can also be exchanged.

You may say that the client is legally obligated to keep the certificate to himself, but that does not provide enough security for me. Is there a way to specifically control who connects with my service, without having to resort to IP-based access control (as my clients use DHCP issued IP addresses, there is no way to couple the IP to a client/certificate).

My initial quess is to somehow use a unique identifier generated by the client who installs the client certificate (like a GUID) in its certificate store and register this on the server-side so that only the combination of the certificate subject and that unique identifier may access the service. But I have no idea if there is a mechanism to do that.

Edit: since I have no control over the clients I cannot solve this problem easily by altering the code on the client side.

I'm really grateful for any pointers/answers!

Upvotes: 0

Views: 774

Answers (1)

Dejan Janjušević
Dejan Janjušević

Reputation: 3230

I use the IDispatchMessageInspector for this. While not being sure whether this is a best-performant approach, it has proven to be working just fine.

My clients use the message header to send their unique GUID. Each GUID corresponds to each client certificate and these are stored in the Windows Registry. GUID is generated by the client (I do it using UuidCreateSequential())

I will share my solution with you. This is my service code:

    public object AfterReceiveRequest(ref System.ServiceModel.Channels.Message request, System.ServiceModel.IClientChannel channel, System.ServiceModel.InstanceContext instanceContext)
    {
        IList<IIdentity> identities = OperationContext.Current.ServiceSecurityContext.AuthorizationContext.Properties["Identities"] as IList<IIdentity>;
        string clientGuid = request.Headers.GetHeader<string>("Guid", "MyNamespace");
        if (clientGuid == null)
            throw new FaultException("Client GUID not sent!");
        Guid testGuid = Guid.Empty;

        if (!Guid.TryParse(clientGuid, out testGuid))
        {
            throw new FaultException(string.Format("The format of the GUID '{0}' is not valid!", clientGuid));
        }

        IIdentity X509Identity = identities.Where(x => x.AuthenticationType == "X509").SingleOrDefault();
        RegistryKey identityKey = Registry.CurrentUser.CreateSubKey("SOFTWARE\\MySoftware\\Identities");
        string storedSubjectName = (string)identityKey.GetValue(clientGuid);

        if (storedSubjectName == null)
        {
            string[] valueNames = identityKey.GetValueNames();
            for (int idx = 0; idx < valueNames.Count(); idx++)
            {
                string testCN = (string)identityKey.GetValue(valueNames[idx]);
                if (testCN == X509Identity.Name)
                {
                    throw new FaultException(string.Format("Certificate '{0}' has already been registered!", X509Identity.Name));
                }
            }
            identityKey.SetValue(clientGuid, X509Identity.Name, RegistryValueKind.String);
        }
        else
        {
            if (storedSubjectName != X509Identity.Name)
                throw new FaultException(string.Format("Certificate '{0}' used by the user registered with the certificate '{0}'", X509Identity.Name, storedSubjectName));
        }

        identityKey.Close();

        return null;
    }

The code on the client:
On app startup

        RegistryKey guidKey = Registry.CurrentUser.CreateSubKey("SOFTWARE\\MySoftware\\Settings");
        string clientGuid = (string)guidKey.GetValue("Guid");
        if (clientGuid == null)
        {
            clientGuid = UUID.mGetNewGUID().ToString();
            guidKey.SetValue("Guid", clientGuid, RegistryValueKind.String);
        }

A helper class to get the unique UUID thanks to this article

using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Runtime.InteropServices;

static class UUID
{

    // ==========================================================
    // GUID Creation
    // ==========================================================
    private const int RPC_S_OK = 0;
    private const int RPC_S_OUT_OF_MEMORY = 14;
    private const int RPC_S_UUID_NO_ADDRESS = 1739;

    private const int RPC_S_UUID_LOCAL_ONLY = 1824;
    [DllImport("rpcrt4.dll", SetLastError = true)]
    public static extern int UuidCreateSequential(ref Guid guid);

    [DllImport("rpcrt4.dll", SetLastError = true)]
    public static extern int UuidCreate(ref Guid guid);

    /// <summary>
    /// Creates the machine GUID.
    /// </summary>
    public static Guid mGetNewGUID()
    {
        Guid guidMachineGUID = default(Guid);
        int intReturnValue = UuidCreateSequential(ref guidMachineGUID);
        switch (intReturnValue)
        {
            case RPC_S_OK:
                return guidMachineGUID;

            case RPC_S_OUT_OF_MEMORY:
                throw new Exception("UuidCreate returned RPC_S_OUT_OF_MEMORY");
            case RPC_S_UUID_NO_ADDRESS:
                throw new Exception("UuidCreate returned RPC_S_UUID_NO_ADDRESS");
            case RPC_S_UUID_LOCAL_ONLY:
                throw new Exception("UuidCreate returned RPC_S_UUID_LOCAL_ONLY");
            default:
                throw new Exception("UuidCreate returned an unexpected value = " + intReturnValue.ToString());
        }
    }

}

In BeforeSendRequest of IClientMessageInspector

        var guidHeader = new MessageHeader<string>(Service.ClientGuid);
        var untypedGuid = guidHeader.GetUntypedHeader("Guid", "MyNamespace");
        request.Headers.Add(untypedGuid);

Upvotes: 2

Related Questions