When I first released my open-source certificate transparency library for Android and the JVM, I provided mechanisms to cover the majority of network connections, namely OkHttp, HttpURLConnection and Volley. However, there was no support for Android WebViews.
If you want to learn more about certificate transparency, read Android Security: Certificate Transparency.
In this article, we explore the issues surrounding WebView support. We look at how to overcome this to implement certificate transparency checks in WebViews with a single line of code (and a bit of magic in the library):
installCertificateTransparencyProvider()
The Problem with WebViews
The Status of Certificate Transparency in Android Webview makes it seem unlikely WebViews will have certificate transparency checks enabled any time soon, regardless of the support built into Chromium.
We need to find a way to perform these checks by intercepting the SSL handshaking of all the network connections and terminating if necessary.
However, on the surface, with WebViews, this doesn’t seem possible. The best you can hope for is to override shouldInterceptRequest
in a WebViewClient, which only allows you to handle GET requests, there is no access to the body of POST requests. If you were to go down this route, you would make the network calls yourself using something like OkHttp and certificate transparency checks could then be enabled with the libraries OkHttp network interceptor.
TrustManager factories
Fortunately, there is a lower-level way to intercept the SSL handshaking process, even in WebViews.
Under the hood making a secure network connection uses an X509TrustManagerand an
SSLSocketFactory. It is the
X509TrustManager that performs checks on the SSL certificates of the connection. We can use this to add additional certificate transparency checks.
A typical implementation might look like the following; indeed, this matches the defaults in OkHttp:
val trustManagerFactory = TrustManagerFactory.getInstance( TrustManagerFactory.getDefaultAlgorithm() ) trustManagerFactory.init(null as KeyStore?) val x509TrustManager = trustManagerFactory .trustManagers .filterIsInstance<X509TrustManager>() .first() val sslContext = SSLContext.getInstance("SSL") sslContext.init(null, arrayOf(x509TrustManager), null) val sslSocketFactory = sslContext.socketFactory
The SSLSocketFactory
is then applied to the network connection, whether using HttpsURLConnection or OkHttp.
Interestingly, WebView
uses TrustManagerFactory
, as seen in the source code.
Introduction to Java security
TrustManagerFactory
is part of the Java Security API, a plugin-based system providing cryptographic and public key infrastructure (PKI) interfaces that form the underlying basis for developing secure applications.
When we call TrustManagerFactory.getInstance(algorithm)
Java security finds the highest-priority security provider that can generate a TrustManagerFactory
for the required algorithm.
We need to create our provider and register it with the highest priority.
If you ever wondered how network security configuration on Android worked with WebViews, well, this is how.
Creating a TrustManagerFactory
Creating a custom TrustManagerFactory
is as simple as creating a class that implements TrustManagerFactorySpi
(Spi = Service provider interface).
public abstract class TrustManagerFactorySpi { protected abstract void engineInit(KeyStore ks); protected abstract void engineInit(ManagerFactoryParameters spec); protected abstract TrustManager[] engineGetTrustManagers(); }
Java uses reflection to create an instance of our factory which means if we need any data for initialisation, we cannot use constructor parameters. So while the provider can receive an instance of ManagerFactoryParameters
through engineInit(…)
we don’t have control of the calls to TrustManagerFactory.getInstance(…)
, which would provide this data.
Effectively this means if we need any additional data in our class, we have to use a singleton. Indeed, network security config uses this technique in places.
The gist of our TrustManagerFactory
is to wrap around the system default TrustManagerFactory
providing the additional certificate transparency checks. The certificate transparency library already has support to wrap a X509TrustManager.
The code for our TrustManagerFactory
is here. We store the system factory instance we delegate to in a singleton.
Job Offers
Installing a provider
Creating a provider is a simple enough process. We implement the Provider
abstract class and initialise it with what our Provider
provides. You can see the mention of PKIX
, which stands for public-key infrastructure and is the default algorithm on Android. i.e. calling TrustManagerFactory.getDefaultAlgorithm()
returns PKIX
. We additionally map X509
to the same TrustManagerFactory
class for completeness.
class CTProvider : Provider("CT Provider", 1.0, "") { init { put("TrustManagerFactory.PKIX", CTTrustManagerFactory::class.java.name) put("Alg.Alias.TrustManagerFactory.X509", "PKIX") } }
Then to activate our provider, all we need to do is create an instance and insert it in our preferred position. The position is 1-based; 1
is most preferred, followed by 2
, etc.
Security.insertProviderAt(provider, 1)
Our provider is now the highest priority and will be used by default when someone calls TrustManagerFactory.getInstance(…)
for the PKIX
or X509
algorithms.
Conclusions
The certificate transparency library exposes all the above into a single line of code:
installCertificateTransparencyProvider()
Internally it installs a security provider which provides a custom TrustManagerFactory
that wraps the system default — this means your network security configuration is still applied, i.e. the certificate transparency checks are in addition to the security the platform provides.
Join medium to read all of my articles or subscribe for e-mail updates.
This article was originally published on proandroiddev.com on March 06, 2022