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?
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>
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.
I would suggest using ModernHttpClient. It supports ClientCertificates for Android and iOS. It's opensource so you could always check out their github for reference if you want to finish your own implementation.
ModernHttpClient
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With