In my WCF self-hosting WebService using mutual certificate to validate the client, i set the CertificateValidationMode = PeerTrust but its seems ignored, since i can still execute the methods with some client wich i have deleted the corresponding certificate of the TrustedPeople server store.
Heres the host example:
static void Main()
{
var httpsUri = new Uri("https://192.168.0.57:xxx/HelloServer");
var binding = new WSHttpBinding
{
Security =
{
Mode = SecurityMode.Transport,
Transport = {ClientCredentialType = HttpClientCredentialType.Certificate}
};
var host = new ServiceHost(typeof(HelloWorld), httpsUri);
//This line is not working
host.Credentials.ClientCertificate.Authentication.CertificateValidationMode =X509CertificateValidationMode.PeerTrust;
host.AddServiceEndpoint(typeof(IHelloWorld), binding, string.Empty, httpsUri);
host.Credentials.ServiceCertificate.SetCertificate(
StoreLocation.LocalMachine,
StoreName.My,
X509FindType.FindBySubjectName,
"server.com");
// Open the service.
host.Open();
Console.WriteLine("Listening on {0}...", httpsUri);
Console.ReadLine();
// Close the service.
host.Close();
}
The client app:
static void Main(string[] args)
{
try
{
var c = new HelloWorld.HelloWorldClient();
ServicePointManager.ServerCertificateValidationCallback = (sender, cert, chain, error) => true;
c.ClientCredentials.ClientCertificate.SetCertificate(
StoreLocation.LocalMachine,
StoreName.My,
X509FindType.FindBySubjectName,
"client.com");
Console.WriteLine(c.GetIp());
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
Console.ReadKey();
}
I generate the server.com and the client.com with a RootCA certificate. This RootCA certificate is instaled on the trusted root store of the client and server.
The question is, i should not execute the GetIp() method if my client.com certificate is not in the TrustedPeople store of the server, right? But im executing it without any problems.
The question is, how to, in this scenario, validate the client certificate put its public key on TrustedPeople of server?
ps: In this MSDN article of Transport security with client certificate, theres a quote saying The server’s certificate must be trusted by the client and the client’s certificate must be trusted by the server. But i can execute the webmethods from client even if the client certificate isnt in the server TrustedPeople store.
My suggestion would be to use custom validation. This way you can set some breakpoints and watch the validation take place as well as see what other validation options you could come up with based on the data available throughout the validation process.
First make sure you have your binding requiring Certificate for Message Client Credentials. If you only use Certificate for Transport, the Client in my tests did not validate. This alone may fix your issue.
binding.Security.Mode = SecurityMode.TransportWithMessageCredential;
binding.Security.Message.ClientCredentialType =
MessageCredentialType.Certificate;
To setup a custom validator follow the rest.
Replace:
host.Credentials.ClientCertificate.Authentication.CertificateValidationMode
=X509CertificateValidationMode.PeerTrust;
With:
host.Credentials.ClientCertificate.Authentication.CertificateValidationMode
=X509CertificateValidationMode.Custom;
host.Credentials.ClientCertificate.Authentication.CustomCertificateValidator =
new IssuerNameCertValidator("CN=client.com");
Then add this to create the custom validator and tweak as needed (this one validates based on Issuer):
public class IssuerNameCertValidator : X509CertificateValidator
{
string allowedIssuerName;
public IssuerNameCertValidator(string allowedIssuerName)
{
if (allowedIssuerName == null)
{
throw new ArgumentNullException("allowedIssuerName");
}
this.allowedIssuerName = allowedIssuerName;
}
public override void Validate(X509Certificate2 certificate)
{
// Check that there is a certificate.
if (certificate == null)
{
throw new ArgumentNullException("certificate");
}
// Check that the certificate issuer matches the configured issuer.
if (allowedIssuerName != certificate.IssuerName.Name)
{
throw new SecurityTokenValidationException
("Certificate was not issued by a trusted issuer");
}
}
}
Related
I have several remote servers that communicate with a central SOAP Service, where they can download the latest X509Certificate2 which can then be used to call a third-party API that requires this certificate to authenticate the requests.
Some of these remote servers are hosted by some of our clients on their own Windows Servers, which may be VMs or physical boxes, and others are hosted by us on Azure VMs.
We have had no previous issue with this functionality until recently when we moved our APIs from being hosted on a physical box to now being hosted in an Azure App Service (with an App Gateway handling requests).
What now happens is that all of the non-Azure servers, download the certificate successfully, but the certificate fails when used with the third-party rejecting the certificate with the error:
The request was aborted: Could not create SSL/TLS secure channel.
I have confirmed that if I manually copy the certificate to the servers, it works fine, but for some reason somewhere in the process of downloading it from the Azure SOAP API it now fails.
The code that we have to export the certificate on the API side so that it can be downloaded is something like:
[WebMethod]
public ClientCertificateMessage GetCertificate(LoginMessage login, string customer)
{
ClientCertificateMessage returnValue = new ClientCertificateMessage();
if (Authentication.VerifyLogin(login))
{
try
{
string filePathNameCertificate = ConfigurationManager.AppSettings["PathToLocalCert"].ToString() + customer + ".p12";
string filePathNamePassword = ConfigurationManager.AppSettings["PathToLocalCert"].ToString() + customer + ".txt";
string password = File.ReadAllText(filePathNamePassword);
X509Certificate2 x509Certificate2 = new X509Certificate2( filePathNameCertificate, password, X509KeyStorageFlags.Exportable);
returnValue.Certificate = x509Certificate2.Export(X509ContentType.Pkcs12, password);
}
catch (Exception ex)
{
// Log Error stuff here is removed
}
}
else
{
// error stuff here is removed
}
return returnValue;
}
The service that retrieves this certificate and uses it looks like:
public static X509Certificate2 GetCertificate(int certificateID, string password, string customer)
{
X509Certificate2 x509Certificate2 = null;
try
{
SoapClient client = new SoapClient();
Login login = CreateLogin();
ClientCertificateMessage clientCertificateMessage = null;
try
{
clientCertificateMessage = client.GetCertificate(login, customer);
}
catch (Exception ex)
{
// error logging removed
}
if ((clientCertificateMessage != null) && (clientCertificateMessage.Certificate != null))
{
using (CertificateData cd = new CertificateData())
{
dynamic revisedCertificate = new ExpandoObject();
revisedCertificate.Certificate = clientCertificateMessage.Certificate;
cd.Update(revisedCertificate, certificateID); // Save certificate data in database for later use
}
x509Certificate2 = new X509Certificate2(clientCertificateMessage.Certificate, password);
}
else
{
// handle logic got no certificate remove
}
}
catch (Exception ex)
{
// error logging removed
}
return x509Certificate2;
}
I cannot see anything that would explain why VMs on Azure continue to work, but all other VMs do not. We know those other servers can communicate and download the certificate, and when recreating the certificate from the byte array saved in the database the thumbprint and everything else matches.
I have seen other articles regarding Certificates on App Services where you need add Settings for WEBSITE_LOAD_CERTIFICATES or WEBSITE_LOAD_USER_PROFILE, however, we have not done this as the certificates do not fail to be generated.
Is there anything that I am missing where perhaps some odd Azure configuration or even some obvious technical reason for why a certificate downloaded fails, but the same certificate manually copied to the server works?
Thanks in advance for helping out.
I'm actually trying to expose some methods of an ASP.NET MVC specific controller, in order to secure sensitive calls.
The entire website doesn't have to be protected by a specific SSL certificate, but some requests are.
Here is my code (as simple as it is) to get "Data", as you can see, I first check the SSL certificate, then the process continues if the SSL Certificate is correct :
public string GetData()
{
try
{
var certificate = Request.ClientCertificate;
if (certificate == null || string.IsNullOrEmpty(certificate.Subject))
{
// certificate may not be here
throw new Exception("ERR_00_NO_SSL_CERTIFICATE");
}
if (!certificate.IsValid || !IsMyCertificateOK(certificate))
{
// certificate is not valid
throw new Exception("ERR_01_WRONG_SSL_CERTIFICATE");
}
// Actions here ...
}
catch (Exception)
{
Response.StatusCode = 400;
Response.StatusDescription = "Bad Request";
}
}
Here is my IIS configuration :
SSL Certificate is set to "Accept", thus, I hope I could get the client certificate in the Request.ClientCertificate property, but it's never the case, I never get the certificate set in my client.
Here is my client code (copied from generated Postman C# code) :
string PFX_PATH = #"C:\Test\test.pfx"; // set here path of the PFX file
string PFX_PASSWORD = "password"; // set here password of the PFX file
var client = new RestClient("https://mywebsite.com/GetData?input=test");
client.Timeout = -1;
client.ClientCertificates = new System.Security.Cryptography.X509Certificates.X509CertificateCollection()
{
new System.Security.Cryptography.X509Certificates.X509Certificate(PFX_PATH,
PFX_PASSWORD,
System.Security.Cryptography.X509Certificates.X509KeyStorageFlags.Exportable)
};
var request = new RestRequest(Method.GET);
IRestResponse response = client.Execute(request);
Console.WriteLine(response.Content);
The PFX file has a private key, and is accessible from client side.
Am I missing something regarding the IIS configuration, or should I update my web.config somehow ?
I've done work with WCF before - but since it was for in-house use only, not accessable from the internet at all, i just used net.tcp and not cared much about security.
However, i am now in pre-production for a project that will be made availlable over the internet to customers, so security must be planed for.
I've been doing some research on the matter, and from what i gathered (correct me if I am wrong here), HTTPS is my best bet, as HTTP isn't secured at all (by default) and net.tcp could find problems with some firewalls.
Howerer, I don't want to force customers to have to install IIS in their servers if they don't want to, so the plan is to use a self hosted Windows Service. However, i can't seem to find any information on how to setup the server to use HTTPS without na IIS.
I found information about using makecert and httpcfg set ssl to add a new certificate to the store and then set it to a port - that's ok for testing but i'm not seeing this feaseable in the customer's server - not to mention this means i'll be using aself signed certificate - again ok for testing, not so much in production
I also found information (ServiceCredentials MSDN page) about using something like
sh.Credentials.ServiceCertificate.SetCertificate(
StoreLocation.LocalMachine, StoreName.My,
X509FindType.FindByThumbprint,
"af1f50b20cd413ed9cd00c315bbb6dc1c08da5e6");
to set a certificate that is already in the server's certificate store - that would almost be ok - it still require the customer to know how to manage certificates in the store, not perfect but ok. However i couldn't get it to work - i don't get any error starting the servisse, but if i try to go to the service address in a browser i get na error regarding TLS beeing out of date - Q1: Any idea what could be the problem here?
Q2: Is it possible to have a configuration somewhere where the customer could input the has or at least location for the cert and key files one gets when buying a certificate and use that to secure the service?
Q1: As mentioned in the errors, there may be a problem in your certificate. be sure that the certificate is valid(self signed certificate can not expire).
Q2: As far as I know, we could save the certificate as a file(pfx, cert) or install the certificate in the certificate store(certlm.msc, certmgr.msc) in order to manage.
Do you want to host the WCF service over https in windows service project? I have made a demo, wish it is useful to you.
Service1.cs
public partial class Service1 : ServiceBase
{
public Service1()
{
InitializeComponent();
}
Uri uri = new Uri("https://localhost:1017");
ServiceHost sh = null;
protected override void OnStart(string[] args)
{
BasicHttpBinding binding = new BasicHttpBinding();
binding.Security.Mode = BasicHttpSecurityMode.Transport;
binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.None;
try
{
ServiceHost sh = new ServiceHost(typeof(MyService), uri);
sh.AddServiceEndpoint(typeof(IService), binding, "");
ServiceMetadataBehavior smb;
smb = sh.Description.Behaviors.Find<ServiceMetadataBehavior>();
if (smb == null)
{
smb = new ServiceMetadataBehavior()
{
HttpsGetEnabled=true,
};
sh.Description.Behaviors.Add(smb);
}
Binding mexbinding = MetadataExchangeBindings.CreateMexHttpsBinding();
sh.AddServiceEndpoint(typeof(IMetadataExchange), mexbinding, "mex");
sh.Open();
WriteLog($"Service is ready at {DateTime.Now.ToString("hh-mm-ss")}");
}
catch (Exception e)
{
WriteLog(e.ToString());
throw;
}
}
protected override void OnStop()
{
if (sh!=null&&sh.State==CommunicationState.Opened)
{
sh.Close();
WriteLog($"Service is closed at {DateTime.Now.ToString("hh-mm-ss")}");
}
}
public static void WriteLog(string text)
{
using (StreamWriter sw = File.AppendText(#"C:\Mylog.txt"))
{
sw.WriteLine(text);
sw.Flush();
}
}
}
[ServiceContract(Namespace = "mydomain")]
public interface IService
{
[OperationContract]
string SayHello();
}
public class MyService : IService
{
public string SayHello()
{
Service1.WriteLog(string.Format("Wow, I have been called at {0}", DateTime.Now.ToString("hh-mm-ss")));
return "Hello stranger";
}
}
ProjectInstaller.cs
Install the windows service(administrator privilege CMD)
Bind the certificate to the application port.
https://learn.microsoft.com/en-us/windows/desktop/http/add-sslcert
https://learn.microsoft.com/en-us/dotnet/framework/wcf/feature-details/how-to-configure-a-port-with-an-ssl-certificate
Certhash parameter specifies the thumbprint of the certificate. The appid parameter is a GUID that can be used to identify the owning application(open the project.csproj file)
<ProjectGuid>{56FDE5B9-3821-49DB-82D3-9DCE376D950A}</ProjectGuid>
Start the windows service.
Test(Server Ip is 10.157.13.70):
Client invocation(there is a step that validates the server certificate by default)
static void Main(string[] args)
{
ServicePointManager.ServerCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true;
ServiceReference1.ServiceClient client = new ServiceReference1.ServiceClient();
try
{
var result = client.SayHello();
Console.WriteLine(result);
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
}
Result
Feel free to let me know if there is anything I can help with.
I am trying to secure my RESTful WebApi service with ssl and client authentication using client certificates.
To test; I have generated a self signed certificate and placed in the local machine, trusted root certification authorities folder and i have generated a "server" and "client" certificates.
Standard https to the server works without issue.
However I have some code in the server to validate the certificate, this never gets called when I connect using my test client which supplies my client certificate and the test client is returned a 403 Forbidden status.
This imples the server is failing my certificate before it reaches my validation code.
However if i fire up fiddler it knows a client certificate is required and asks me to supply one to My Documents\Fiddler2. I gave it the same client certificate i use in my test client and my server now works and received the client certificate i expect!
This implies that the WebApi client is not properly sending the certificate, my client code below is pretty much the same as other examples i have found.
static async Task RunAsync()
{
try
{
var handler = new WebRequestHandler();
handler.ClientCertificateOptions = ClientCertificateOption.Manual;
handler.ClientCertificates.Add(GetClientCert());
handler.ServerCertificateValidationCallback += Validate;
handler.UseProxy = false;
using (var client = new HttpClient(handler))
{
client.BaseAddress = new Uri("https://hostname:10001/");
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml"));
var response = await client.GetAsync("api/system/");
var str = await response.Content.ReadAsStringAsync();
Console.WriteLine(str);
}
} catch(Exception ex)
{
Console.Write(ex.Message);
}
}
Any ideas why it would work in fiddler but not my test client?
Edit: Here is the code to GetClientCert()
private static X509Certificate GetClientCert()
{
X509Store store = null;
try
{
store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
store.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadOnly);
var certs = store.Certificates.Find(X509FindType.FindBySubjectName, "Integration Client Certificate", true);
if (certs.Count == 1)
{
var cert = certs[0];
return cert;
}
}
finally
{
if (store != null)
store.Close();
}
return null;
}
Granted the test code does not handle a null certificate but i am debugging to enssure that the correct certificate is located.
There are 2 types of certificates. The first is the public .cer file that is sent to you from the owner of the server. This file is just a long string of characters. The second is the keystore certificate, this is the selfsigned cert you create and send the cer file to the server you are calling and they install it. Depending on how much security you have, you might need to add one or both of these to the Client (Handler in your case). I've only seen the keystore cert used on one server where security is VERY secure. This code gets both certificates from the bin/deployed folder:
#region certificate Add
// KeyStore is our self signed cert
// TrustStore is cer file sent to you.
// Get the path where the cert files are stored (this should handle running in debug mode in Visual Studio and deployed code) -- Not tested with deployed code
string executableLocation = Path.GetDirectoryName(AppDomain.CurrentDomain.RelativeSearchPath ?? AppDomain.CurrentDomain.BaseDirectory);
#region Add the TrustStore certificate
// Get the cer file location
string pfxLocation = executableLocation + "\\Certificates\\TheirCertificate.cer";
// Add the certificate
X509Certificate2 theirCert = new X509Certificate2();
theirCert.Import(pfxLocation, "Password", X509KeyStorageFlags.DefaultKeySet);
handler.ClientCertificates.Add(theirCert);
#endregion
#region Add the KeyStore
// Get the location
pfxLocation = executableLocation + "\\Certificates\\YourCert.pfx";
// Add the Certificate
X509Certificate2 YourCert = new X509Certificate2();
YourCert.Import(pfxLocation, "PASSWORD", X509KeyStorageFlags.DefaultKeySet);
handler.ClientCertificates.Add(YourCert);
#endregion
#endregion
Also - you need to handle cert errors (note: this is BAD - it says ALL cert issues are okay) you should change this code to handle specific cert issues like Name Mismatch. it's on my list to do. There are plenty of example on how to do this.
This code at the top of your method
// Ignore Certificate errors need to fix to only handle
ServicePointManager.ServerCertificateValidationCallback = MyCertHandler;
Method somewhere in your class
private bool MyCertHandler(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors error)
{
// Ignore errors
return true;
}
In the code you are using store = new X509Store(StoreName.My, StoreLocation.LocalMachine);.
Client certificates are not picked up from LocalMachine, you should instead use StoreLocation.CurrentUser.
Checking MMC -> File -> Add or Remove Snap-ins -> Certificates -> My user account you will see the certificate that fiddler uses. If you remove it from My user account and only have it imported in Computer account you will see that Fiddler can not pick it up either.
A side note is when finding certificates you also have to address for culture.
Example:
var certificateSerialNumber= "83 c6 62 0a 73 c7 b1 aa 41 06 a3 ce 62 83 ae 25".ToUpper().Replace(" ", string.Empty);
//0 certs
var certs = store.Certificates.Find(X509FindType.FindBySerialNumber, certificateSerialNumber, true);
//null
var cert = store.Certificates.Cast<X509Certificate>().FirstOrDefault(x => x.GetSerialNumberString() == certificateSerialNumber);
//1 cert
var cert1 = store.Certificates.Cast<X509Certificate>().FirstOrDefault(x =>
x.GetSerialNumberString().Equals(certificateSerialNumber, StringComparison.InvariantCultureIgnoreCase));
try this.
Cert should be with the current user store.
Or give full rights and read from a file as it is a console application.
// Load the client certificate from a file.
X509Certificate x509 = X509Certificate.CreateFromCertFile(#"c:\user.cer");
Read from the user store.
private static X509Certificate2 GetClientCertificate()
{
X509Store userCaStore = new X509Store(StoreName.My, StoreLocation.CurrentUser);
try
{
userCaStore.Open(OpenFlags.ReadOnly);
X509Certificate2Collection certificatesInStore = userCaStore.Certificates;
X509Certificate2Collection findResult = certificatesInStore.Find(X509FindType.FindBySubjectName, "localtestclientcert", true);
X509Certificate2 clientCertificate = null;
if (findResult.Count == 1)
{
clientCertificate = findResult[0];
}
else
{
throw new Exception("Unable to locate the correct client certificate.");
}
return clientCertificate;
}
catch
{
throw;
}
finally
{
userCaStore.Close();
}
}
My C#.NET SSL connect works when I import the certificate manually in IE (Tools/Internet Options/Content/Certificates), but how can I load the certificate by code?
Here is my code:
TcpClient client = new TcpClient(ConfigManager.SSLSwitchIP, Convert.ToInt32(ConfigManager.SSLSwitchPort));
SslStream sslStream = new SslStream(
client.GetStream(),
false,
new RemoteCertificateValidationCallback(ValidateServerCertificate),
null
);
sslStream.AuthenticateAsClient("Test");
The above code works fine if i import my certificate file manually in Internet Explorer. But if i remove my certificate from IE and use the following code instead, i get Authentication exception:
sslStream.AuthenticateAsClient("Test", GetX509CertificateCollection(), SslProtocols.Default, false);
and here is the 'GetX509CertificateCollection' method :
public static X509CertificateCollection GetX509CertificateCollection()
{
X509Certificate2 certificate1 = new X509Certificate2("c:\\ssl.txt");
X509CertificateCollection collection1 = new X509CertificateCollection();
collection1.Add(certificate1);
return collection1;
}
What should I do to load my certificate dynamically?
To build upon owlstead's answer, here's how I use a single CA certificate and a custom chain in the verification callback to avoid Microsoft's store.
I have not figured out how to use this chain (chain2 below) by default such that there's no need for the callback. That is, install it on the ssl socket and the connection will "just work". And I have not figured out how install it such that its passed into the callback. That is, I have to build the chain for each invocation of the callback. I think these are architectural defects in .Net, but I might be missing something obvious.
The name of the function does not matter. Below, VerifyServerCertificate is the same callback as RemoteCertificateValidationCallback. You can also use it for the ServerCertificateValidationCallback in ServicePointManager.
static bool VerifyServerCertificate(object sender, X509Certificate certificate,
X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
try
{
String CA_FILE = "ca-cert.der";
X509Certificate2 ca = new X509Certificate2(CA_FILE);
X509Chain chain2 = new X509Chain();
chain2.ChainPolicy.ExtraStore.Add(ca);
// Check all properties
chain2.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag;
// This setup does not have revocation information
chain2.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
// Build the chain
chain2.Build(new X509Certificate2(certificate));
// Are there any failures from building the chain?
if (chain2.ChainStatus.Length == 0)
return true;
// If there is a status, verify the status is NoError
bool result = chain2.ChainStatus[0].Status == X509ChainStatusFlags.NoError;
Debug.Assert(result == true);
return result;
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
return false;
}
A quick Google pointed me to a piece of text from the Microsoft SslStream class.
The authentication is handled by the Security Support Provider (SSPI)
channel provider. The client is given an opportunity to control
validation of the server's certificate by specifying a
RemoteCertificateValidationCallback delegate when creating an
SslStream. The server can also control validation by supplying a
RemoteCertificateValidationCallback delegate. The method referenced by
the delegate includes the remote party's certificate and any errors
SSPI encountered while validating the certificate. Note that if the
server specifies a delegate, the delegate's method is invoked
regardless of whether the server requested client authentication. If
the server did not request client authentication, the server's
delegate method receives a null certificate and an empty array of
certificate errors.
So simply implement the delegate and do the verification yourself.
I wrote another method to add my certificate to Trusted Root Certification Authorities (root) before attempting to authenticate as client via SSLStream object:
public static void InstallCertificate()
{
X509Store store = new X509Store(StoreName.Root, StoreLocation.LocalMachine);
store.Open(OpenFlags.ReadWrite);
string fileName = "sslcert.pem";
X509Certificate2 certificate1;
try
{
certificate1 = new X509Certificate2(fileName);
}
catch (Exception ex)
{
throw new Exception("Error loading SSL certificate file." + Environment.NewLine + fileName);
}
store.Add(certificate1);
store.Close();
}
And then:
InstallCertificate();
sslStream.AuthenticateAsClient("Test");
It works fine without any warnings or errors. But base question still remains unsolved:
How can I use a certificate to authenticate as client without installing it in Windows?