I'm developing a authentication system with a user certificates based on C# and MVC and I’m having some problems.
I use a SSL security with a CA certificate and I want to get back some certificates on a client side. When I run my application on the local side, I get the personal certificate, and my authentication system works correctly.
However, when I run the same application on the server side, I don't get back any personal certificate if it's installed in the navigator...
Here, you have the C# code that im developing.
private X509Certificate2 GetClientCertificate()
{
X509Store userCaStore = new X509Store(StoreName.My,StoreLocation.CurrentUser);
try
{
userCaStore.Open(OpenFlags.OpenExistingOnly);
X509Certificate2Collection certificatesInStore = userCaStore.Certificates
.Find(X509FindType.FindByTimeValid, DateTime.Now, true);
X509Certificate2 clientCertificate = null;
if (certificatesInStore.Count > 0)
{
clientCertificate = certificatesInStore[0];
}
else
{
return null;
}
return clientCertificate;
}
catch
{
throw;
}
finally
{
userCaStore.Close();
}
}
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 talk to my server using SSL, but have self-signed cert.
In Android I use this code to pass my SSL cert to system to be able make requests to my server:
KeyStore ks = KeyStore.getInstance("BKS");
InputStream in = new ByteArrayInputStream(<byte [] my_ssl_data>);
ks.load(in, <string mypassword>.toCharArray());
in.close();
TrustManagerFactory Main_TMF = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
Main_TMF.init(ks);
X509TrustManager Cur_Trust_Manager = new X509TrustManager()
{
public void checkClientTrusted(X509Certificate [] chain, String authType) throws CertificateException { }
public void checkServerTrusted(X509Certificate [] chain, String authType) throws CertificateException { }
public X509Certificate [] getAcceptedIssuers() { return null; }
};
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[] { Cur_Trust_Manager }, new SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());
HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier()
{
#Override
public boolean verify(String hostname, SSLSession session)
{
try
{
Cur_Trust_Manager.checkServerTrusted((X509Certificate []) session.getPeerCertificates(), session.getCipherSuite());
return true;
}
catch (Exception e) {}
return false;
}
});
Now I need something like this in Windows Universal App 8.1+ (Windows+WindowsPhone) and iOS 7.0+.
For network requests I use System.Net.Http.HttpClient which works both with UWP and Xamarin.iOS. I have cert from my server in DER format but still can't add a handler to HttpClient.
Xamarin.iOS version says "Not implemented"
HttpClientHandler myHandler = new HttpClientHandler();
X509Certificate2 certificate = new X509Certificate2();
certificate.Import(my_cert_der_bytes);
myHandler.ClientCertificates.Add(certificate);
myHandler.ClientCertificateOptions = ClientCertificateOption.Manual;
myHandler.AllowAutoRedirect = false;
HttpClient c = new HttpClient(myHandler);
....
UWP version unfortunately (and why the hell?) doesn't know X509Certificate2 and all that stuff. I tried to use WinRtHttpClientHandler but didn't understand where should I pass my cert. I tried to skip errors (started to work), yet that's not a solution, because I don't want my requests to be redirected to another untrasted server.
var filter = new HttpBaseProtocolFilter();
Certificate cer = new Certificate(my_cert_bytes.AsBuffer());
filter.IgnorableServerCertificateErrors.Add(ChainValidationResult.Untrusted);
filter.IgnorableServerCertificateErrors.Add(ChainValidationResult.Expired);
WinRtHttpClientHandler myHandler = new WinRtHttpClientHandler(filter);
HttpClient c = new HttpClient(myHandler);
....
I suspect this is common task for most indies, certs are seldom for tests and small apps. But it seems task is made very difficult by platform devs. Is there any reliable solution?
UWP apps cannot work with invalid (and self-signed) certificates. You may use Fiddler as proxy with fiddler's certificate for tests https.
Telerik Fiddler options - HTTPS - Capture HTTPS connects.
This work only on desktop not a mobile emulator.
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");
}
}
}
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();
}
}
Apparently I was asking the wrong question in my earlier post. I have a web service secured with a X.509 certificate, running as a secure web site (https://...). I want to use the client's machine certificate (also X.509) issued by the company's root CA to verify to the server that the client machine is authorized to use the service. In order to do this, I need to inspect the certificate and look for some identifying feature and match that to a value stored in a database (maybe the Thumbprint?).
Here is the code I use to get the certificate from the local certificate store (lifted straight from http://msdn.microsoft.com/en-us/magazine/cc163454.aspx):
public static class SecurityCertificate
{
private static X509Certificate2 _certificate = null;
public static X509Certificate2 Certificate
{
get { return _certificate; }
}
public static bool LoadCertificate()
{
// get thumbprint from app.config
string thumbPrint = Properties.Settings.Default.Thumbprint;
if ( string.IsNullOrEmpty( thumbPrint ) )
{
// if no thumbprint on file, user must select certificate to use
_certificate = PickCertificate( StoreLocation.LocalMachine, StoreName.My );
if ( null != _certificate )
{
// show certificate details dialog
X509Certificate2UI.DisplayCertificate( _certificate );
Properties.Settings.Default.Thumbprint = _certificate.Thumbprint;
Properties.Settings.Default.Save();
}
}
else
{
_certificate = FindCertificate( StoreLocation.LocalMachine, StoreName.My, X509FindType.FindByThumbprint, thumbPrint );
}
if ( null == _certificate )
{
MessageBox.Show( "You must have a valid machine certificate to use STS." );
return false;
}
return true;
}
private static X509Certificate2 PickCertificate( StoreLocation location, StoreName name )
{
X509Store store = new X509Store( name, location );
try
{
// create and open store for read-only access
store.Open( OpenFlags.ReadOnly );
X509Certificate2Collection coll = store.Certificates.Find( X509FindType.FindByIssuerName, STSClientConstants.NBCCA, true );
if ( 0 == coll.Count )
{
MessageBox.Show( "No valid machine certificate found - please contact tech support." );
return null;
}
// pick a certificate from the store
coll = null;
while ( null == coll || 0 == coll.Count )
{
coll = X509Certificate2UI.SelectFromCollection(
store.Certificates, "Local Machine Certificates",
"Select one", X509SelectionFlag.SingleSelection );
}
// return first certificate found
return coll[ 0 ];
}
// always close the store
finally { store.Close(); }
}
private static X509Certificate2 FindCertificate( StoreLocation location, StoreName name, X509FindType findType, string findValue )
{
X509Store store = new X509Store( name, location );
try
{
// create and open store for read-only access
store.Open( OpenFlags.ReadOnly );
// search store
X509Certificate2Collection col = store.Certificates.Find( findType, findValue, true );
// return first certificate found
return col[ 0 ];
}
// always close the store
finally { store.Close(); }
}
Then, I attach the certificate to the outbound stream thusly:
public static class ServiceDataAccess
{
private static STSWebService _stsWebService = new STSWebService();
public static DataSet GetData(Dictionary<string,string> DictParam, string action)
{
// add the machine certificate here, the first web service call made by the program (only called once)
_stsWebService.ClientCertificates.Add( SecurityCertificate.Certificate );
// rest of web service call here...
}
}
My question is this -- how do I "get" the certificate in the web service code? Most sample code snippets I have come across that cover how to do custom validation have a GetCertificate() call in there, apparently assuming that part is so easy everyone should know how to do it?
My main class inherits from WebService, so I can use Context.Request.ClientCertificate to get a certificate, but that's an HttpClientCertificate, not an X509Certificate2. HttpContext gives me the same result. Other approaches all use web configuration code to call pre-defined verification code, with no clue as to how to call a custom C# method to do the verification.
I recall doing something similar, its been awhile but, have you tried this in your web service:
X509Certificate2 cert = new X509Certificate2(Context.Request.ClientCertificate.Certificate);
On the subject of how to tie the certificate back to a user, so assuming the identity of the user associated with the key is good (as the certificate has been verified back to a trusted root and has not been revoked) then you need to tie the identity claimed by the cert to a user. You could just use the LDAP string form of the subject DN and look that up (cn=Username,ou=Department...) to determine the local ID. This is resiliant in the case the user re-generates their key/certificate say because of a card loss or natural expiry of the certificate. This relies on the fact that two CAs won't issue two certs with the same subject name to two different people.
If the certificate was issued by a MS CA it might have a UPN in it that is effectively a domain logon name.
Alternatively if you want to tie the user's identity to an actual certificate the usual method is to store the issuer name and certificate serial number. CAs must issue unique serial numbers for each certificate. Note serial numbers can be large depending on the CA. Not however that using this method then means the cert details in the database must be updated every time the user cert is re-issued.