diff --git a/README.md b/README.md index 83f4602..cc44845 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,11 @@ protected void onCreate(Bundle savedInstanceState) { URL url = new URL("https://www.datatheorem.com"); String serverHostname = url.getHost(); + + //Optionally add a local broadcast receiver to receive PinningFailureReports + PinningValidationReportTestBroadcastReceiver receiver = new PinningValidationReportTestBroadcastReceiver(); + LocalBroadcastManager.getInstance(context) + .registerReceiver(receiver, new IntentFilter(BackgroundReporter.REPORT_VALIDATION_EVENT)); // HttpsUrlConnection HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(); @@ -127,10 +132,21 @@ protected void onCreate(Bundle savedInstanceState) { TrustKit.getInstance().getTrustManager(serverHostname)) .build(); } + +class PinningFailureReportBroadcastReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + PinningFailureReport report = (PinningFailureReport) intent.getSerializableExtra(BackgroundReporter.EXTRA_REPORT); + } +} ``` Once TrustKit has been initialized and the client or connection's `SSLSocketFactory` has been set, it will verify the server's certificate chain against the configured pinning policy whenever an HTTPS connection is initiated. If a report URI has been configured, the App will also send reports to the specified URI whenever a pin validation failure occurred. +You can also create and register local broadcast receivers to receive the same certificate pinning error reports that would be sent to the report_uris. + + Limitations ---------- diff --git a/app/src/main/java/com/datatheorem/android/trustkit/demoapp/DemoMainActivity.java b/app/src/main/java/com/datatheorem/android/trustkit/demoapp/DemoMainActivity.java index 1dbd00d..5ad5937 100644 --- a/app/src/main/java/com/datatheorem/android/trustkit/demoapp/DemoMainActivity.java +++ b/app/src/main/java/com/datatheorem/android/trustkit/demoapp/DemoMainActivity.java @@ -1,24 +1,31 @@ package com.datatheorem.android.trustkit.demoapp; +import android.content.IntentFilter; import android.os.AsyncTask; import android.os.Bundle; +import android.support.v4.content.LocalBroadcastManager; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.widget.TextView; + import com.datatheorem.android.trustkit.TrustKit; +import com.datatheorem.android.trustkit.reporting.BackgroundReporter; + import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; + import javax.net.ssl.HttpsURLConnection; public class DemoMainActivity extends AppCompatActivity { - private static final String DEBUG_TAG = "TrustKit-Demo"; + protected static final String DEBUG_TAG = "TrustKit-Demo"; + private static final PinningFailureReportBroadcastReceiver pinningFailureReportBroadcastReceiver = new PinningFailureReportBroadcastReceiver(); @Override protected void onCreate(Bundle savedInstanceState) { @@ -38,6 +45,17 @@ protected void onCreate(Bundle savedInstanceState) { new DownloadWebpageTask().execute("https://www.google.com"); textView.setText("Connection results are in the logs"); + + IntentFilter intentFilter = new IntentFilter(BackgroundReporter.REPORT_VALIDATION_EVENT); + LocalBroadcastManager.getInstance(getApplicationContext()) + .registerReceiver(pinningFailureReportBroadcastReceiver,intentFilter); + } + + @Override + protected void onDestroy() { + LocalBroadcastManager.getInstance(getApplicationContext()) + .unregisterReceiver(pinningFailureReportBroadcastReceiver); + super.onDestroy(); } private class DownloadWebpageTask extends AsyncTask { diff --git a/app/src/main/java/com/datatheorem/android/trustkit/demoapp/PinningFailureReportBroadcastReceiver.java b/app/src/main/java/com/datatheorem/android/trustkit/demoapp/PinningFailureReportBroadcastReceiver.java new file mode 100644 index 0000000..362976e --- /dev/null +++ b/app/src/main/java/com/datatheorem/android/trustkit/demoapp/PinningFailureReportBroadcastReceiver.java @@ -0,0 +1,26 @@ +package com.datatheorem.android.trustkit.demoapp; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; +import com.datatheorem.android.trustkit.reporting.BackgroundReporter; + +import java.io.Serializable; + +/** + * Class that provides an example broadcast receiver + * + *

+ * Applications using TrustKit can listen for local broadcasts and receive the same report that + * would be sent to the report_url. + *

+ **/ +class PinningFailureReportBroadcastReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + Serializable result = intent.getSerializableExtra(BackgroundReporter.EXTRA_REPORT); + Log.i(DemoMainActivity.DEBUG_TAG, result.toString()); + } +} diff --git a/build.gradle b/build.gradle index 833cb5c..427f631 100644 --- a/build.gradle +++ b/build.gradle @@ -26,14 +26,14 @@ allprojects { ext{ - trustkitVersionCode = 6 - trustkitVersionName = "1.1.0" + trustkitVersionCode = 7 + trustkitVersionName = "1.1.1" - demoAppTrustKitVersionCode = 1 - demoAppTrustKitVersionName = "1.0" + demoAppTrustKitVersionCode = 2 + demoAppTrustKitVersionName = "1.1" - demoAppKotlinTrustKitVersionCode = 1 - demoAppKotlinTrustKitVersionName = "1.0" + demoAppKotlinTrustKitVersionCode = 2 + demoAppKotlinTrustKitVersionName = "1.1" javaSourceCompatibilty = '1.6' toolVersions = [ android : [ diff --git a/demoappkotlin/src/main/AndroidManifest.xml b/demoappkotlin/src/main/AndroidManifest.xml index 8e2d3d4..883fc1b 100644 --- a/demoappkotlin/src/main/AndroidManifest.xml +++ b/demoappkotlin/src/main/AndroidManifest.xml @@ -20,6 +20,7 @@ + diff --git a/demoappkotlin/src/main/java/com/datatheorem/android/trustkit/demoappkotlin/DemoMainActivity.kt b/demoappkotlin/src/main/java/com/datatheorem/android/trustkit/demoappkotlin/DemoMainActivity.kt index 10d93ed..b760e01 100644 --- a/demoappkotlin/src/main/java/com/datatheorem/android/trustkit/demoappkotlin/DemoMainActivity.kt +++ b/demoappkotlin/src/main/java/com/datatheorem/android/trustkit/demoappkotlin/DemoMainActivity.kt @@ -1,7 +1,9 @@ package com.datatheorem.android.trustkit.demoappkotlin +import android.content.IntentFilter import android.os.AsyncTask import android.os.Bundle +import android.support.v4.content.LocalBroadcastManager import android.support.v7.app.AppCompatActivity import android.support.v7.widget.Toolbar import android.util.Log @@ -10,6 +12,7 @@ import android.view.MenuItem import android.view.View import android.widget.TextView import com.datatheorem.android.trustkit.TrustKit +import com.datatheorem.android.trustkit.reporting.BackgroundReporter import java.io.IOException import java.net.MalformedURLException import java.net.URL @@ -17,6 +20,7 @@ import javax.net.ssl.HttpsURLConnection class DemoMainActivity : AppCompatActivity() { + private lateinit var pinningFailureReportBroadcastReceiver: PinningFailureReportBroadcastReceiver override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -35,6 +39,18 @@ class DemoMainActivity : AppCompatActivity() { DownloadWebpageTask().execute("https://www.google.com") textView.text = "Connection results are in the logs" + + // Adding a local broadcast receiver to listen for validation report events + pinningFailureReportBroadcastReceiver = PinningFailureReportBroadcastReceiver() + val intentFilter = IntentFilter(BackgroundReporter.REPORT_VALIDATION_EVENT) + LocalBroadcastManager.getInstance(this.applicationContext) + .registerReceiver(pinningFailureReportBroadcastReceiver,intentFilter) + } + + override fun onDestroy() { + LocalBroadcastManager.getInstance(this.applicationContext) + .unregisterReceiver(pinningFailureReportBroadcastReceiver) + super.onDestroy() } private inner class DownloadWebpageTask : AsyncTask() { @@ -83,7 +99,7 @@ class DemoMainActivity : AppCompatActivity() { companion object { - private const val DEBUG_TAG = "TrustKit-Demo" + internal const val DEBUG_TAG = "TrustKit-Demo" } } diff --git a/demoappkotlin/src/main/java/com/datatheorem/android/trustkit/demoappkotlin/PinningFailureReportBroadcastReceiver.kt b/demoappkotlin/src/main/java/com/datatheorem/android/trustkit/demoappkotlin/PinningFailureReportBroadcastReceiver.kt new file mode 100644 index 0000000..acbab09 --- /dev/null +++ b/demoappkotlin/src/main/java/com/datatheorem/android/trustkit/demoappkotlin/PinningFailureReportBroadcastReceiver.kt @@ -0,0 +1,24 @@ +package com.datatheorem.android.trustkit.demoappkotlin + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.util.Log +import com.datatheorem.android.trustkit.reporting.BackgroundReporter + +/** + * Class that provides an example broadcast receiver + * + *

+ * Applications using TrustKit can listen for local broadcasts and receive the same report that + * would be sent to the report_url. + *

+ **/ +class PinningFailureReportBroadcastReceiver : BroadcastReceiver() { + + override fun onReceive(context: Context, intent: Intent) { + val result = intent.getSerializableExtra(BackgroundReporter.EXTRA_REPORT) + Log.i(DemoMainActivity.DEBUG_TAG, result.toString()) + } + +} diff --git a/trustkit/build.gradle b/trustkit/build.gradle index 8d02f3c..b34c461 100644 --- a/trustkit/build.gradle +++ b/trustkit/build.gradle @@ -33,6 +33,7 @@ dependencies { androidTestImplementation "com.android.support.test:runner:$rootProject.libVersions.android.testRunner" androidTestImplementation "com.android.support.test:rules:$rootProject.libVersions.android.testRunner" androidTestImplementation "org.mockito:mockito-core:$rootProject.libVersions.mockito.android" + androidTestImplementation "org.awaitility:awaitility:3.1.6" androidTestImplementation "com.crittercism.dexmaker:dexmaker:$rootProject.libVersions.dexmaker" androidTestImplementation "com.crittercism.dexmaker:dexmaker-dx:$rootProject.libVersions.dexmaker" androidTestImplementation "com.crittercism.dexmaker:dexmaker-mockito:$rootProject.libVersions.dexmaker" diff --git a/trustkit/proguard-rules.pro b/trustkit/proguard-rules.pro index cade0cf..d36a045 100644 --- a/trustkit/proguard-rules.pro +++ b/trustkit/proguard-rules.pro @@ -15,3 +15,7 @@ #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} + +-keep class com.datatheorem.android.trustkit.reporting.BackgroundReporter { + public static *; +} \ No newline at end of file diff --git a/trustkit/src/androidTest/java/com/datatheorem/android/trustkit/reporting/BackgroundReporterTest.java b/trustkit/src/androidTest/java/com/datatheorem/android/trustkit/reporting/BackgroundReporterTest.java index 3acf6cf..13c138b 100644 --- a/trustkit/src/androidTest/java/com/datatheorem/android/trustkit/reporting/BackgroundReporterTest.java +++ b/trustkit/src/androidTest/java/com/datatheorem/android/trustkit/reporting/BackgroundReporterTest.java @@ -1,25 +1,20 @@ package com.datatheorem.android.trustkit.reporting; -import static com.datatheorem.android.trustkit.CertificateUtils.testCertChain; -import static com.datatheorem.android.trustkit.CertificateUtils.testCertChainPem; -import static junit.framework.Assert.assertEquals; -import static junit.framework.Assert.assertNotNull; -import static junit.framework.Assert.assertTrue; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; import android.os.Build; import android.support.test.InstrumentationRegistry; import android.support.test.runner.AndroidJUnit4; +import android.support.v4.content.LocalBroadcastManager; + import com.datatheorem.android.trustkit.TestableTrustKit; import com.datatheorem.android.trustkit.config.DomainPinningPolicy; import com.datatheorem.android.trustkit.pinning.PinningValidationResult; import com.datatheorem.android.trustkit.utils.VendorIdentifier; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.ArrayList; -import java.util.HashSet; + +import org.awaitility.Awaitility; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -29,6 +24,23 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mockito; +import java.io.Serializable; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import static com.datatheorem.android.trustkit.CertificateUtils.testCertChain; +import static com.datatheorem.android.trustkit.CertificateUtils.testCertChainPem; +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNotNull; +import static junit.framework.Assert.assertTrue; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + @RunWith(AndroidJUnit4.class) public class BackgroundReporterTest { @@ -44,7 +56,7 @@ public void testPinValidationFailed() throws MalformedURLException, JSONExceptio // TrustKit does not do anything for API level < 17 hence there is no reporting return; } - + Context context = InstrumentationRegistry.getContext(); // Initialize TrustKit String serverHostname = "mail.google.com"; final DomainPinningPolicy domainPolicy = new DomainPinningPolicy.Builder() @@ -60,9 +72,14 @@ public void testPinValidationFailed() throws MalformedURLException, JSONExceptio .setReportUris(new HashSet() {{ add("https://overmind.datatheorem.com"); }}) .build(); - TestableBackgroundReporter reporter = new TestableBackgroundReporter("com.unit.tests", + final PinningValidationReportTestBroadcastReceiver receiver = new PinningValidationReportTestBroadcastReceiver(); + LocalBroadcastManager.getInstance(context) + .registerReceiver(receiver, new IntentFilter(BackgroundReporter.REPORT_VALIDATION_EVENT)); + + TestableBackgroundReporter reporter = new TestableBackgroundReporter( context, + "com.unit.tests", "1.2", - VendorIdentifier.getOrCreate(InstrumentationRegistry.getContext())); + VendorIdentifier.getOrCreate(context)); TestableBackgroundReporter reporterSpy = Mockito.spy(reporter); // Call the method twice to also test the report rate limiter @@ -81,8 +98,17 @@ public void testPinValidationFailed() throws MalformedURLException, JSONExceptio eq(new HashSet() {{ add(new URL("https://overmind.datatheorem.com")); }} ) ); + validateSentReport(reportSent.getValue()); + + Awaitility.await().atMost(2, TimeUnit.SECONDS).untilTrue(receiver.broadcastReceived); + assertTrue(receiver.broadcastReceived.get()); + assertTrue(receiver.containedReport instanceof PinningFailureReport); + validateSentReport((PinningFailureReport) receiver.containedReport); + } + + private void validateSentReport(PinningFailureReport reportSent) throws JSONException { // Validate the content of the generated report - JSONObject reportSentJson = reportSent.getValue().toJson(); + JSONObject reportSentJson = reportSent.toJson(); assertEquals("com.unit.tests", reportSentJson.getString("app-bundle-id")); assertEquals("1.2", reportSentJson.getString("app-version")); assertEquals("ANDROID", reportSentJson.getString("app-platform")); @@ -123,7 +149,17 @@ public void testPinValidationFailed() throws MalformedURLException, JSONExceptio .contains("pin-sha256=\"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\"")); assertTrue(pinsTestable .contains("pin-sha256=\"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=\"")); + } + private class PinningValidationReportTestBroadcastReceiver extends BroadcastReceiver{ + public AtomicBoolean broadcastReceived = new AtomicBoolean(false); + public Serializable containedReport; + + @Override + public void onReceive(Context context, Intent intent) { + broadcastReceived.set(true); + containedReport = intent.getSerializableExtra(BackgroundReporter.EXTRA_REPORT); + } } } diff --git a/trustkit/src/androidTest/java/com/datatheorem/android/trustkit/reporting/TestableBackgroundReporter.java b/trustkit/src/androidTest/java/com/datatheorem/android/trustkit/reporting/TestableBackgroundReporter.java index 073103f..639d231 100644 --- a/trustkit/src/androidTest/java/com/datatheorem/android/trustkit/reporting/TestableBackgroundReporter.java +++ b/trustkit/src/androidTest/java/com/datatheorem/android/trustkit/reporting/TestableBackgroundReporter.java @@ -1,6 +1,7 @@ package com.datatheorem.android.trustkit.reporting; +import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.RequiresApi; import java.net.URL; @@ -9,8 +10,8 @@ @RequiresApi(api = 16) public class TestableBackgroundReporter extends BackgroundReporter { - public TestableBackgroundReporter(String appPackageName, String appVersion, String appVendorId){ - super(appPackageName, appVersion, appVendorId); + public TestableBackgroundReporter(Context context, String appPackageName, String appVersion, String appVendorId){ + super(context, appPackageName, appVersion, appVendorId); } @Override diff --git a/trustkit/src/main/java/com/datatheorem/android/trustkit/TrustKit.java b/trustkit/src/main/java/com/datatheorem/android/trustkit/TrustKit.java index 7319f2d..67bb212 100644 --- a/trustkit/src/main/java/com/datatheorem/android/trustkit/TrustKit.java +++ b/trustkit/src/main/java/com/datatheorem/android/trustkit/TrustKit.java @@ -6,12 +6,16 @@ import android.os.Build; import android.support.annotation.NonNull; import android.util.Printer; + import com.datatheorem.android.trustkit.config.ConfigurationException; import com.datatheorem.android.trustkit.config.TrustKitConfiguration; import com.datatheorem.android.trustkit.pinning.TrustManagerBuilder; import com.datatheorem.android.trustkit.reporting.BackgroundReporter; import com.datatheorem.android.trustkit.utils.TrustKitLog; import com.datatheorem.android.trustkit.utils.VendorIdentifier; + +import org.xmlpull.v1.XmlPullParserException; + import java.io.IOException; import java.security.KeyManagementException; import java.security.KeyStoreException; @@ -19,11 +23,11 @@ import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.util.Set; + import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; -import org.xmlpull.v1.XmlPullParserException; /** @@ -203,7 +207,7 @@ protected TrustKit(@NonNull Context context, } String appVendorId = VendorIdentifier.getOrCreate(context); - BackgroundReporter reporter = new BackgroundReporter(appPackageName, appVersion, + BackgroundReporter reporter = new BackgroundReporter(context, appPackageName, appVersion, appVendorId); // Initialize the trust manager builder diff --git a/trustkit/src/main/java/com/datatheorem/android/trustkit/reporting/BackgroundReporter.java b/trustkit/src/main/java/com/datatheorem/android/trustkit/reporting/BackgroundReporter.java index 8855dd8..04d5613 100644 --- a/trustkit/src/main/java/com/datatheorem/android/trustkit/reporting/BackgroundReporter.java +++ b/trustkit/src/main/java/com/datatheorem/android/trustkit/reporting/BackgroundReporter.java @@ -1,12 +1,17 @@ package com.datatheorem.android.trustkit.reporting; +import android.content.Context; +import android.content.Intent; import android.support.annotation.NonNull; import android.support.annotation.RequiresApi; +import android.support.v4.content.LocalBroadcastManager; import android.util.Base64; + import com.datatheorem.android.trustkit.config.DomainPinningPolicy; import com.datatheorem.android.trustkit.pinning.PinningValidationResult; import com.datatheorem.android.trustkit.utils.TrustKitLog; + import java.net.URL; import java.security.cert.CertificateEncodingException; import java.security.cert.X509Certificate; @@ -17,17 +22,22 @@ public class BackgroundReporter { + public static final String REPORT_VALIDATION_EVENT = "com.datatheorem.android.trustkit.reporting.BackgroundReporter:REPORT_VALIDATION_EVENT"; + public static final String EXTRA_REPORT = "Report"; // App meta-data to be sent with the reports private final String appPackageName; private final String appVersion; private final String appVendorId; + private final Context context; - public BackgroundReporter(@NonNull String appPackageName, @NonNull String appVersion, + public BackgroundReporter(@NonNull Context context, @NonNull String appPackageName, @NonNull String appVersion, @NonNull String appVendorId) { + this.context = context; this.appPackageName = appPackageName; this.appVersion = appVersion; this.appVendorId = appVendorId; + } private static String certificateToPem(X509Certificate certificate) { @@ -85,6 +95,7 @@ validatedCertificateChainAsPem, new Date(System.currentTimeMillis()), // If a similar report hasn't been sent recently, send it now if (!(ReportRateLimiter.shouldRateLimit(report))) { sendReport(report, serverConfig.getReportUris()); + broadcastReport(report); } else { TrustKitLog.i("Report for " + serverHostname + " was not sent due to rate-limiting"); } @@ -101,5 +112,15 @@ protected void sendReport(@NonNull PinningFailureReport report, } // Call the task new BackgroundReporterTask().execute(taskParameters.toArray()); + } + + protected void broadcastReport(@NonNull PinningFailureReport report){ + Intent intent = new Intent(REPORT_VALIDATION_EVENT); + intent.putExtra(EXTRA_REPORT, report); + LocalBroadcastManager.getInstance(context).sendBroadcast(intent); + + + + } } diff --git a/trustkit/src/main/java/com/datatheorem/android/trustkit/reporting/PinningFailureReport.java b/trustkit/src/main/java/com/datatheorem/android/trustkit/reporting/PinningFailureReport.java index ba13d58..111a237 100644 --- a/trustkit/src/main/java/com/datatheorem/android/trustkit/reporting/PinningFailureReport.java +++ b/trustkit/src/main/java/com/datatheorem/android/trustkit/reporting/PinningFailureReport.java @@ -2,21 +2,24 @@ import android.support.annotation.NonNull; import android.text.format.DateFormat; + import com.datatheorem.android.trustkit.BuildConfig; import com.datatheorem.android.trustkit.config.PublicKeyPin; import com.datatheorem.android.trustkit.pinning.PinningValidationResult; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + import java.io.Serializable; import java.util.Date; import java.util.List; import java.util.Set; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; /** * A pinning validation failure report. */ -class PinningFailureReport implements Serializable { +public class PinningFailureReport implements Serializable { // Fields specific to TrustKit reports private static final String APP_PLATFORM = "ANDROID"; private static final String trustKitVersion = BuildConfig.VERSION_NAME; @@ -110,12 +113,12 @@ public String toString() { } @NonNull - String getNotedHostname() { + public String getNotedHostname() { return notedHostname; } @NonNull - String getServerHostname() { + public String getServerHostname() { return serverHostname; } @@ -125,7 +128,7 @@ List getValidatedCertificateChainAsPem() { } @NonNull - PinningValidationResult getValidationResult() { + public PinningValidationResult getValidationResult() { return validationResult; }