From 964b03c068205e21bd4f6deb91098a71a797d44c Mon Sep 17 00:00:00 2001 From: Michael Gratton Date: Tue, 25 Aug 2020 16:37:08 +1000 Subject: [PATCH] Application.TlsDatabase: Add unit tests for local (non-GCR) pinning Make the class internal so it can be tested, add unit tests covering both in-memory-only and on-disk pinning. --- .../application-certificate-manager.vala | 9 +- .../application-certificate-manager-test.vala | 216 ++++++++++++++++++ test/meson.build | 1 + test/test-client.vala | 1 + 4 files changed, 225 insertions(+), 2 deletions(-) create mode 100644 test/client/application/application-certificate-manager-test.vala diff --git a/src/client/application/application-certificate-manager.vala b/src/client/application/application-certificate-manager.vala index 65f6af4f..ff0e7785 100644 --- a/src/client/application/application-certificate-manager.vala +++ b/src/client/application/application-certificate-manager.vala @@ -153,8 +153,13 @@ public class Application.CertificateManager : GLib.Object { } -/** TLS database that observes locally pinned certs. */ -private class Application.TlsDatabase : GLib.TlsDatabase { +/** + * TLS database that observes locally pinned certs. + * + * An instance of this is managed by {@link CertificateManager}, the + * application should simply construct an instance of that. + */ +internal class Application.TlsDatabase : GLib.TlsDatabase { /** A certificate and the identities it is trusted for. */ diff --git a/test/client/application/application-certificate-manager-test.vala b/test/client/application/application-certificate-manager-test.vala new file mode 100644 index 00000000..6cb74b04 --- /dev/null +++ b/test/client/application/application-certificate-manager-test.vala @@ -0,0 +1,216 @@ +/* + * Copyright © 2020 Michael Gratton + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + + +class Application.CertificateManagerTest : TestCase { + + + private const string IDENITY_HOSTNAME = "localhost"; + private const uint16 IDENITY_PORT = 143; + + private static int cert_id = 1; + + private GLib.File? tmp = null; + private GLib.File? db_dir = null; + private GLib.File? cert_dir = null; + + + public CertificateManagerTest() { + base("Application.CertificateManagerTest"); + add_test( + "database_memory_certificate_pinning_without_gcr", + database_memory_certificate_pinning_without_gcr + ); + add_test( + "database_disk_certificate_pinning_without_gcr", + database_disk_certificate_pinning_without_gcr + ); + } + + public override void set_up() throws GLib.Error { + this.tmp = GLib.File.new_for_path( + GLib.DirUtils.make_tmp("application-certificate-manager-test-XXXXXX") + ); + this.db_dir = this.tmp.get_child("db"); + this.db_dir.make_directory(); + this.cert_dir = this.tmp.get_child("certs"); + this.cert_dir.make_directory(); + } + + public override void tear_down() throws GLib.Error { + delete_file(this.tmp); + this.db_dir = null; + this.cert_dir = null; + this.tmp = null; + } + + public void database_memory_certificate_pinning_without_gcr() + throws GLib.Error { + var test_article1 = new Application.TlsDatabase( + GLib.TlsBackend.get_default().get_default_database(), + this.db_dir, + false + ); + var id = new_identity(); + var cert1 = new_cert("cert1"); + var cert2 = new_cert("cert2"); + + // Assert the db doesn't know about the cert first up. + assert_pinning(test_article1, cert1, id, false); + assert_pinning(test_article1, cert2, id, false); + + // Pin a cert in the db + test_article1.pin_certificate.begin( + cert1, id, false, null, this.async_completion + ); + test_article1.pin_certificate.end(this.async_result()); + + // Assert the db now knows about it, but not the other + assert_pinning(test_article1, cert1, id, true); + assert_pinning(test_article1, cert2, id, false); + + // Construct a new test article and ensure it doesn't know + // about either + var test_article2 = new Application.TlsDatabase( + GLib.TlsBackend.get_default().get_default_database(), + this.db_dir, + false + ); + assert_pinning(test_article2, cert1, id, false); + assert_pinning(test_article2, cert2, id, false); + } + + public void database_disk_certificate_pinning_without_gcr() + throws GLib.Error { + var test_article1 = new Application.TlsDatabase( + GLib.TlsBackend.get_default().get_default_database(), + this.db_dir, + false + ); + var id = new_identity(); + var cert1 = new_cert("cert1"); + var cert2 = new_cert("cert2"); + + // Assert the db doesn't know about the cert first up. + assert_pinning(test_article1, cert1, id, false); + assert_pinning(test_article1, cert2, id, false); + + // Pin a cert in the db + test_article1.pin_certificate.begin( + cert1, id, true, null, this.async_completion + ); + test_article1.pin_certificate.end(this.async_result()); + + // Assert the db now knows about it, but not the other + assert_pinning(test_article1, cert1, id, true); + assert_pinning(test_article1, cert2, id, false); + + // Construct a new test article and ensure it has loaded the + // first from disk + var test_article2 = new Application.TlsDatabase( + GLib.TlsBackend.get_default().get_default_database(), + this.db_dir, + false + ); + assert_pinning(test_article2, cert1, id, true); + assert_pinning(test_article2, cert2, id, false); + } + + private void assert_pinning(Application.TlsDatabase db, + GLib.TlsCertificate cert, + GLib.SocketConnectable id, + bool is_pinned) + throws GLib.Error { + // Test both the sync and async calls to ensure equivalence + var sync_ret = db.verify_chain( + cert, + GLib.TlsDatabase.PURPOSE_AUTHENTICATE_SERVER, + id, + null, + NONE, + null + ); + if (is_pinned) { + assert_true( + sync_ret == 0, + "is pinned sync" + ); + } else { + assert_true( + sync_ret == GLib.TlsCertificateFlags.UNKNOWN_CA, + "not pinned sync" + ); + } + + db.verify_chain_async.begin( + cert, + GLib.TlsDatabase.PURPOSE_AUTHENTICATE_SERVER, + id, + null, + NONE, + null, + this.async_completion + ); + var async_ret = db.verify_chain_async.end(this.async_result()); + if (is_pinned) { + assert_true( + async_ret == 0, + "is pinned async" + ); + } else { + assert_true( + async_ret == GLib.TlsCertificateFlags.UNKNOWN_CA, + "not pinned async" + ); + } + } + + private GLib.SocketConnectable new_identity() { + return new GLib.NetworkAddress(IDENITY_HOSTNAME, IDENITY_PORT); + } + + private GLib.TlsCertificate new_cert(string name) throws GLib.Error { + var priv_name = name + ".priv"; + var cert_name = name + ".cert"; + var template_name = name + ".template"; + GLib.Process.spawn_sync( + this.cert_dir.get_path(), + { + "certtool", "--generate-privkey", "--outfile", priv_name + }, + GLib.Environ.get(), + SpawnFlags.SEARCH_PATH, + null + ); + this.cert_dir.get_child(template_name).create(NONE).write(""" +organization = "Example Inc." +country = AU +serial = %d +expiration_days = 1 +dns_name = "%s" +encryption_key +""".printf(CertificateManagerTest.cert_id++, IDENITY_HOSTNAME).data); + + GLib.Process.spawn_sync( + this.cert_dir.get_path(), + { + "certtool", + "--generate-self-signed", + "--load-privkey", priv_name, + "--template", template_name, + "--outfile", cert_name + }, + GLib.Environ.get(), + SpawnFlags.SEARCH_PATH, + null + ); + return new GLib.TlsCertificate.from_file( + this.cert_dir.get_child(cert_name).get_path() + ); + } + +} diff --git a/test/meson.build b/test/meson.build index 6ea5e27a..fe3040dd 100644 --- a/test/meson.build +++ b/test/meson.build @@ -79,6 +79,7 @@ test_client_sources = [ 'test-client.vala', 'client/accounts/accounts-manager-test.vala', + 'client/application/application-certificate-manager-test.vala', 'client/application/application-client-test.vala', 'client/application/application-configuration-test.vala', 'client/components/client-web-view-test.vala', diff --git a/test/test-client.vala b/test/test-client.vala index 1cdbf8f6..573aaac1 100644 --- a/test/test-client.vala +++ b/test/test-client.vala @@ -50,6 +50,7 @@ int main(string[] args) { // Keep this before other ClientWebView based tests since it tests // WebContext init client.add_suite(new Accounts.ManagerTest().suite); + client.add_suite(new Application.CertificateManagerTest().suite); client.add_suite(new Application.ClientTest().suite); client.add_suite(new Application.ConfigurationTest().suite); client.add_suite(new ClientWebViewTest().suite);