I made a console application that hosts a WCF service. I am now trying to modify it. Here is what currently works...
Program.cs:
namespace FileRetrievalPoC
{
class Program
{
static void Main(string[] args)
{
var address = "https://localhost:8000/FileRetrievalPoC";
Console.WriteLine("Starting a service at {0}...", address);
FileRetrievalService.Start(address, StoreLocation.LocalMachine, StoreName.TrustedPeople, "b7ba8f6e4c33ba55053f2e85898b383e40bf8ac9");
Console.WriteLine("Service started.");
Console.WriteLine("Press Enter to create a new proxy client and call the Get method.");
Console.WriteLine("Press Escape to end the application.");
while (true)
{
var key = Console.ReadKey();
if (key.Key == ConsoleKey.Enter)
{
var proxy = FileRetrievalService.Connect(address, "localhost", "username", "password");
Console.WriteLine("Read the following from the server: {0}", proxy.Get(@"C:\Users\User\Desktop\Document.txt"));
FileRetrievalService.CloseClients();
}
else if (key.Key == ConsoleKey.Escape)
break;
}
FileRetrievalService.CloseServer();
}
}
}
FileRetrievalService.cs:
class FileRetrievalService : IFileRetrieval
{
private static BasicHttpsBinding _binding = new BasicHttpsBinding()
{
HostNameComparisonMode = HostNameComparisonMode.Exact,
Security = new BasicHttpsSecurity()
{
Message = new BasicHttpMessageSecurity()
{
AlgorithmSuite = SecurityAlgorithmSuite.Basic256Sha256Rsa15,
ClientCredentialType = BasicHttpMessageCredentialType.UserName
},
Mode = BasicHttpsSecurityMode.TransportWithMessageCredential,
Transport = new HttpTransportSecurity()
{
ClientCredentialType = HttpClientCredentialType.Windows,
}
}
};
private static ChannelFactory<IFileRetrieval> _channelFactory;
private static ServiceHost _host;
public static void Start(string address, StoreLocation location, StoreName name, string thumbprint)
{
_host = new ServiceHost(typeof(FileRetrievalService));
_host.Credentials.ServiceCertificate.SetCertificate(location, name, X509FindType.FindByThumbprint, thumbprint);
_host.AddServiceEndpoint(typeof(IFileRetrieval), _binding, address);
_host.Open();
}
public static void CloseClients()
{
if (_channelFactory != null)
_channelFactory.Close();
_channelFactory = null;
}
public static void CloseServer()
{
if (_host != null)
_host.Close();
_host = null;
}
public static void CloseServerAndClients()
{
CloseServer();
CloseClients();
}
public static IFileRetrieval Connect(string address, string domain, string username, string password)
{
if (_channelFactory == null)
{
_channelFactory = new ChannelFactory<IFileRetrieval>(_binding, address);
_channelFactory.Credentials.UserName.UserName = domain + '\\' + username;
_channelFactory.Credentials.UserName.Password = password;
_channelFactory.Credentials.Windows.ClientCredential = new NetworkCredential(username, password, domain);
}
return _channelFactory.CreateChannel();
}
public string Get(string path)
{
return File.ReadAllText(path);
}
public void Set(string path, string contents)
{
File.WriteAllText(path, contents);
}
}
IFileRetrieval.cs:
[ServiceContract]
public interface IFileRetrieval
{
[OperationContract]
string Get(string path);
[OperationContract]
void Set(string path, string contents);
}
All the above code works, because:
- Server-side has
ClientCredentialType = BasicHttpMessageCredentialType.UserName
on the binding. - Client-side sets the username credentials on the
_channelFactory.Credentials.UserName
object.
I want to add more security, and have the client-side also provide an SSL certificate when connecting to the server. In order to try to do this, I have made the following replacements in the code for points 1 and 2 with the following logic:
- I am trying to use
ClientCredentialType = BasicHttpMessageCredentialType.Certificate
on the binding instead ofClientCredentialType = BasicHttpMessageCredentialType.UserName
. - In the
Connect
method, I am now setting the client's certificate (_channelFactory.Credentials.ClientCertificate.SetCertificate(location, name, X509FindType.FindByThumbprint, thumbprint);
) and modified the method to take the appropriate certificate information (StoreLocation location, StoreName name, string thumbprint
) passing in exactly the same parameters to reference, from the client-side, the same certificate that is presented from the server-side. I have removed the previous logic that sets the_channelFactory.Credentials.UserName
object.
This did not work, and I get the following exception:
An unsecured or incorrectly secured fault was received from the other party. See the inner FaultException for the fault code and detail.
Its inner exception says:
An error occurred when verifying security for the message.
Where does one go from here? I am not using IIS, so how can I trace this error further? I am at a bit of a loss. I was hoping someone could explain what might be wrong, and what I can do to fix the error or debug it further.
Edit: I figured out a way to debug it further, but its not much more helpful than I thought it would be. Inside of my App.config
, I added the following tracing:
<system.diagnostics>
<sources>
<source name="System.ServiceModel" switchValue="Verbose, ActivityTracing" propagateActivity="true">
<listeners>
<add name="listener" type="System.Diagnostics.XmlWriterTraceListener" initializeData= "C:\Users\YourAccountUsername\Desktop\WCFTraces.svclog" />
</listeners>
</source>
<source name="System.ServiceModel.MessageLogging" switchValue="Verbose, ActivityTracing" propagateActivity="true">
<listeners>
<add name="listener" type="System.Diagnostics.XmlWriterTraceListener" initializeData= "C:\Users\YourAccountUsername\Desktop\WCFTraces.svclog" />
</listeners>
</source>
</sources>
</system.diagnostics>
Now inside of WCFTraces.svclog
, I see a bunch of exceptions from the server side as well. The first one being:
Security processor was unable to find a security header in the message. This might be because the message is an unsecured fault or because there is a binding mismatch between the communicating parties. This can occur if the service is configured for security and the client is not using security.
I'm not sure why the message does not have a security header, considering I've specified the client certificate. What is the client code missing?
Edit: Ah, I also see this error:
The key size requirements for the 'Basic256Sha256Rsa15' algorithm suite are not met by the 'System.IdentityModel.Tokens.X509SecurityToken' token which has key size of '8192'.
Its under received bytes. My certificate has an 8192-byte private and public key, but this must not be a good match for my selected AlgorithmSuite = SecurityAlgorithmSuite.Basic256Sha256Rsa15
. Does anyone know what I should do here to allow this certificate? I can try using a 256 bit key for this...or is the problem that the signature algorithm is 512 bits on my certificate?