Skip to content

Instantly share code, notes, and snippets.

@atom1cx
Last active September 9, 2020 08:56
Show Gist options
  • Save atom1cx/f5843f18b9b4f06e5177f68ad9e49d7d to your computer and use it in GitHub Desktop.
Save atom1cx/f5843f18b9b4f06e5177f68ad9e49d7d to your computer and use it in GitHub Desktop.
Xamarin Android & IOS Custom CA Trust
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
public class DroidTlsClientHandler : AndroidClientHandler
{
private TrustManagerFactory _trustManagerFactory;
private KeyManagerFactory _keyManagerFactory;
private KeyStore _keyStore;
protected override TrustManagerFactory ConfigureTrustManagerFactory(KeyStore keyStore)
{
if (_trustManagerFactory != null)
{
return _trustManagerFactory;
}
_trustManagerFactory = TrustManagerFactory
.GetInstance(TrustManagerFactory.DefaultAlgorithm);
_trustManagerFactory.Init(keyStore);
return _trustManagerFactory;
}
protected override KeyManagerFactory ConfigureKeyManagerFactory(KeyStore keyStore)
{
if (_keyManagerFactory != null)
{
return _keyManagerFactory;
}
_keyManagerFactory = KeyManagerFactory
.GetInstance(KeyManagerFactory.DefaultAlgorithm);
_keyManagerFactory.Init(keyStore, null);
return _keyManagerFactory;
}
protected override KeyStore ConfigureKeyStore(KeyStore keyStore)
{
if (_keyStore != null)
{
return _keyStore;
}
_keyStore = KeyStore.GetInstance(KeyStore.DefaultType);
_keyStore.Load(null, null);
KeyStore androidCAKs = KeyStore.GetInstance("AndroidCAStore");
if (androidCAKs != null)
{
androidCAKs.Load(null, null);
var aliases = androidCAKs.Aliases();
while (aliases.HasMoreElements)
{
String alias = (String)aliases.NextElement();
var caCertFromAndroid = androidCAKs.GetCertificate(alias);
_keyStore.SetCertificateEntry(alias, caCertFromAndroid);
}
}
CertificateFactory cff = CertificateFactory.GetInstance("X.509");
Certificate cert;
using (Stream certStream = Android.App.Application.Context.Assets.Open("OurCAPK.pem"))
{
cert = cff.GenerateCertificate(certStream);
}
_keyStore.SetCertificateEntry("OurTrustedCert", cert);
return _keyStore;
}
}
//Modified part of https://github.com/xamarin/xamarin-macios/blob/main/src/Foundation/NSUrlSessionHandler.cs
public NSData OurCA { get; set; }
...
[Preserve(Conditional = true)]
public override void DidReceiveChallenge(NSUrlSession session, NSUrlSessionTask task, NSUrlAuthenticationChallenge challenge, Action<NSUrlSessionAuthChallengeDisposition, NSUrlCredential> completionHandler)
{
var inflight = GetInflightData(task);
if (inflight == null)
return;
// ToCToU for the callback
var trustCallback = sessionHandler.TrustOverride;
var trustCallbackForUrl = sessionHandler.TrustOverrideForUrl;
var hasCallBack = trustCallback != null || trustCallbackForUrl != null;
if (hasCallBack && challenge.ProtectionSpace.AuthenticationMethod == NSUrlProtectionSpace.AuthenticationMethodServerTrust)
{
// if one of the delegates allows to ignore the cert, do it. We check first the one that takes the url because is more precisse, later the
// more general one. Since we are using nullables, if the delegate is not present, by default is false
var trustSec = (trustCallbackForUrl?.Invoke(sessionHandler, inflight.RequestUrl, challenge.ProtectionSpace.ServerSecTrust) ?? false) ||
(trustCallback?.Invoke(sessionHandler, challenge.ProtectionSpace.ServerSecTrust) ?? false);
if (trustSec)
{
var credential = new NSUrlCredential(challenge.ProtectionSpace.ServerSecTrust);
completionHandler(NSUrlSessionAuthChallengeDisposition.UseCredential, credential);
}
else
{
// user callback rejected the certificate, we want to set the exception, else the user will
// see as if the request was cancelled.
lock (inflight.Lock)
{
inflight.Exception = new HttpRequestException("An error occurred while sending the request.", new WebException("Error: TrustFailure"));
}
completionHandler(NSUrlSessionAuthChallengeDisposition.CancelAuthenticationChallenge, null);
}
return;
}
// case for the basic auth failing up front. As per apple documentation:
// The URL Loading System is designed to handle various aspects of the HTTP protocol for you. As a result, you should not modify the following headers using
// the addValue(_:forHTTPHeaderField:) or setValue(_:forHTTPHeaderField:) methods:
// Authorization
// Connection
// Host
// Proxy-Authenticate
// Proxy-Authorization
// WWW-Authenticate
// but we are hiding such a situation from our users, we can nevertheless know if the header was added and deal with it. The idea is as follows,
// check if we are in the first attempt, if we are (PreviousFailureCount == 0), we check the headers of the request and if we do have the Auth
// header, it means that we do not have the correct credentials, in any other case just do what it is expected.
if (challenge.PreviousFailureCount == 0)
{
var authHeader = inflight.Request?.Headers?.Authorization;
if (!(string.IsNullOrEmpty(authHeader?.Scheme) && string.IsNullOrEmpty(authHeader?.Parameter)))
{
completionHandler(NSUrlSessionAuthChallengeDisposition.RejectProtectionSpace, null);
return;
}
}
if (sessionHandler.Credentials != null && TryGetAuthenticationType(challenge.ProtectionSpace, out string authType))
{
NetworkCredential credentialsToUse = null;
if (authType != RejectProtectionSpaceAuthType)
{
// interesting situation, when we use a credential that we created that is empty, we are not getting the RejectProtectionSpaceAuthType,
// nevertheless, we need to check is not the first challenge we will continue trusting the
// TryGetAuthenticationType method, but we will also check that the status response in not a 401
// look like we do get an exception from the credentials db:
// TestiOSHttpClient[28769:26371051] CredStore - performQuery - Error copying matching creds. Error=-25300, query={
// class = inet;
// "m_Limit" = "m_LimitAll";
// ptcl = htps;
// "r_Attributes" = 1;
// sdmn = test;
// srvr = "jigsaw.w3.org";
// sync = syna;
// }
// do remember that we ALWAYS get a challenge, so the failure count has to be 1 or more for this check, 1 would be the first time
var nsurlRespose = challenge.FailureResponse as NSHttpUrlResponse;
var responseIsUnauthorized = (nsurlRespose == null) ? false : nsurlRespose.StatusCode == (int)HttpStatusCode.Unauthorized && challenge.PreviousFailureCount > 0;
if (!responseIsUnauthorized)
{
var uri = inflight.Request.RequestUri;
credentialsToUse = sessionHandler.Credentials.GetCredential(uri, authType);
}
}
if (credentialsToUse != null)
{
var credential = new NSUrlCredential(credentialsToUse.UserName, credentialsToUse.Password, NSUrlCredentialPersistence.ForSession);
completionHandler(NSUrlSessionAuthChallengeDisposition.UseCredential, credential);
}
else
{
// Rejecting the challenge allows the next authentication method in the request to be delivered to
// the DidReceiveChallenge method. Another authentication method may have credentials available.
completionHandler(NSUrlSessionAuthChallengeDisposition.RejectProtectionSpace, null);
}
}
else
{
// Trust our CA if given
//https://developer.apple.com/documentation/security/certificate_key_and_trust_services/trust/configuring_a_trust
if (sessionHandler.OurCA != null)
{
var trust = challenge.ProtectionSpace.ServerSecTrust;
var rootCaData = sessionHandler.OurCA;
var ourCustomAnchor = new SecCertificate(rootCaData);
trust.SetAnchorCertificates(new[] { ourCustomAnchor });
trust.SetAnchorCertificatesOnly(false);
}
completionHandler(NSUrlSessionAuthChallengeDisposition.PerformDefaultHandling, challenge.ProposedCredential);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment