From c213f5e9e2a354a512cd236461da13bf2e7aa574 Mon Sep 17 00:00:00 2001 From: Tim Uy Date: Fri, 19 Jun 2015 23:00:05 -0700 Subject: [PATCH 1/6] Add some notes on client cert for ios --- .../Resources/Resource.designer.cs | 20 +++++++++---------- .../iOS/NSUrlSessionHandler.cs | 10 ++++++++++ 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/ModernHttpClient/Resources/Resource.designer.cs b/src/ModernHttpClient/Resources/Resource.designer.cs index 19c6965..75c3896 100644 --- a/src/ModernHttpClient/Resources/Resource.designer.cs +++ b/src/ModernHttpClient/Resources/Resource.designer.cs @@ -1,15 +1,15 @@ #pragma warning disable 1591 -// ------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Mono Runtime Version: 4.0.30319.17020 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -// ------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.34014 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ -[assembly: Android.Runtime.ResourceDesignerAttribute("ModernHttpClient.Resource", IsApplication=false)] +[assembly: global::Android.Runtime.ResourceDesignerAttribute("ModernHttpClient.Resource", IsApplication=false)] namespace ModernHttpClient { diff --git a/src/ModernHttpClient/iOS/NSUrlSessionHandler.cs b/src/ModernHttpClient/iOS/NSUrlSessionHandler.cs index 7226c51..dd2ce5d 100644 --- a/src/ModernHttpClient/iOS/NSUrlSessionHandler.cs +++ b/src/ModernHttpClient/iOS/NSUrlSessionHandler.cs @@ -248,6 +248,16 @@ public override void DidReceiveChallenge(NSUrlSession session, NSUrlSessionTask goto doDefault; } + // According to https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/URLLoadingSystem/Articles/AuthenticationChallenges.html + // "Some" possibilities: + // NSURLAuthenticationMethodHTTPBasic + // NSURLAuthenticationMethodHTTPDigest + // NSURLAuthenticationMethodClientCertificate + // NSURLAuthenticationMethodServerTrust + + // TODO: Could it be all these things? Would we know it? (Answer is that it is either or) + // See http://stackoverflow.com/questions/21537203/ios-nsurlauthenticationmethodclientcertificate-not-requested-vs-activesync-serve + if (challenge.ProtectionSpace.AuthenticationMethod != "NSURLAuthenticationMethodServerTrust") { goto doDefault; } From 9efe6917a17a812a14a5bf532166c32002436de2 Mon Sep 17 00:00:00 2001 From: Tim Uy Date: Wed, 24 Jun 2015 16:37:47 -0700 Subject: [PATCH 2/6] Untested but 'should be working' iOS client cert, Android notes --- .../Android/OkHttpNetworkHandler.cs | 4 ++ .../iOS/NSUrlSessionHandler.cs | 63 +++++++++++++++---- 2 files changed, 56 insertions(+), 11 deletions(-) diff --git a/src/ModernHttpClient/Android/OkHttpNetworkHandler.cs b/src/ModernHttpClient/Android/OkHttpNetworkHandler.cs index de6d3c3..85ed907 100644 --- a/src/ModernHttpClient/Android/OkHttpNetworkHandler.cs +++ b/src/ModernHttpClient/Android/OkHttpNetworkHandler.cs @@ -13,6 +13,10 @@ 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 + + namespace ModernHttpClient { public class NativeMessageHandler : HttpClientHandler diff --git a/src/ModernHttpClient/iOS/NSUrlSessionHandler.cs b/src/ModernHttpClient/iOS/NSUrlSessionHandler.cs index dd2ce5d..3c33656 100644 --- a/src/ModernHttpClient/iOS/NSUrlSessionHandler.cs +++ b/src/ModernHttpClient/iOS/NSUrlSessionHandler.cs @@ -14,6 +14,7 @@ #if UNIFIED using Foundation; +using Security; #else using MonoTouch.Foundation; using System.Globalization; @@ -50,6 +51,7 @@ public class NativeMessageHandler : HttpClientHandler readonly bool customSSLVerification; public bool DisableCaching { get; set; } + public X509Certificate2 ClientCertificate { get; set; } public NativeMessageHandler(): this(false, false) { } public NativeMessageHandler(bool throwOnCaptiveNetwork, bool customSSLVerification, NativeCookieHandler cookieHandler = null) @@ -242,26 +244,65 @@ InflightOperation getResponseForTask(NSUrlSessionTask task) static readonly Regex cnRegex = new Regex(@"CN\s*=\s*([^,]*)", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.Singleline); + private NSUrlCredential exportCredential(byte[] pfxData, string password) + { + NSDictionary[] items; + NSDictionary 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; } - // According to https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/URLLoadingSystem/Articles/AuthenticationChallenges.html - // "Some" possibilities: - // NSURLAuthenticationMethodHTTPBasic - // NSURLAuthenticationMethodHTTPDigest - // NSURLAuthenticationMethodClientCertificate - // NSURLAuthenticationMethodServerTrust - - // TODO: Could it be all these things? Would we know it? (Answer is that it is either or) - // See http://stackoverflow.com/questions/21537203/ios-nsurlauthenticationmethodclientcertificate-not-requested-vs-activesync-serve + // 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; + } - if (challenge.ProtectionSpace.AuthenticationMethod != "NSURLAuthenticationMethodServerTrust") { + clientCert: + if (This.ClientCertificate == null) goto doDefault; - } + var cert = new SecCertificate(This.ClientCertificate); + + + var credential = exportCredential(cert); + challenge.Sender.UseCredential(credential, challenge); + + goto doDefault; + + serverTrust: if (ServicePointManager.ServerCertificateValidationCallback == null) { goto doDefault; } From 68876ead5ba7f4e2aeed2638d23599c762322cef Mon Sep 17 00:00:00 2001 From: Tim Uy Date: Wed, 24 Jun 2015 17:28:45 -0700 Subject: [PATCH 3/6] Clean-up iOS client cert code --- .../iOS/NSUrlSessionHandler.cs | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/ModernHttpClient/iOS/NSUrlSessionHandler.cs b/src/ModernHttpClient/iOS/NSUrlSessionHandler.cs index 3c33656..5c5b2d3 100644 --- a/src/ModernHttpClient/iOS/NSUrlSessionHandler.cs +++ b/src/ModernHttpClient/iOS/NSUrlSessionHandler.cs @@ -11,12 +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 @@ -51,7 +53,8 @@ public class NativeMessageHandler : HttpClientHandler readonly bool customSSLVerification; public bool DisableCaching { get; set; } - public X509Certificate2 ClientCertificate { get; set; } + public byte[] PfxData { get; set; } + public string PfxPassword { get; set; } public NativeMessageHandler(): this(false, false) { } public NativeMessageHandler(bool throwOnCaptiveNetwork, bool customSSLVerification, NativeCookieHandler cookieHandler = null) @@ -244,10 +247,17 @@ InflightOperation getResponseForTask(NSUrlSessionTask task) static readonly Regex cnRegex = new Regex(@"CN\s*=\s*([^,]*)", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.Singleline); - private NSUrlCredential exportCredential(byte[] pfxData, string password) + private static NSUrlCredential exportCredential(byte[] pfxData, string password = null) { NSDictionary[] items; - NSDictionary opt = NSDictionary.FromObjectsAndKeys(new object[] { password }, new object[] { "passphrase" }); + 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); @@ -291,14 +301,11 @@ public override void DidReceiveChallenge(NSUrlSession session, NSUrlSessionTask } clientCert: - if (This.ClientCertificate == null) + if (This.PfxData == null) goto doDefault; - var cert = new SecCertificate(This.ClientCertificate); - - - var credential = exportCredential(cert); - challenge.Sender.UseCredential(credential, challenge); + var credential = exportCredential(This.PfxData, This.PfxPassword); + challenge.Sender.UseCredentials(credential, challenge); goto doDefault; From 64f4e303bba1e494a83e018c910e063a80c34de1 Mon Sep 17 00:00:00 2001 From: Tim Uy Date: Wed, 24 Jun 2015 19:28:44 -0700 Subject: [PATCH 4/6] Additional client cert tweaks to get things compiling --- .../Android/OkHttpNetworkHandler.cs | 35 ++++++++++++++++++- .../ModernHttpClient.Android.csproj | 1 - .../Resources/Resource.designer.cs | 20 +++++------ .../iOS/NSUrlSessionHandler.cs | 5 ++- 4 files changed, 48 insertions(+), 13 deletions(-) diff --git a/src/ModernHttpClient/Android/OkHttpNetworkHandler.cs b/src/ModernHttpClient/Android/OkHttpNetworkHandler.cs index 85ed907..3355afa 100644 --- a/src/ModernHttpClient/Android/OkHttpNetworkHandler.cs +++ b/src/ModernHttpClient/Android/OkHttpNetworkHandler.cs @@ -15,6 +15,7 @@ // 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 @@ -36,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/Resources/Resource.designer.cs b/src/ModernHttpClient/Resources/Resource.designer.cs index 75c3896..19c6965 100644 --- a/src/ModernHttpClient/Resources/Resource.designer.cs +++ b/src/ModernHttpClient/Resources/Resource.designer.cs @@ -1,15 +1,15 @@ #pragma warning disable 1591 -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.34014 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ +// ------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Mono Runtime Version: 4.0.30319.17020 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// ------------------------------------------------------------------------------ -[assembly: global::Android.Runtime.ResourceDesignerAttribute("ModernHttpClient.Resource", IsApplication=false)] +[assembly: Android.Runtime.ResourceDesignerAttribute("ModernHttpClient.Resource", IsApplication=false)] namespace ModernHttpClient { diff --git a/src/ModernHttpClient/iOS/NSUrlSessionHandler.cs b/src/ModernHttpClient/iOS/NSUrlSessionHandler.cs index 5c5b2d3..8e5d8ff 100644 --- a/src/ModernHttpClient/iOS/NSUrlSessionHandler.cs +++ b/src/ModernHttpClient/iOS/NSUrlSessionHandler.cs @@ -305,8 +305,11 @@ public override void DidReceiveChallenge(NSUrlSession session, NSUrlSessionTask goto doDefault; var credential = exportCredential(This.PfxData, This.PfxPassword); +#if UNIFIED + challenge.Sender.UseCredential(credential, challenge); +#else challenge.Sender.UseCredentials(credential, challenge); - +#endif goto doDefault; serverTrust: From 43350d870b29d41ea1a6a8b8a4fb293d53c0a4a4 Mon Sep 17 00:00:00 2001 From: Tim Uy Date: Thu, 25 Jun 2015 11:13:17 -0700 Subject: [PATCH 5/6] Add suo to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) 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 From 52e235240bd44d14f521704236a90178941cebb2 Mon Sep 17 00:00:00 2001 From: Tim Uy Date: Thu, 25 Jun 2015 12:28:21 -0700 Subject: [PATCH 6/6] Match the interface between Android and iOS --- .../iOS/NSUrlSessionHandler.cs | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/ModernHttpClient/iOS/NSUrlSessionHandler.cs b/src/ModernHttpClient/iOS/NSUrlSessionHandler.cs index 8e5d8ff..78d59dd 100644 --- a/src/ModernHttpClient/iOS/NSUrlSessionHandler.cs +++ b/src/ModernHttpClient/iOS/NSUrlSessionHandler.cs @@ -53,15 +53,16 @@ public class NativeMessageHandler : HttpClientHandler readonly bool customSSLVerification; public bool DisableCaching { get; set; } - public byte[] PfxData { get; set; } - public string PfxPassword { 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; @@ -147,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) @@ -301,14 +308,13 @@ public override void DidReceiveChallenge(NSUrlSession session, NSUrlSessionTask } clientCert: - if (This.PfxData == null) + if (_credential == null) goto doDefault; - var credential = exportCredential(This.PfxData, This.PfxPassword); #if UNIFIED - challenge.Sender.UseCredential(credential, challenge); + challenge.Sender.UseCredential(_credential, challenge); #else - challenge.Sender.UseCredentials(credential, challenge); + challenge.Sender.UseCredentials(_credential, challenge); #endif goto doDefault;