Last active
September 9, 2020 08:56
-
-
Save atom1cx/f5843f18b9b4f06e5177f68ad9e49d7d to your computer and use it in GitHub Desktop.
Xamarin Android & IOS Custom CA Trust
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//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