I want to implement client certificate authentication in my xamarin app. On top of that I am using a custom Certificate Authority (CA) and TLS 1.2.
Until now I managed to get it running using android, UWP and WPF. The only platform missing is ios.
Here is my NSUrlSessionDelegate:
public class SSLSessionDelegate : NSUrlSessionDelegate, INSUrlSessionDelegate { private NSUrlCredential Credential { get; set; } private SecIdentity identity = null; private X509Certificate2 ClientCertificate = null; private readonly SecCertificate CACertificate = null; public SSLSessionDelegate(byte[] caCert) : base() { if (caCert != null) { CACertificate = new SecCertificate(new X509Certificate2(caCert)); } } public void SetClientCertificate(byte[] pkcs12, char[] password) { if (pkcs12 != null) { ClientCertificate = new X509Certificate2(pkcs12, new string(password)); identity = SecIdentity.Import(ClientCertificate); SecCertificate certificate = new SecCertificate(ClientCertificate); SecCertificate[] certificates = { certificate }; Credential = NSUrlCredential.FromIdentityCertificatesPersistance(identity, certificates, NSUrlCredentialPersistence.ForSession); } else { ClientCertificate = null; identity = null; Credential = null; } } public override void DidReceiveChallenge(NSUrlSession session, NSUrlAuthenticationChallenge challenge, Action<NSUrlSessionAuthChallengeDisposition, NSUrlCredential> completionHandler) { if (challenge.ProtectionSpace.AuthenticationMethod == NSUrlProtectionSpace.AuthenticationMethodClientCertificate) { NSUrlCredential c = Credential; if (c != null) { completionHandler.Invoke(NSUrlSessionAuthChallengeDisposition.UseCredential, c); return; } } if (challenge.ProtectionSpace.AuthenticationMethod == NSUrlProtectionSpace.AuthenticationMethodServerTrust) { SecTrust secTrust = challenge.ProtectionSpace.ServerSecTrust; secTrust.SetAnchorCertificates(new SecCertificate[] { CACertificate }); secTrust.SetAnchorCertificatesOnly(true); } completionHandler.Invoke(NSUrlSessionAuthChallengeDisposition.PerformDefaultHandling, null); } } This works if no client certificate is configured DidReceiveChallenge is called once with AuthenticationMethodServerTrust and the custom CA is accepted.
But as soon as a client certificate is configured DidReceiveChallenge gets called 4 times (twice for each AuthenticationMethod) and I am getting NSURLErrorDomain (-1200) error.
Anyone any idea what I am doing wrong?
Update
The SSLSessionDelegate is used like this:
public class HttpsServer : AbstractRemoteServer, IRemoteServer { private static readonly Logger LOG = LogManager.GetLogger(); private SSLSessionDelegate sSLSessionDelegate; private NSUrlSession session; private NSUrl baseAddress; public HttpsServer() { sSLSessionDelegate = new SSLSessionDelegate(SSLSupport.GetTruststoreRaw()); NSUrlSessionConfiguration configuration = NSUrlSessionConfiguration.DefaultSessionConfiguration; configuration.HttpShouldSetCookies = true; configuration.TimeoutIntervalForRequest = 30; configuration.TLSMinimumSupportedProtocol = SslProtocol.Tls_1_2; configuration.TimeoutIntervalForResource = 30; NSMutableDictionary requestHeaders; if (configuration.HttpAdditionalHeaders != null) { requestHeaders = (NSMutableDictionary)configuration.HttpAdditionalHeaders.MutableCopy(); } else { requestHeaders = new NSMutableDictionary(); } AppendHeaders(requestHeaders, SSLSupport.GetDefaultHeaders()); configuration.HttpAdditionalHeaders = requestHeaders; session = NSUrlSession.FromConfiguration(configuration, (INSUrlSessionDelegate)sSLSessionDelegate, NSOperationQueue.MainQueue); baseAddress = NSUrl.FromString(SSLSupport.GetBaseAddress()); } public void SetClientCertificate(byte[] pkcs12, char[] password) { sSLSessionDelegate.SetClientCertificate(pkcs12, password); } public override async Task<string> GetString(string url, Dictionary<string, string> headers, CancellationToken cancellationToken) { NSData responseContent = await GetRaw(url, headers, cancellationToken); return NSString.FromData(responseContent, NSStringEncoding.UTF8).ToString(); } private async Task<NSData> GetRaw(string url, Dictionary<string, string> headers, CancellationToken cancellationToken) { NSMutableUrlRequest request = GetRequest(url); request.HttpMethod = "GET"; request.Headers = AppendHeaders(request.Headers, headers); Task<NSUrlSessionDataTaskRequest> taskRequest = session.CreateDataTaskAsync(request, out NSUrlSessionDataTask task); cancellationToken.Register(() => { if (task != null) { task.Cancel(); } }); try { task.Resume(); NSUrlSessionDataTaskRequest taskResponse = await taskRequest; if (taskResponse == null || taskResponse.Response == null) { throw new Exception(task.Error.Description); } else { NSHttpUrlResponse httpResponse = (NSHttpUrlResponse)taskResponse.Response; if (httpResponse.StatusCode == 303) { if (!httpResponse.AllHeaderFields.TryGetValue(new NSString("Location"), out NSObject locationValue)) { throw new Exception("redirect received without Location-header!"); } return await GetRaw(locationValue.ToString(), headers, cancellationToken); } if (httpResponse.StatusCode != 200) { throw new Exception("unsupported statuscode: " + httpResponse.Description); } return taskResponse.Data; } } catch (Exception ex) { throw new Exception("communication exception: " + ex.Message); } } } And here my Info.plist
<key>NSAppTransportSecurity</key> <dict> <key>NSExceptionDomains</key> <dict> <key>XXXXXXXXXX</key> <dict> <key>NSExceptionAllowsInsecureHTTPLoads</key> <true/> <key>NSIncludesSubdomains</key> <true/> </dict> </dict> </dict> Update 2
Neither I found the solution nor could anyone give me a hint, so I finally dropped client-certificates for now. I switched to OAuth2 for authorization and use my own certificate-authority (no self-signed certificate) for server -authentication which works well.
But still I am interested in this issue and glad for every idea in how to make it work.