Commit c52e8395 authored by Ricki Hirner's avatar Ricki Hirner 🐑

Update libraries, Kotlin tests, minor refactoring

parent 0d12bf44
Pipeline #18765988 passed with stages
buildscript {
ext.kotlin_version = '1.2.21'
ext.kotlin_version = '1.2.30'
ext.dokka_version = '0.9.15'
repositories {
......@@ -46,8 +46,8 @@ android {
dependencies {
compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
compile 'com.android.support:appcompat-v7:27.0.2'
compile 'com.android.support:cardview-v7:27.0.2'
compile 'com.android.support:appcompat-v7:27.1.0'
compile 'com.android.support:cardview-v7:27.1.0'
androidTestCompile 'com.android.support.test:runner:1.0.1'
androidTestCompile 'com.android.support.test:rules:1.0.1'
......
/*
* Copyright © Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.cert4android;
import android.content.Intent;
import android.os.IBinder;
import android.os.Messenger;
import android.support.test.rule.ServiceTestRule;
import android.support.test.runner.AndroidJUnit4;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.IOException;
import java.net.URL;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.concurrent.TimeoutException;
import javax.net.ssl.HttpsURLConnection;
import static android.support.test.InstrumentationRegistry.getContext;
import static android.support.test.InstrumentationRegistry.getTargetContext;
import static org.junit.Assert.assertNotNull;
@RunWith(AndroidJUnit4.class)
public class CustomCertManagerTest {
CustomCertManager certManager, paranoidCertManager;
static {
CustomCertManager.SERVICE_TIMEOUT = 1000;
}
@Rule
public ServiceTestRule serviceTestRule = new ServiceTestRule();
Messenger service;
private static X509Certificate[] siteCerts;
static {
try {
siteCerts = getSiteCertificates(new URL("https://www.davdroid.com"));
} catch(IOException ignored) {
}
assertNotNull(siteCerts);
}
@Before
public void initCertManager() throws TimeoutException, InterruptedException {
// prepare a bound and ready service for testing
// loop required because of https://code.google.com/p/android/issues/detail?id=180396
IBinder binder = bindService(CustomCertService.class);
assertNotNull(binder);
CustomCertManager.resetCertificates(getContext());
certManager = new CustomCertManager(getContext(), false);
assertNotNull(certManager);
paranoidCertManager = new CustomCertManager(getContext(), false, false);
assertNotNull(paranoidCertManager);
}
@After
public void closeCertManager() {
paranoidCertManager.close();
certManager.close();
}
@Test(expected = CertificateException.class)
public void testCheckClientCertificate() throws CertificateException {
certManager.checkClientTrusted(null, null);
}
@Test
public void testTrustedCertificate() throws CertificateException, TimeoutException {
certManager.checkServerTrusted(siteCerts, "RSA");
}
@Test(expected = CertificateException.class)
public void testParanoidCertificate() throws CertificateException {
paranoidCertManager.checkServerTrusted(siteCerts, "RSA");
}
@Test
public void testAddCustomCertificate() throws CertificateException, TimeoutException, InterruptedException {
addCustomCertificate();
paranoidCertManager.checkServerTrusted(siteCerts, "RSA");
}
// fails randomly for unknown reason:
@Test(expected = CertificateException.class)
public void testRemoveCustomCertificate() throws CertificateException, TimeoutException, InterruptedException {
addCustomCertificate();
// remove certificate and check again
// should now be rejected for the whole session, i.e. no timeout anymore
Intent intent = new Intent(getContext(), CustomCertService.class);
intent.setAction(CustomCertService.CMD_CERTIFICATION_DECISION);
intent.putExtra(CustomCertService.EXTRA_CERTIFICATE, siteCerts[0].getEncoded());
intent.putExtra(CustomCertService.EXTRA_TRUSTED, false);
startService(intent, CustomCertService.class);
paranoidCertManager.checkServerTrusted(siteCerts, "RSA");
}
private void addCustomCertificate() throws CertificateException, TimeoutException, InterruptedException {
// add certificate and check again
Intent intent = new Intent(getContext(), CustomCertService.class);
intent.setAction(CustomCertService.CMD_CERTIFICATION_DECISION);
intent.putExtra(CustomCertService.EXTRA_CERTIFICATE, siteCerts[0].getEncoded());
intent.putExtra(CustomCertService.EXTRA_TRUSTED, true);
startService(intent, CustomCertService.class);
}
private IBinder bindService(Class clazz) throws TimeoutException, InterruptedException {
IBinder binder = null;
int it = 0;
while ((binder = serviceTestRule.bindService(new Intent(getTargetContext(), clazz))) == null && it++ <100) {
System.err.println("Waiting for ServiceTestRule.bindService");
Thread.sleep(50);
}
if (binder == null)
throw new IllegalStateException("Couldn't bind to service");
return binder;
}
private void startService(Intent intent, Class clazz) throws TimeoutException, InterruptedException {
serviceTestRule.startService(intent);
bindService(clazz);
}
private static X509Certificate[] getSiteCertificates(URL url) throws IOException {
HttpsURLConnection conn = (HttpsURLConnection)url.openConnection();
try {
conn.getInputStream().read();
Certificate[] certs = conn.getServerCertificates();
X509Certificate[] x509 = new X509Certificate[certs.length];
System.arraycopy(certs, 0, x509, 0, certs.length);
return x509;
} finally {
conn.disconnect();
}
}
}
/*
* Copyright © Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.cert4android
import android.app.Service
import android.content.Intent
import android.os.IBinder
import android.support.test.InstrumentationRegistry.getContext
import android.support.test.InstrumentationRegistry.getTargetContext
import android.support.test.rule.ServiceTestRule
import org.junit.After
import org.junit.Assert.assertNotNull
import org.junit.Assume.assumeNotNull
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import java.io.IOException
import java.net.URL
import java.security.cert.CertificateException
import java.security.cert.X509Certificate
import javax.net.ssl.HttpsURLConnection
class CustomCertManagerTest {
companion object {
private fun getSiteCertificates(url: URL): List<X509Certificate> {
val conn = url.openConnection() as HttpsURLConnection
try {
conn.inputStream.read()
val certs = mutableListOf<X509Certificate>()
conn.serverCertificates.forEach { certs += it as X509Certificate }
return certs
} finally {
conn.disconnect()
}
}
}
lateinit var certManager: CustomCertManager
lateinit var paranoidCertManager: CustomCertManager
init {
CustomCertManager.SERVICE_TIMEOUT = 1000
}
@JvmField
@Rule
val serviceTestRule = ServiceTestRule()
var siteCerts: List<X509Certificate>? = null
init {
try {
siteCerts = getSiteCertificates(URL("https://www.davdroid.com"))
} catch(e: IOException) {
}
assumeNotNull(siteCerts)
}
@Before
fun initCertManager() {
// prepare a bound and ready service for testing
// loop required because of https://code.google.com/p/android/issues/detail?id=180396
val binder = bindService(CustomCertService::class.java)
assertNotNull(binder)
CustomCertManager.resetCertificates(getContext())
certManager = CustomCertManager(getContext(), false)
assertNotNull(certManager)
paranoidCertManager = CustomCertManager(getContext(), false, false)
assertNotNull(paranoidCertManager)
}
@After
fun closeCertManager() {
paranoidCertManager.close()
certManager.close()
}
@Test(expected = CertificateException::class)
fun testCheckClientCertificate() {
certManager.checkClientTrusted(null, null)
}
@Test
fun testTrustedCertificate() {
certManager.checkServerTrusted(siteCerts!!.toTypedArray(), "RSA")
}
@Test(expected = CertificateException::class)
fun testParanoidCertificate() {
paranoidCertManager.checkServerTrusted(siteCerts!!.toTypedArray(), "RSA")
}
@Test
fun testAddCustomCertificate() {
addCustomCertificate()
paranoidCertManager.checkServerTrusted(siteCerts!!.toTypedArray(), "RSA")
}
// fails randomly for unknown reason:
@Test(expected = CertificateException::class)
fun testRemoveCustomCertificate() {
addCustomCertificate()
// remove certificate and check again
// should now be rejected for the whole session, i.e. no timeout anymore
val intent = Intent(getContext(), CustomCertService::class.java)
intent.action = CustomCertService.CMD_CERTIFICATION_DECISION
intent.putExtra(CustomCertService.EXTRA_CERTIFICATE, siteCerts!!.first().encoded)
intent.putExtra(CustomCertService.EXTRA_TRUSTED, false)
startService(intent, CustomCertService::class.java)
paranoidCertManager.checkServerTrusted(siteCerts!!.toTypedArray(), "RSA")
}
private fun addCustomCertificate() {
// add certificate and check again
val intent = Intent(getContext(), CustomCertService::class.java)
intent.action = CustomCertService.CMD_CERTIFICATION_DECISION
intent.putExtra(CustomCertService.EXTRA_CERTIFICATE, siteCerts!!.first().encoded)
intent.putExtra(CustomCertService.EXTRA_TRUSTED, true)
startService(intent, CustomCertService::class.java)
}
private fun bindService(clazz: Class<out Service>): IBinder {
var binder = serviceTestRule.bindService(Intent(getTargetContext(), clazz))
var it = 0
while (binder == null && it++ <100) {
binder = serviceTestRule.bindService(Intent(getTargetContext(), clazz))
System.err.println("Waiting for ServiceTestRule.bindService")
Thread.sleep(50)
}
if (binder == null)
throw IllegalStateException("Couldn't bind to service")
return binder
}
private fun startService(intent: Intent, clazz: Class<out Service>) {
serviceTestRule.startService(intent)
bindService(clazz)
}
}
......@@ -17,7 +17,6 @@ import javax.net.ssl.X509TrustManager
object CertUtils {
@JvmStatic
fun getTrustManager(keyStore: KeyStore?): X509TrustManager? {
try {
val tmf = TrustManagerFactory.getInstance("X509")
......@@ -31,7 +30,6 @@ object CertUtils {
return null
}
@JvmStatic
fun getTag(cert: X509Certificate): String {
val str = StringBuilder()
for (b in cert.signature)
......
......@@ -14,9 +14,8 @@ import java.util.logging.Logger
object Constants {
val TAG = "cert4android"
const val TAG = "cert4android"
@JvmField
var log: Logger = Logger.getLogger(TAG)
init {
log.level = if (Log.isLoggable(TAG, Log.VERBOSE))
......@@ -25,7 +24,6 @@ object Constants {
Level.INFO
}
@JvmField
val NOTIFICATION_CERT_DECISION = 88809
const val NOTIFICATION_CERT_DECISION = 88809
}
......@@ -30,9 +30,17 @@ import javax.net.ssl.X509TrustManager
* each of them with an own [CustomCertManager], want to access a synchronized central
* certificate trust store + UI (for accepting certificates etc.).
*
* @param context used to bind to [CustomCertService]
* @param interactive true: users will be notified in case of unknown certificates;
* false: unknown certificates will be rejected (only uses custom certificate key store)
* @param trustSystemCerts whether system certificates will be trusted
* @param trustSystemCerts whether system certificates will be trusted
*
* @constructor Creates a new instance, using a certain [CustomCertService] messenger (for testing).
* Must not be run from the main thread because this constructor may request binding to [CustomCertService].
* The actual binding code is called by the looper in the main thread, so waiting for the
* service would block forever.
*
* @throws IllegalStateException if run from main thread
*/
class CustomCertManager @JvmOverloads constructor(
val context: Context,
......@@ -43,10 +51,8 @@ class CustomCertManager @JvmOverloads constructor(
companion object {
/** how long to wait for a decision from [CustomCertService] before giving up temporarily */
@JvmField
var SERVICE_TIMEOUT: Long = 3*60*1000
@JvmStatic
fun resetCertificates(context: Context): Boolean {
val intent = Intent(context, CustomCertService::class.java)
intent.action = CustomCertService.CMD_RESET_CERTIFICATES
......@@ -64,22 +70,10 @@ class CustomCertManager @JvmOverloads constructor(
if (trustSystemCerts) CertUtils.getTrustManager(null) else null
/** Whether to launch {@link TrustCertificateActivity} directly. The notification will always be shown. */
@JvmField
/** Whether to launch [TrustCertificateActivity] directly. The notification will always be shown. */
var appInForeground = false
/**
* Creates a new instance, using a certain [CustomCertService] messenger (for testing).
* Must not be run from the main thread because this constructor may request binding to [CustomCertService].
* The actual binding code is called by the looper in the main thread, so waiting for the
* service would block forever.
*
* @param context used to bind to [CustomCertService]
* @param interactive whether calls to [CustomCertService] are flagged as interactive (which allows the user to accept/deny certificates)
* @param trustSystemCerts whether to trust system/user-installed CAs (default trust store)
* @throws IllegalStateException if run from main thread
*/
init {
serviceConnection = object: ServiceConnection {
override fun onServiceConnected(className: ComponentName, binder: IBinder) {
......@@ -180,7 +174,6 @@ class CustomCertManager @JvmOverloads constructor(
}
}
val id: Int
try {
svc.checkTrusted(cert.encoded, interactive, appInForeground, callback)
synchronized(lock) {
......@@ -222,7 +215,7 @@ class CustomCertManager @JvmOverloads constructor(
): HostnameVerifier {
override fun verify(host: String, sslSession: SSLSession): Boolean {
Constants.log.fine("Verifying certificate for " + host)
Constants.log.fine("Verifying certificate for $host")
if (defaultVerifier?.verify(host, sslSession) == true)
return true
......
......@@ -26,21 +26,34 @@ import java.util.*
import java.util.logging.Level
import javax.net.ssl.X509TrustManager
/**
* The service which manages the certificates. Communications with
* the [CustomCertManager]s over IPC.
*
* This services is both a started and a bound service.
*/
class CustomCertService: Service() {
companion object {
// started service
@JvmField val CMD_CERTIFICATION_DECISION = "certificateDecision"
@JvmField val CMD_RESET_CERTIFICATES = "resetCertificates"
@JvmField val EXTRA_CERTIFICATE = "certificate"
@JvmField val EXTRA_TRUSTED = "trusted"
val KEYSTORE_DIR = "KeyStore"
val KEYSTORE_NAME = "KeyStore.bks"
/**
* Command when used as started service to accept/reject an open certificate decision.
* Usually sent by a notification action or [TrustCertificateActivity].
*/
const val CMD_CERTIFICATION_DECISION = "certificateDecision"
/**
* Command when used as a started service to remove all known certificates.
* Resets the state of all previously accepted and rejected certificates.
*/
const val CMD_RESET_CERTIFICATES = "resetCertificates"
const val EXTRA_CERTIFICATE = "certificate"
const val EXTRA_TRUSTED = "trusted"
const val KEYSTORE_DIR = "KeyStore"
const val KEYSTORE_NAME = "KeyStore.bks"
}
private var keyStoreFile: File? = null
private lateinit var keyStoreFile: File
private val certFactory = CertificateFactory.getInstance("X.509")
private val trustedKeyStore = KeyStore.getInstance(KeyStore.getDefaultType())!!
......@@ -87,7 +100,7 @@ class CustomCertService: Service() {
// started service
override fun onStartCommand(intent: Intent?, flags: Int, id: Int): Int {
Constants.log.fine("Received command:" + intent)
Constants.log.fine("Received command: $intent")
when (intent?.action) {
CMD_CERTIFICATION_DECISION -> {
......
......@@ -12,19 +12,20 @@ import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.os.Build
import android.support.v4.app.NotificationManagerCompat
object NotificationUtils {
val CHANNEL_CERTIFICATES = "cert4android"
const val CHANNEL_CERTIFICATES = "cert4android"
fun createChannels(context: Context): NotificationManager {
fun createChannels(context: Context): NotificationManagerCompat {
val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= 26)
nm.createNotificationChannel(NotificationChannel(CHANNEL_CERTIFICATES,
context.getString(R.string.certificate_notification_connection_security), NotificationManager.IMPORTANCE_DEFAULT))
context.getString(R.string.certificate_notification_connection_security), NotificationManager.IMPORTANCE_HIGH))
return nm
return NotificationManagerCompat.from(context)
}
}
\ No newline at end of file
......@@ -27,7 +27,7 @@ import java.util.logging.Level
class TrustCertificateActivity: AppCompatActivity() {
companion object {
val EXTRA_CERTIFICATE = "certificate"
const val EXTRA_CERTIFICATE = "certificate"
val certFactory = CertificateFactory.getInstance("X.509")!!
}
......@@ -65,18 +65,18 @@ class TrustCertificateActivity: AppCompatActivity() {
var tv = findViewById<TextView>(R.id.issuedFor)
tv.text = subject
tv = findViewById<TextView>(R.id.issuedBy)
tv = findViewById(R.id.issuedBy)
tv.text = cert.issuerDN.toString()
val formatter = DateFormat.getDateInstance(DateFormat.LONG)
tv = findViewById<TextView>(R.id.validity_period)
tv = findViewById(R.id.validity_period)
tv.text = getString(R.string.trust_certificate_validity_period_value,
formatter.format(cert.notBefore),
formatter.format(cert.notAfter))
tv = findViewById<TextView>(R.id.fingerprint_sha1)
tv = findViewById(R.id.fingerprint_sha1)
tv.text = fingerprint(cert, "SHA-1")
tv = findViewById<TextView>(R.id.fingerprint_sha256)
tv = findViewById(R.id.fingerprint_sha256)
tv.text = fingerprint(cert, "SHA-256")
} catch(e: CertificateParsingException) {
Constants.log.log(Level.WARNING, "Couldn't parse certificate", e)
......@@ -110,17 +110,16 @@ class TrustCertificateActivity: AppCompatActivity() {
}
private fun fingerprint(cert: X509Certificate, algorithm: String): String {
try {
val md = MessageDigest.getInstance(algorithm)
return "$algorithm: ${hexString(md.digest(cert.encoded))}"
} catch(e: Exception) {
return e.message ?: "Couldn't create message digest"
}
}
private fun fingerprint(cert: X509Certificate, algorithm: String) =
try {
val md = MessageDigest.getInstance(algorithm)
"$algorithm: ${hexString(md.digest(cert.encoded))}"
} catch(e: Exception) {
e.message ?: "Couldn't create message digest"
}
private fun hexString(data: ByteArray): String {
val str = data.mapTo(LinkedList<String>()) { String.format("%02x", it) }
val str = data.mapTo(LinkedList()) { String.format("%02x", it) }
return str.joinToString(":")
}
......