ASP.NET MVC Client Certificate validation - c#

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 ?

Related

How to authenticate SSL certificates using RestSharp c#

The location of the certificates
Inherit auth from parent
The test in postman
I want to use Restsharp to send this GET request -
This is what I want to send -
Request - GET,
URL - https://18.0.1.230:8080/api/report/test,
Header - Key = Content-Type , Value = application/x-www-form-urlencoded
On Postman , I am not sending anything on Authorisation type (its set on "Inherit auth from parent") check 'inherit auth from parent' image
I have set Client certificates in Postman, so I have set the location of them, which postman uses when sending this request. Check 'Location of certificate' image
I want to know how do I add these certificates to a GET request using restSharp c# code, so that I pass the authentication and can get a 200 response?
What you need to do is first import the certificates:
public static async Task<X509Certificate2> LoadPemCertificate(string certificatePath, string privateKeyPath)
{
using var publicKey = new X509Certificate2(certificatePath);
var privateKeyText = await File.ReadAllTextAsync(privateKeyPath);
var privateKeyBlocks = privateKeyText.Split("-", StringSplitOptions.RemoveEmptyEntries);
var privateKeyBytes = Convert.FromBase64String(privateKeyBlocks[1]);
using var rsa = RSA.Create();
if (privateKeyBlocks[0] == "BEGIN PRIVATE KEY")
{
rsa.ImportPkcs8PrivateKey(privateKeyBytes, out _);
}
else if (privateKeyBlocks[0] == "BEGIN RSA PRIVATE KEY")
{
rsa.ImportRSAPrivateKey(privateKeyBytes, out _);
}
var keyPair = publicKey.CopyWithPrivateKey(rsa);
return new X509Certificate2(keyPair.Export(X509ContentType.Pfx));
}
Once you're you have loaded the certificate in memory, all you have to do is attach certificate to restsharp client:
var client = new RestClient("https://18.0.1.230:8080/api/report/test");
client.ClientCertificates.Add(new X509Certificate(certificate));
...

Getting a 403 when calling Azure Resource Rate API using certificate auth

I am trying to create a console app that can call the Azure Resource Rate API using certificate authentication. For this, I used the following branch GitHub link.
I am getting a 403 error. I've added an Web App to my Azure AD. In the manifest, I've copied the key credentials from the certificate I've signed using the following PowerShell commands;
$cert=New-SelfSignedCertificate -Subject "CN=RateCardCert"
-CertStoreLocation "Cert:\CurrentUser\My" -KeyExportPolicy Exportable -KeySpec Signature
$bin = $cert.RawData $base64Value = [System.Convert]::ToBase64String($bin)
$bin = $cert.GetCertHash()
$base64Thumbprint = [System.Convert]::ToBase64String($bin)
$keyid = [System.Guid]::NewGuid().ToString()
$jsonObj = # customKeyIdentifier=$base64Thumbprint;keyId=$keyid;type="AsymmetricX509Cert";usage="Verify";value=$base64Value}
$keyCredentials=ConvertTo-Json #($jsonObj) | Out-File "keyCredentials.txt"
In de console app, I use the following function to get the token;
public static string GetOAuthTokenFromAAD_ByCertificate(string TenanatID, string ClientID, string CertificateName)
{
//Creating the Authentication Context
var authContext = new AuthenticationContext(string.Format("https://login.windows.net/{0}", TenanatID));
//Console.WriteLine("new authContext made");
//Creating the certificate object. This will be used to authenticate
X509Certificate2 cert = null;
//Console.WriteLine("empty 'cert' made, null");
//The Certificate should be already installed in personal store of the current user under
//the context of which the application is running.
X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
try
{
//Trying to open and fetch the certificate
store.Open(OpenFlags.ReadOnly);
var certCollection = store.Certificates;
var certs = certCollection.Find(X509FindType.FindBySubjectName, CertificateName, false);
//Checking if certificate found
if (certs == null || certs.Count <= 0)
{
//Throwing error if certificate not found
throw new Exception("Certificate " + CertificateName + " not found.");
}
cert = certs[0];
}
finally
{
//Closing the certificate store
store.Close();
}
//Creating Client Assertion Certificate object
var certCred = new ClientAssertionCertificate(ClientID, cert);
//Fetching the actual token for authentication of every request from Azure using the certificate
var token = authContext.AcquireToken("https://management.core.windows.net/", certCred);
//Optional steps if you need more than just a token from Azure AD
//var creds = new TokenCloudCredentials(subscriptionId, token.AccessToken);
//var client = new ResourceManagementClient(creds);
//Returning the token
return token.AccessToken;
}
This is the part of the code that makes the URL and puts in the request (the xxxx part is replaced by the ClientID of the webapp that I've registered in Azure AD);
//Get the AAD User token to get authorized to make the call to the Usage API
string token = GetOAuthTokenFromAAD_ByCertificate("<MyTenantName.onmicrosoft.com", "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "RateCardCert");
// Build up the HttpWebRequest
string requestURL = String.Format("{0}/{1}/{2}/{3}",
ConfigurationManager.AppSettings["ARMBillingServiceURL"],
"subscriptions",
ConfigurationManager.AppSettings["SubscriptionID"],
"providers/Microsoft.Commerce/RateCard?api-version=2015-06-01-preview&$filter=OfferDurableId eq 'MS-AZR-0044P' and Currency eq 'EUR' and Locale eq 'nl-NL' and RegionInfo eq 'NL'");
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(requestURL);
// Add the OAuth Authorization header, and Content Type header
request.Headers.Add(HttpRequestHeader.Authorization, "Bearer " + token);
request.ContentType = "application/json";
// Call the RateCard API, dump the output to the console window
try
{
// Call the REST endpoint
Console.WriteLine("Calling RateCard service...");
HttpWebResponse response = (HttpWebResponse)request.GetResponse();
Console.WriteLine(String.Format("RateCard service response status: {0}", response.StatusDescription));
Stream receiveStream = response.GetResponseStream();
// Pipes the stream to a higher level stream reader with the required encoding format.
StreamReader readStream = new StreamReader(receiveStream, Encoding.UTF8);
var rateCardResponse = readStream.ReadToEnd();
Console.WriteLine("RateCard stream received. Press ENTER to continue with raw output.");
Console.ReadLine();
Console.WriteLine(rateCardResponse);
Console.WriteLine("Raw output complete. Press ENTER to continue with JSON output.");
Console.ReadLine();
// Convert the Stream to a strongly typed RateCardPayload object.
// You can also walk through this object to manipulate the individuals member objects.
RateCardPayload payload = JsonConvert.DeserializeObject<RateCardPayload>(rateCardResponse);
Console.WriteLine(rateCardResponse.ToString());
response.Close();
readStream.Close();
Console.WriteLine("JSON output complete. Press ENTER to close.");
Console.ReadLine();
}
catch(Exception e)
{
Console.WriteLine(String.Format("{0} \n\n{1}", e.Message, e.InnerException != null ? e.InnerException.Message : ""));
Console.ReadLine();
}
I just don't know what I have to do anymore, what am I missing here??
Full return of the console is:
Calling RateCard Service...
The remote server returned an error: (403) Forbidden.
After a chat in the comments the problem was found:
You are calling the Azure Resource Management API, but you gave permissions only on the Azure Service Management API. You need to add your app's service principal to a role in your subscription. Find your subscription, then the Access Control (IAM) blade, and then add your app to a role there. You should be able to find it with its name.
You can also add the service principal to a role on a resource group or even specific resources if you want to limit its capabilities.

Reading WebService Wsdl using c#.Net

We have a SOAP based web service and we are able to read its wsdl when we type in the url in Browser. We sit behind a proxy in our network but its not blocking anything and we are always able to read wsdl using browser.But when we enter the url in Browser say http://ist.services/CoreServices/coreservices?wsdl it asks for username and password which is not same as my windows credentials. So when i enter the username and password shared by the dev team , it returns the wsdl page. Please note that this webservice is developed and deployed on java based server.
How do i do the same in c#.net code and how do i pass the Security Crednetials in DiscoveryClientProtocol? I tried the below code which works for the webservices which doesn't ask for the Security credentials.
// Specify the URL to discover.
string sourceUrl = "http://ist.services/CoreServices/coreservices?wsdl";
string outputDirectory = "C:\\Temp";
DiscoveryClientProtocol client = new DiscoveryClientProtocol();
var credentials = new NetworkCredential("sunuser1", "xxxxxxx", "");
WebProxy proxy = new WebProxy("http://proxy.bingo:8000/", true) { Credentials = credentials };
client.Credentials = credentials;
// Use default credentials to access the URL being discovered.
//client.Credentials = credentials;//CredentialCache.DefaultCredentials;
client.Proxy = proxy;
String DiscoverMode = "DiscoverAny";
String ResolveMode = "ResolveAll";
try
{
DiscoveryDocument doc;
// Check to see if whether the user wanted to read in existing discovery results.
if (DiscoverMode == "ReadAll")
{
DiscoveryClientResultCollection results = client.ReadAll(Path.Combine("C:\\Temp", "results.discomap"));
//SaveMode.Value = "NoSave";
}
else
{
// Check to see if whether the user wants the capability to discover any kind of discoverable document.
if (DiscoverMode == "DiscoverAny")
{
doc = client.DiscoverAny(sourceUrl);
}
else
// Discover only discovery documents, which might contain references to other types of discoverable documents.
{
doc = client.Discover(sourceUrl);
}
// Check to see whether the user wants to resolve all possible references from the supplied URL.
if (ResolveMode == "ResolveAll")
client.ResolveAll();
else
{
// Check to see whether the user wants to resolve references nested more than one level deep.
if (ResolveMode == "ResolveOneLevel")
client.ResolveOneLevel();
else
Console.WriteLine("empty");
}
}
}
catch (Exception e2)
{
//DiscoveryResultsGrid.Columns.Clear();
//Status.Text = e2.Message;
Console.WriteLine(e2.Message);
}
// If documents were discovered, display the results in a data grid.
if (client.Documents.Count > 0)
Console.WriteLine(client);
}
}
Since the code didn't help me much , i opened the fiddler to trace the http calls when i manual read the wsdl in browser and i see it takes the credentials i entered as "Authorization: Basic cGDFDdsfdfsdsfdsgsgfg=" . In fiddler i see three calls with responses 401,302 and 200. But in my c#.net code i don't get the 200 response and it always throws me the 404 error.
I further debugged this and in httpresponse of client object i see the flag status as INVOCATION_FLAGS_INITIALIZED | INVOCATION_FLAGS_NEED_SECURITY
So looks like i need to pass the credentials as Security Credentials rather than Network credentials.
The below code has fixed the issue.
CredentialCache myCredentialCache = new CredentialCache { { new Uri(sourceUrl),
"Basic", networkCredential } };
discoveryClientProtocol.AllowAutoRedirect = true;
discoveryClientProtocol.Credentials = myCredentialCache;

Wcf with certificate as ClientCredentials

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");
}
}
}

WebApi HttpClient not sending client certificate

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();
}
}

Categories

Resources