SSL public key pinning is not working with HttpClientHandler for Xamarin.iOS, can still be eavesdropped. How do I secure the app against MITM attacks? - xamarin.ios

I am new to Xamarin Forms and also SSL Pinning. I am looking at an issue regarding SSL pinning in a preexisting app at work.
The idea is that, with server certificate (or public key) pinned, the app should close when a proxy (middle man) is connected.
I have checked many tutorials and have tried many solutions but the app seems to work normally confirming the public key even with proxy setup.
Attempt 1:
The code that was given to me is below and it uses ServerCertificateCustomValidationCallback and HttpClientHandler. I learnt that the ValidateRemoteCertificate method has the details of the server certificate, and the chain of certs associated to the remote cert (server).
https://learn.microsoft.com/en-gb/dotnet/api/system.net.security.remotecertificatevalidationcallback?view=net-7.0
// Main.cs
const string MyPublicKey = "YOUR_PUBLIC_KEY_STRING"
public async Task<HttpContent> CheckCertificate()
{
var destUri = new Uri("YOUR_URL");
var handler = new HttpClientHandler();
handler.ServerCertificateCustomValidationCallback = ValidateRemoteCertificate;
using (HttpClient client = new HttpClient(handler))
{
using (HttpResponseMessage result = await client.GetAsync(destUri))
{
return result.Content;
}
}
}
public bool ValidateRemoteCertificate(object sender,
X509Certificate cert,
X509Chain chain,
SslPolicyErrors policyErrors)
{
byte[] foundCert = null;
if (chain != null)
{
if (chain.ChainElements != null && chain.ChainElements.Count > 0)
{
if (chain.ChainElements[1].Certificate != null)
{
foundCert = chain.ChainElements[1].Certificate.RawData;
}
}
}
if (foundCert != null)
{
var key = cert.GetPublicKeyString();
if (!MyPublicKey.Equals(key))
{
// PublicKey mismatch, Exiting...
CloseApp();
return false;
}
else
{
// "Public key matches
}
return true;
}
return false;
}
This returns an exception with "Certificate Unknown". I found that the chain is not null, but ChainElements is empty.
I read many articles about certificate pinning and added the following code in Info.plist.
<key>NSAppTransportSecurity</key>
<dict>
<key>NSPinnedDomains</key>
<dict>
<key>YOUR_URL_HERE</key>
<dict>
<key>NSIncludesSubdomains</key>
<true/>
<key>NSPinnedCAIdentities</key>
<array>
<dict>
<key>SPKI-SHA256-BASE64</key>
<string>YOUR_KEY</string>
</dict>
</array>
</dict>
</dict>
</dict>
And I receive an exception that certificate is not trusted. I assume that this code in info.plist expects a trusted CA relating to this certificate to be trusted by the device owner. Which is not the target of this task.
Attempt 2:
I decided to just use public key pinning instead of certificate. So I changed ValidateRemoteCertificate code like this:
var key = cert.GetPublicKeyString();
if (!MyPublicKey.Equals(key))
{
// PublicKey mismatch, Exiting...
CloseApp();
return false;
}
else
{
// "Public key matches
}
return true;
And I removed info.plist changes. This allows the app to run smoothly. But the problem is, when a proxy is connected, it still works, meaning the cert still comes with the correct public key.
I am trying to figure out if I am missing something in this implementation or if the methods used here is even valid. I checked how it is handled in Android, I see the exact same implementation as my first code block, and with proxy, the app fails to launch (handshake failed; Trust anchor for certification path not found).
If this is an iOS issue, is there any other form of implementation that I need to use? Or is there a better way to pin cert (perhaps bundling the cert with the app)?
Update 1:
I have analysed the certificate chain validation a little further and observed these:
As mentioned in my Attempt 1, the chain received in ValidateRemoteCertificate method is empty (with or without proxy). Someone suggested that to build the chain using the certificate if this happened, in order to validate the chain.
I attempted to do "Build the chain" using this code.
chain.Build(cert))
But I get an error "operation not permitted on this platform" suggesting that "chain.Build" doesn't work for Xamarin.iOS.
Update 2:
I did manage to get the chain built using the following code and everything seems to work normally. But the public key also matches when a proxy is connected and is able to crawl iOS traffic successfully. Hence, making the app function normally in case of man in the middle.
public bool ValidateRemoteCertificate(object sender,
X509Certificate cert,
X509Chain chain,
SslPolicyErrors policyErrors)
{
if (policyErrors == SslPolicyErrors.None)
{
String actualPKString = cert.GetPublicKeyString();
// validate public key
if (!MyPublicKey.SequenceEqual(actualPKString))
{
Console.WriteLine("Security: public key mismatched.");
CloseApp();
return false;
}
//validate chain
if (chain != null)
{
if (!(chain.ChainElements != null && chain.ChainElements.Count > 0))
{
try
{
chain.Build(new X509Certificate2(cert));
Console.WriteLine("Security: built chain");
} catch(Exception e)
{
Console.WriteLine("Security: issues building chain {0}", e);
}
}
if (chain.ChainElements == null || chain.ChainElements.Count == 0)
{
Console.WriteLine("Security: Chain elements still null");
CloseApp();
return false;
}
}
return true;
}
Console.WriteLine("Security: error occured");
CloseApp();
return false;
}

Related

How Java (Java8) fix WhiteHat "Improper Certificate Validation" (CWE-295) security vulnerability

We use the WhiteHat Source scanner to scan our source code. The tool finds out 'Improper Certificate Validation' (CWE-295) security issue at 2 methods. Is it a True Positive security issue? If yes, how could we fix it in Java 8, do we have a solution to fix issue like this? Thank you very much.
public void checkClientTrusted(X509Certificate[] certs, String authType) --> security vuln
public void checkServerTrusted(X509Certificate[] certs, String authType) --> security vuln
// http://www.nakov.com/blog/2009/07/16/disable-certificate-validation-in-java-ssl-connections/
public class JavaCertificationUtils {
private static final SanitizedLogger LOG = new SanitizedLogger(JavaCertificationUtils.class);
public static void javaTrustAllCerts() {
try {
// Create a trust manager that does not validate certificate chains
TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() {
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return null;
}
public void checkClientTrusted(X509Certificate[] certs, String authType) {
}
public void checkServerTrusted(X509Certificate[] certs, String authType) {
}
}};
// Install the all-trusting trust manager
SSLContext sc = SSLContext.getInstance("SSL");
sc.init(null, trustAllCerts, new java.security.SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
// Create all-trusting host name verifier
HostnameVerifier allHostsValid = new HostnameVerifier() {
public boolean verify(String hostname, SSLSession session) {
return true;
}
};
// Install the all-trusting host verifier
HttpsURLConnection.setDefaultHostnameVerifier(allHostsValid);
} catch (Exception e) {
LOG.error("Java Certificate All Certs Exception.", e);
}
}
}
The code you are showing does two things:
It disables the TLS certificate chain validation (with trustAllCerts).
And than it disables the host name verification (with allHostsValid).
How to fix this? First of all - remove the code. All of it. The exact lines that seem to do the damage are:
sc.init(null, trustAllCerts, new java.security.SecureRandom());
HttpsURLConnection.setDefaultHostnameVerifier(allHostsValid);
You will probably notice (with a probability of 90%), that the code will stop working. This might be due to different reasons.
You are trying to connect to a service that uses a self signed certificate (might be a reason someone made a hack like this in the first place). The service would need to fix this.
You don't have all necessary certificates in the trust store to verify the trust chain. In this case you would need to extend your trust store and add the necessary Root CA certificates.
You connect over a SSH tunnel to a TLS secured service. Once you translate a proper domain name to a localhost: the TLS hostname validation will fail.
Something else will fail.
Let us know what the error is then.

Azure Web App: System.Security.Cryptography.CryptographicException: Keyset does not exist

My app encounters "System.Security.Cryptography.CryptographicException: Keyset does not exist" exception during the creation of a X509Certificate2.
Function to read cert from Azure Key Vault:
private string GetEncryptSecret(string certConfigName)
{
var kv = new KeyVaultClient(GetToken);
var sec = kv.GetSecretAsync(WebConfigurationManager.AppSettings[certConfigName]).Result;
return sec.Value;
}
How I create the new X509Certificate2 object:
public X509Certificate2 GetCertificate(CertificatesEnum certificate)
{
switch (certificate)
{
case CertificatesEnum.Accounts:
return new X509Certificate2(
Convert.FromBase64String(GetEncryptSecret(Constants.Magda.Certificates.Accounts)),
string.Empty, X509KeyStorageFlags.MachineKeySet);
}
}
After noticing that this was only working for the first 3-9 requests I've started an Azure Remote Debugging session and saw that the new certificate.Privatekey caused the "System.Security.Cryptography.CryptographicException: Keyset does not exist" exception.
Temporary fixed this by the implementation of a waiting loop because after a couple of retries the new certificate will be created without exception.
public X509Certificate2 GetCertificate(CertificatesEnum certificate)
{
switch (certificate)
{
case CertificatesEnum.Accounts:
var accountCertRawData = Convert.FromBase64String(GetEncryptSecret(Constants.Magda.Certificates.Accounts));
X509Certificate2 accountCert = null;
for (int i = 0; i < 20; i++)
{
try
{
accountCert= new X509Certificate2(accountCertRawData , string.Empty,
X509KeyStorageFlags.MachineKeySet);
//set variable to test exception
var accountCert = accountCert.PrivateKey;
break;
}
catch (System.Security.Cryptography.CryptographicException)
{
Thread.Sleep(1000 * i);
}
}
return accountCert;
}
}
Does anyone has a proper solution for this and can explain what is happening in the background on the Azure Web app?
Try removing WEBSITE_LOAD_USER_PROFILE application setting (if available) from the web application.
In Azure WebApp what seams to fix for me the issue with "some times" throws Keyset does not exist when calling GetPrivateKey() was not to use MachineKeySet for storage flags argument in constructor of X509Certificate2
new X509Certificate2(certContent, "", X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable)

Not able to verify SMIME messages signature signed with SHA256WITHRSA in Bouncy castle in java

One of our partner is sending signed SMIME messages to us using NOSPAMPROXY. Everything works fine except the one using SHA256WITHRSA . Bouncy castle is saying that the signature verification failed. Please see the code segment below,
public static boolean verifySignature(SMIMESigned s, X509Certificate myCert) throws Exception {
SignerInformationStore signers = s.getSignerInfos();
Collection c = signers.getSigners();
for (Iterator it = c.iterator(); it.hasNext(); ) {
try {
SignerInformation signer = (SignerInformation) it.next();
final SignerInformationVerifier verifier = new JcaSimpleSignerInfoVerifierBuilder().setProvider(BouncyCastleProvider.PROVIDER_NAME).build(
myCert);
if (signer.verify(verifier)) {
return true;
}
} catch (CMSException ex) {
Tracer.debug(ex);
}
}
return false;
}
myCert is the certificate we are giving for verify. Any help is appreciated .

Azure WorkerRole: Certificate Key not valid for use in specified state

System.Security.Cryptography.CryptographicException: Key not valid for
use in specified state.
at
System.Security.Cryptography.CryptographicException.ThrowCryptographicException(Int32
hr) at System.Security.Cryptography.Utils._ExportKey(SafeKeyHandle
hKey, Int32 blobType, Object cspObject) at
System.Security.Cryptography.RSACryptoServiceProvider.ExportParameters(Boolean
includePrivateParameters) at
System.Security.Cryptography.RSA.ToXmlString(Boolean
includePrivateParameters)
Now, i belive this happens because that when Azure adds the Certificate to my WorkerRole deployment, it do not install the certificate with the option "Mark this Key as Exportable".
I need to add a certificate to my workerrole to beable to decypt a encryptet setting.
Anyone have any ideas about how i can make Azure Mark the certificates private key as exportable. or if it could be another issue.
Onstart:
try{
var conn = System.Text.UTF8Encoding.UTF8.GetString(Decrypt(Convert.FromBase64String(setting), true, cert));
}catch(Exception ex)
{
Trace.TraceError(ex.ToString());
}
Methods:
public static X509Certificate2 LoadCertificate(StoreName storeName,
StoreLocation storeLocation, string tprint)
{
X509Store store = new X509Store(storeName, storeLocation);
try
{
store.Open(OpenFlags.ReadOnly);
X509Certificate2Collection certificateCollection =
store.Certificates.Find(X509FindType.FindByThumbprint,
tprint, false);
if (certificateCollection.Count > 0)
{
// We ignore if there is more than one matching cert,
// we just return the first one.
return certificateCollection[0];
}
else
{
throw new ArgumentException("Certificate not found");
}
}
finally
{
store.Close();
}
}
public static byte[] Decrypt(byte[] encryptedData, bool fOAEP,
X509Certificate2 certificate)
{
if (encryptedData == null)
{
throw new ArgumentNullException("encryptedData");
}
if (certificate == null)
{
throw new ArgumentNullException("certificate");
}
using (RSACryptoServiceProvider provider = new RSACryptoServiceProvider())
{
// Note that we use the private key to decrypt
provider.FromXmlString(GetXmlKeyPair(certificate));
return provider.Decrypt(encryptedData, fOAEP);
}
}
public static string GetXmlKeyPair(X509Certificate2 certificate)
{
if (certificate == null)
{
throw new ArgumentNullException("certificate");
}
if (!certificate.HasPrivateKey)
{
throw new ArgumentException("certificate does not have a private key");
}
else
{
return certificate.PrivateKey.ToXmlString(true);
}
}
I found a solution.
the answer is given in another question of mine here: How can I get a certificate by name given in ServiceDefinition on Azure

What is the best place to detect user sign in when using azure acs and mvc3?

I want to be able to detect when a user signs on to my application using passive acs, so that I can add them to my database if this is the first time using my app. Right now I am subscribing to WSFederationAuthenticationModule.SignedIn but I feel I'm missing something. Mainly I'm not sure the best place to subscribe to the event, I got it to work inside PostAuthenticateRequest but its a bit hacky. Any suggestions?
this code is from global.asax
public override void Init()
{
base.Init();
PostAuthenticateRequest += (s, e) =>
{
try
{
FederatedAuthentication.WSFederationAuthenticationModule.SignedIn -= SignedIn;
}
finally
{
FederatedAuthentication.WSFederationAuthenticationModule.SignedIn += SignedIn;
}
};
}
private void SignedIn(object sender, EventArgs e)
{
//do something
}
EDIT:
For now I'm going to use a flag variable to make sure I only subscribe once to SignedIn. Unless someone has any other suggestions that is :) thanks for the help Sandrino. Here is what I have at the moment.
private static bool isFirstRequest = true;
public override void Init()
{
base.Init();
PostAuthenticateRequest += (s, e) => {
if (isFirstRequest)
{
FederatedAuthentication
.WSFederationAuthenticationModule.SignedIn += SignedIn;
isFirstRequest = false;
}
};
}
private void SignedIn(object sender, EventArgs e)
{
//do something
}
EDIT:
A little more info. This problem happens if I'm using the azure emulator, it probably happens when deployed as well but I haven't tried that. I have tested if I am just not able to debug by trying to write to a text file and no text file was created.
Why do you subscribe to the SignedIn event each time the PostAuthenticateRequest event is raised? You can simple subscribe to it when the application starts (in the Global.asax) and it will be raised for each user that signed in:
public class MvcApplication : System.Web.HttpApplication
{
...
protected void Application_Start()
{
...
FederatedAuthentication.ServiceConfigurationCreated += (s, e) =>
{
FederatedAuthentication.WSFederationAuthenticationModule.SignedIn += new EventHandler(OnUserSignedIn);
};
}
private void OnUserSignedIn(object sender, EventArgs e)
{
// Custom logic here.
}
}
The SignedIn event is the best way to detect a user sign in before the application continues. Take a look at the following diagram. Before redirecting back to a page, the SignedIn event is raised to allow you to detect an user sign in:
Reference: http://msdn.microsoft.com/en-us/library/ee517293.aspx
I created a class that derives from ClaimsAuthenticationManager. There is only one method that you have to override, which is
public virtual IClaimsPrincipal Authenticate(string resourceName, IClaimsPrincipal incomingPrincipal);
In my app, I use this method to check if the user, who has successfully authenticated, is really a user of my app (i.e. they exist in my database). If not, I direct them to a signup page.
My class looks something like this:
public override IClaimsPrincipal Authenticate(string resourceName, IClaimsPrincipal incomingPrincipal)
{
if (incomingPrincipal.Identity.IsAuthenticated)
{
var identity = incomingPrincipal.Identity as IClaimsIdentity;
User user = null;
// Get name identifier and identity provider
var nameIdentifierClaim = identity.Claims.SingleOrDefault(c => c.ClaimType.Equals(ClaimTypes.NameIdentifier, StringComparison.OrdinalIgnoreCase));
var identityProviderClaim = identity.Claims.SingleOrDefault(c => c.ClaimType.Equals(CustomClaimTypes.IdentityProviderClaimType, StringComparison.OrdinalIgnoreCase));
if (nameIdentifierClaim == null || identityProviderClaim == null)
{
throw new AuthenticationErrorException("Invalid claims", "The claims provided by your Identity Provider are invalid. Please contact your administrator.");
}
try
{
//checking the database here...
using (var context = new CloudContext())
{
user = (from u in context.Users
where u.IdentityProvider == identityProviderClaim.Value &&
u.NameIdentifier == nameIdentifierClaim.Value &&
!u.Account.PendingDelete
select u).FirstOrDefault();
}
}
catch (System.Data.DataException ex)
{
Console.WriteLine(ex.Message);
if (ex.InnerException != null)
Console.WriteLine(ex.InnerException);
throw;
}
}
return incomingPrincipal;
}
Then, in your web.config, you add a section to the <microsoft.identitymodel> area, as so:
<claimsAuthenticationManager type="CloudAnalyzer.UI.Security.CloudAnalyzerClaimsAuthenticationManager" />
I learned this trick from the sample app located here: Windows Azure Marketplace. Even if you're not going to publish in the Window Azure Marketplace it's a good sample with some helpful code snippets you can use for ACS integration.

Resources