diff --git a/.gitignore b/.gitignore index f5dc20d..ac2791b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ build/ *.userprefs build/ *.xam +*.suo \ No newline at end of file diff --git a/src/ModernHttpClient/Android/OkHttpNetworkHandler.cs b/src/ModernHttpClient/Android/OkHttpNetworkHandler.cs index de6d3c3..3355afa 100644 --- a/src/ModernHttpClient/Android/OkHttpNetworkHandler.cs +++ b/src/ModernHttpClient/Android/OkHttpNetworkHandler.cs @@ -13,6 +13,11 @@ using System.Globalization; using Android.OS; +// See http://chariotsolutions.com/blog/post/https-with-client-certificates-on/ +// for clue on Android client certs http://square.github.io/okhttp/javadoc/com/squareup/okhttp/CertificatePinner.html +using Java.Security; + + namespace ModernHttpClient { public class NativeMessageHandler : HttpClientHandler @@ -32,12 +37,44 @@ public class NativeMessageHandler : HttpClientHandler public NativeMessageHandler() : this(false, false) {} - public NativeMessageHandler(bool throwOnCaptiveNetwork, bool customSSLVerification, NativeCookieHandler cookieHandler = null) + public NativeMessageHandler( + bool throwOnCaptiveNetwork, bool customSSLVerification, + NativeCookieHandler cookieHandler = null, + byte[] pfxData = null, string pfxPassword = null) { this.throwOnCaptiveNetwork = throwOnCaptiveNetwork; if (customSSLVerification) client.SetHostnameVerifier(new HostnameVerifier()); noCacheCacheControl = (new CacheControl.Builder()).NoCache().Build(); + + if (pfxData != null && pfxData.Length > 0) { + var sslSocketFactory = createSSLSocketFactory(pfxData, pfxPassword); + client.SetSslSocketFactory(sslSocketFactory); + } + } + + private SSLSocketFactory createSSLSocketFactory(byte[] pfxData, string pfxPassword) + { + var sslSocketFactory = client.SslSocketFactory; + + try { + var stream = new System.IO.MemoryStream(pfxData); + KeyStore keyStore = KeyStore.GetInstance("PKCS12"); + keyStore.Load(stream, pfxPassword.ToCharArray()); + + KeyManagerFactory kmf = KeyManagerFactory.GetInstance("X509"); + kmf.Init(keyStore, pfxPassword.ToCharArray()); + IKeyManager[] keyManagers = kmf.GetKeyManagers(); + + SSLContext sslContext = SSLContext.GetInstance("TLS"); + sslContext.Init(keyManagers, null, null); + sslSocketFactory = sslContext.SocketFactory; + + } catch (Exception e) { + throw e; // The system has no TLS. Just give up. + } + + return sslSocketFactory; } public void RegisterForProgress(HttpRequestMessage request, ProgressDelegate callback) diff --git a/src/ModernHttpClient/ModernHttpClient.Android.csproj b/src/ModernHttpClient/ModernHttpClient.Android.csproj index 87271cd..52a5524 100644 --- a/src/ModernHttpClient/ModernHttpClient.Android.csproj +++ b/src/ModernHttpClient/ModernHttpClient.Android.csproj @@ -15,7 +15,6 @@ Assets False ModernHttpClient - v2.3 true diff --git a/src/ModernHttpClient/iOS/NSUrlSessionHandler.cs b/src/ModernHttpClient/iOS/NSUrlSessionHandler.cs index 7226c51..78d59dd 100644 --- a/src/ModernHttpClient/iOS/NSUrlSessionHandler.cs +++ b/src/ModernHttpClient/iOS/NSUrlSessionHandler.cs @@ -11,11 +11,14 @@ using System.Text.RegularExpressions; using ModernHttpClient.CoreFoundation; using ModernHttpClient.Foundation; +using System.Security; #if UNIFIED using Foundation; +using Security; #else using MonoTouch.Foundation; +using MonoTouch.Security; using System.Globalization; #endif @@ -52,11 +55,14 @@ public class NativeMessageHandler : HttpClientHandler public bool DisableCaching { get; set; } public NativeMessageHandler(): this(false, false) { } - public NativeMessageHandler(bool throwOnCaptiveNetwork, bool customSSLVerification, NativeCookieHandler cookieHandler = null) + public NativeMessageHandler( + bool throwOnCaptiveNetwork, bool customSSLVerification, + NativeCookieHandler cookieHandler = null, + byte[] pfxData = null, string pfxPassword = null) { session = NSUrlSession.FromConfiguration( NSUrlSessionConfiguration.DefaultSessionConfiguration, - new DataTaskDelegate(this), null); + new DataTaskDelegate(this, pfxData, pfxPassword), null); this.throwOnCaptiveNetwork = throwOnCaptiveNetwork; this.customSSLVerification = customSSLVerification; @@ -142,10 +148,16 @@ protected override async Task SendAsync(HttpRequestMessage class DataTaskDelegate : NSUrlSessionDataDelegate { NativeMessageHandler This { get; set; } + NSUrlCredential _credential; - public DataTaskDelegate(NativeMessageHandler that) + public DataTaskDelegate(NativeMessageHandler that, byte[] pfxData = null, string pfxPassword = null) { this.This = that; + + if (pfxData != null) + { + _credential = exportCredential(pfxData, pfxPassword); + } } public override void DidReceiveResponse(NSUrlSession session, NSUrlSessionDataTask dataTask, NSUrlResponse response, Action completionHandler) @@ -242,16 +254,71 @@ InflightOperation getResponseForTask(NSUrlSessionTask task) static readonly Regex cnRegex = new Regex(@"CN\s*=\s*([^,]*)", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.Singleline); + private static NSUrlCredential exportCredential(byte[] pfxData, string password = null) + { + NSDictionary[] items; + NSDictionary opt; + + if (String.IsNullOrEmpty(password)) { + opt = new NSDictionary(); + } else { + opt = NSDictionary.FromObjectsAndKeys( + new object[] { password }, new object[] { "passphrase" }); + } + + var status = SecImportExport.ImportPkcs12(pfxData, opt, out items); + + if (status == SecStatusCode.Success) + { + var identityRef = items[0]["identity"]; + + var identity = new SecIdentity(identityRef.Handle); + + SecCertificate[] certs = { identity.Certificate }; + + var credential = new NSUrlCredential(identity, certs, NSUrlCredentialPersistence.ForSession); + return credential; + } + + return null; + } + public override void DidReceiveChallenge(NSUrlSession session, NSUrlSessionTask task, NSUrlAuthenticationChallenge challenge, Action completionHandler) { if (!This.customSSLVerification) { goto doDefault; } - if (challenge.ProtectionSpace.AuthenticationMethod != "NSURLAuthenticationMethodServerTrust") { - goto doDefault; + // https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/URLLoadingSystem/Articles/AuthenticationChallenges.html\ + // For Client Certificate, + // http://stackoverflow.com/questions/21537203/ios-nsurlauthenticationmethodclientcertificate-not-requested-vs-activesync-serve + // http://www.sunethmendis.com/2013/01/11/certificate-based-client-authentication-in-ios/ + // https://forums.xamarin.com/discussion/39535/didreceivechallenge-issue-with-x509-certificates + switch (challenge.ProtectionSpace.AuthenticationMethod) + { + case "NSURLAuthenticationMethodServerTrust": + goto serverTrust; + case "NSURLAuthenticationMethodClientCertificate": + goto clientCert; + case "NSURLAuthenticationMethodHTTPBasic": + case "NSURLAuthenticationMethodHTTPDigest": + case "NSURLAuthenticationMethodNTLM": + default: + goto doDefault; } + clientCert: + if (_credential == null) + goto doDefault; + +#if UNIFIED + challenge.Sender.UseCredential(_credential, challenge); +#else + challenge.Sender.UseCredentials(_credential, challenge); +#endif + goto doDefault; + + serverTrust: if (ServicePointManager.ServerCertificateValidationCallback == null) { goto doDefault; }