I’m trying to setup a demo of Mutual SSL between a self-hosted WCF service and a client app (command prompt for now). In the end I’m trying to get to a solution where I have transport security (not message security) between a server that uses a certificate for its incoming connections and multiple clients each with individual certs that I can use to uniquely identify each of the clients.
I’ve tried a number of differing approaches to this, but none have worked (I was unable to find an exact example for what I’ve been trying to do). Each time I think I’m getting close I end up with an exception in the client when I try to invoke the service. The most common exception I’ve run into is:
“The HTTP request was forbidden with client authentication scheme 'Anonymous'.”
Inner exception: "The remote server returned an error: (403) Forbidden."
Does anyone have any thoughts on what I might have done wrong or perhaps a better walk through of how to setup mutual SSL in the above scenario?
Full disclosure - as of right now I am running both the client and server on the same computer. Not sure if that matters.
Config snippets below
The service and client code is relatively trivial so I’m pretty confident that I’ve gotten them to work. The app configs (specifically the bindings and behaviors) and certs are “more interesting” so I’m not as confident there.
How I created the certs (actual commands verbatim)
makecert -pe -n "CN=SelfSignedCA" -ss Root -sr LocalMachine -a sha1 -sky signature -r -sv "SelfSignedCA.cer" "SelfSignedCA.pvk"
makecert -pe -n "CN=system" -ss my -sr LocalMachine -a sha1 -sky exchange -eku 1.3.6.1.5.5.7.3.1 -in "SelfSignedCA" -is Root -ir LocalMachine -sp "Microsoft RSA SChannel Cryptographic Provider" -sy 12 Service.cer
makecert -pe -n "CN=client1" -ss my -sr LocalMachine -a sha1 -sky exchange -eku 1.3.6.1.5.5.7.3.1 -in "SelfSignedCA" -is Root -ir LocalMachine -sp "Microsoft RSA SChannel Cryptographic Provider" -sy 12 Client1.cer
Associating the Cert with the port (actual commands verbatim)
netsh http add urlacl url=https://+:44355/MyService/ user=EVERYONE
Server Settings
Bindings:
<wsHttpBinding>
<binding name="CustomBinding">
<security mode="Transport">
<transport clientCredentialType="Certificate"/>
</security>
</binding>
</wsHttpBinding>
Behaviors:
<serviceBehaviors>
<behavior name="">
<!--
<serviceCredentials>
<serviceCertificate
findValue="system"
storeLocation="LocalMachine"
storeName="My"
x509FindType="FindBySubjectName"/>
</serviceCredentials>
-->
<serviceAuthorization
serviceAuthorizationManagerType=
"ClientAuthorization.ClientCertificateAuthorizationManager, Simulator.Service.SideA" />
</behavior>
</serviceBehaviors>
Client
Bindings:
<wsHttpBinding>
<binding name="CustomBinding">
<security mode="Transport">
<transport clientCredentialType="Certificate"/>
</security>
</binding>
</wsHttpBinding>
Behaviors
<endpointBehaviors>
<behavior name="ChannelManagerBehavior">
<clientCredentials>
<clientCertificate findValue="client1"
storeLocation="LocalMachine"
storeName="My"
x509FindType="FindBySubjectName" />
<!--
<serviceCertificate>
<authentication certificateValidationMode="PeerOrChainTrust"/>
</serviceCertificate>
-->
</clientCredentials>
</behavior>
</endpointBehaviors>
UPDATE
So I added a custom username and password validator to the server in an attempt to override the default behavior and always allow regardless of the credentials presented (again I really don't want username/password validation). This validator never gets invoked. The Client still gets the "authentication scheme 'Anonymous'.” exception.
Service Behavior Update
<serviceCredentials>
<userNameAuthentication
userNamePasswordValidationMode="Custom"
customUserNamePasswordValidatorType=
"Service.ClientAuthorization.ClientUserNamePasswordValidatorManager, Service.SideA" />
</serviceCredentials>
here is the demo for your reference. i tested it under win7 + vs2010 + client-server-on-same-machine.
server side:
[ServiceContract(Name="CalculatorService")]
public interface ICalculatorService {
[OperationContract]
int Add(int x, int y);
}
public class CalculatorService : ICalculatorService {
public Int32 Add(Int32 x, Int32 y) {
Console.WriteLine("{0}: service method called (x = {1}, y = {2})",
Thread.CurrentThread.ManagedThreadId, x, y);
return x + y;
}
}
class Program {
static void Main(string[] args) {
ServicePointManager.ServerCertificateValidationCallback +=
(sender, certificate, chain, sslPolicyErrors) => true;
using (var serviceHost = new ServiceHost(typeof(CalculatorService))) {
serviceHost.Opened += delegate {
Console.WriteLine("{0}: service started",
Thread.CurrentThread.ManagedThreadId);
};
serviceHost.Open();
Console.Read();
}
}
}
<?xml version="1.0" encoding="utf-8" ?> <configuration>
<system.serviceModel>
<bindings>
<wsHttpBinding>
<binding name="transportSecurity">
<security mode="Transport">
<transport clientCredentialType="Certificate"/>
</security>
</binding>
</wsHttpBinding>
</bindings>
<services>
<service name="WcfService.CalculatorService">
<endpoint address="https://hp-laptop:3721/calculatorservice"
binding="wsHttpBinding"
bindingConfiguration="transportSecurity"
contract="Contract.ICalculatorService" />
</service>
</services>
</system.serviceModel> </configuration>
Client side:
class Program {
static void Main(string[] args) {
using (var channelFactory =
new ChannelFactory<ICalculatorService>("calculatorservice")) {
ICalculatorService proxy = channelFactory.CreateChannel();
Console.WriteLine(proxy.Add(1, 2));
Console.Read();
}
Console.Read();
}
}
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.serviceModel>
<bindings>
<wsHttpBinding>
<binding name="transportSecurity">
<security mode="Transport">
<transport clientCredentialType="Certificate"/>
</security>
</binding>
</wsHttpBinding>
</bindings>
<behaviors>
<endpointBehaviors>
<behavior name="defaultClientCertificate">
<clientCredentials>
<clientCertificate
storeLocation="LocalMachine"
storeName="My"
x509FindType="FindBySubjectName"
findValue="client1"/>
</clientCredentials>
</behavior>
</endpointBehaviors>
</behaviors>
<client>
<endpoint name="calculatorservice" behaviorConfiguration="defaultClientCertificate"
address="https://hp-laptop:3721/calculatorservice"
binding="wsHttpBinding"
bindingConfiguration="transportSecurity"
contract="Contract.ICalculatorService"/>
</client>
</system.serviceModel>
</configuration>
Certificate creation:
self-created CA
makecert -n "CN=RootCA" -r -sv c:\rootca.pvk c:\rootca.cer
After creation, import this certificate into 'Trusted Root Certification' by Certificates Console. This is the step to stop the exception you mentioned.
certificate for service program
makecert -n "CN=hp-laptop" -ic c:\rootca.cer -iv c:\rootca.pvk -sr LocalMachine -ss My -pe -sky exchange
Note that the CN value above should match to the DNS part of service address. e.g. hp-laptop is my computer name. the address of service endpoint will be "https://google.com:/...". (replace google dot com with 'hp-laptop' due to some stackoverflow rules).
register service certificate with service program:
netsh http add sslcert ipport=0.0.0.0:3721 certhash=6c78ad6480d62f5f460f17f70ef9660076872326 appid={a0327398-4069-4d2d-83c0-a0d5e6cc71b5}
The certhash value is the thumbprint of service program certificate (check with Certificates Console). the appid is the GUID from service program file "AssemblyINfo.cs".
Certicate for client program:
makecert -n "CN=client1" -ic c:\rootca.cer -iv c:\rootca.pvk -sr LocalMachine -ss My -pe -sky exchange
UPDATE: according to typhoid's experience with this solution, the 'Anonymous' exception is still there due to too many trusted root authorities in that server. Two links was provided by typhoid to solve this issue.
http://support.microsoft.com/kb/2464556
http://blog.codit.eu/post/2013/04/03/Troubleshooting-SSL-client-certificate-issue-on-IIS.aspx
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With