[geary/wip/20-cert-pinning: 27/32] Replace controller's GCR pinning impl with new CertificateManager class
- From: Michael Gratton <mjog src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [geary/wip/20-cert-pinning: 27/32] Replace controller's GCR pinning impl with new CertificateManager class
- Date: Tue, 8 Jan 2019 13:01:51 +0000 (UTC)
commit e7889d039bcb9116db2fb09b8321b30489b174b3
Author: Michael Gratton <mike vee net>
Date: Tue Jan 8 23:38:22 2019 +1100
Replace controller's GCR pinning impl with new CertificateManager class
The new class encapsulates cert pinning functionality and provides a
GIO GTlsDatabase implementation for hooking pinned certs directly into
GTlsConnection. Initial impl just stores pinned certs in memory for now.
po/POTFILES.in | 3 +-
.../application-certficate-manager.vala | 376 +++++++++++++++++++++
src/client/application/geary-controller.vala | 36 +-
src/client/meson.build | 3 +-
4 files changed, 415 insertions(+), 3 deletions(-)
---
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 7a7364d1..e714b607 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -15,8 +15,9 @@ src/client/accounts/accounts-editor-row.vala
src/client/accounts/accounts-editor-servers-pane.vala
src/client/accounts/accounts-manager.vala
src/client/application/application-avatar-store.vala
-src/client/application/autostart-manager.vala
+src/client/application/application-certficate-manager.vala
src/client/application/application-command.vala
+src/client/application/autostart-manager.vala
src/client/application/geary-application.vala
src/client/application/geary-args.vala
src/client/application/geary-controller.vala
diff --git a/src/client/application/application-certficate-manager.vala
b/src/client/application/application-certficate-manager.vala
new file mode 100644
index 00000000..a1b00533
--- /dev/null
+++ b/src/client/application/application-certficate-manager.vala
@@ -0,0 +1,376 @@
+/*
+ * Copyright 2016 Software Freedom Conservancy Inc.
+ * Copyright 2019 Michael Gratton <mike vee net>
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+
+// All of the below basically exists since cert pinning using GCR
+// stopped working (GNOME/gcr#10) after gnome-keyring stopped
+// advertising its PKCS11 module (GNOME/gnome-keyring#20). To work
+// around, this piggy-backs off of the GIO infrastructure and adds a
+// custom pinned cert store.
+
+/** Errors thrown by {@link CertificateManager}. */
+public errordomain Application.CertificateManagerError {
+
+ /** The certificate was not trusted by the user. */
+ UNTRUSTED,
+
+ /** The certificate could not be saved. */
+ STORE_FAILED;
+
+}
+
+/**
+ * Managing TLS certificate prompting and storage.
+ */
+public class Application.CertificateManager : GLib.Object {
+
+
+ private TlsDatabase? pinning_database;
+
+
+ /**
+ * Constructs a new instance, globally installing the pinning database.
+ */
+ public CertificateManager() {
+ this.pinning_database = new TlsDatabase(
+ GLib.TlsBackend.get_default().get_default_database()
+ );
+ Geary.Endpoint.default_tls_database = this.pinning_database;
+ }
+
+ /**
+ * Destroys an instance, de-installs the pinning database.
+ */
+ ~CertificateManager() {
+ Geary.Endpoint.default_tls_database = null;
+ }
+
+
+ /**
+ * Prompts the user to trust the certificate for a service.
+ *
+ * Returns true if the user accepted the certificate.
+ */
+ public async void prompt_pin_certificate(Gtk.Window parent,
+ Geary.AccountInformation account,
+ Geary.ServiceInformation service,
+ Geary.Endpoint endpoint,
+ bool is_validation,
+ GLib.Cancellable? cancellable)
+ throws CertificateManagerError {
+ CertificateWarningDialog dialog = new CertificateWarningDialog(
+ parent, account, service, endpoint, is_validation
+ );
+
+ bool save = false;
+ switch (dialog.run()) {
+ case CertificateWarningDialog.Result.TRUST:
+ // noop
+ break;
+
+ case CertificateWarningDialog.Result.ALWAYS_TRUST:
+ save = true;
+ break;
+
+ default:
+ throw new CertificateManagerError.UNTRUSTED("User declined");
+ }
+
+ debug("Pinning certificate for %s...", endpoint.remote.to_string());
+ try {
+ yield add_pinned(
+ endpoint.untrusted_certificate,
+ endpoint.remote,
+ save,
+ cancellable
+ );
+ } catch (GLib.Error err) {
+ throw new CertificateManagerError.STORE_FAILED(err.message);
+ }
+ }
+
+ private async void add_pinned(GLib.TlsCertificate cert,
+ GLib.SocketConnectable? identity,
+ bool save,
+ GLib.Cancellable? cancellable)
+ throws GLib.Error {
+ this.pinning_database.pin_certificate(cert, identity);
+ if (save) {
+ // XXX
+ }
+ }
+
+}
+
+
+/** TLS database that observes locally pinned certs. */
+private class Application.TlsDatabase : GLib.TlsDatabase {
+
+
+ private class TrustContext {
+
+ private static string to_name(GLib.SocketConnectable id) {
+ GLib.NetworkAddress? name = id as GLib.NetworkAddress;
+ if (name != null) {
+ return name.hostname;
+ }
+
+ GLib.NetworkService? service = id as GLib.NetworkService;
+ if (service != null) {
+ return service.domain;
+ }
+
+ GLib.InetSocketAddress? inet = id as GLib.InetSocketAddress;
+ if (inet != null) {
+ return inet.address.to_string();
+ }
+
+ return id.to_string();
+ }
+
+ public string id;
+ public Gcr.Certificate certificate;
+ public Gee.Set<string> pinned_identities = new Gee.HashSet<string>();
+
+
+ public TrustContext(Gcr.Certificate certificate) {
+ this.id = certificate.get_fingerprint_hex(GLib.ChecksumType.SHA256);
+ this.certificate = certificate;
+ }
+
+ public bool add_identity(GLib.SocketConnectable id) {
+ return this.pinned_identities.add(to_name(id));
+ }
+
+ public bool matches_identity(GLib.SocketConnectable id) {
+ return this.pinned_identities.contains(to_name(id));
+ }
+
+ public GLib.TlsCertificate to_tls_certificate()
+ throws GLib.Error {
+ //return new GLib.TlsCertificate.from_pem(
+ // this.certificate.get_pem_data(), -1
+ //);
+ warning("Was actually asked to make a TLS cert from a GCR cert");
+ throw new GLib.IOError.NOT_SUPPORTED("TODO");
+ }
+
+ }
+
+
+ public GLib.TlsDatabase parent { get; private set; }
+
+ private Gee.List<TrustContext> contexts =
+ new Gee.ArrayList<TrustContext>();
+
+
+ public TlsDatabase(GLib.TlsDatabase parent) {
+ this.parent = parent;
+ }
+
+
+ public void pin_certificate(GLib.TlsCertificate certificate,
+ GLib.SocketConnectable identity) {
+ Gcr.Certificate gcr = new Gcr.SimpleCertificate(
+ certificate.certificate.data
+ );
+ lock (this.contexts) {
+ TrustContext? context = lookup_gcr_unlocked(gcr);
+ if (context == null) {
+ context = new TrustContext(gcr);
+ debug("Adding certificate %s",
+ gcr.get_fingerprint_hex(GLib.ChecksumType.SHA1));
+ this.contexts.add(context);
+ }
+ if (context.add_identity(identity)){
+ debug("Adding identity %s", identity.to_string());
+ }
+ }
+ }
+
+ public void remove_certificate(Gcr.Certificate certificate) {
+ lock (this.contexts) {
+ TrustContext? context = lookup_gcr_unlocked(certificate);
+ if (context != null) {
+ this.contexts.remove(context);
+ }
+ }
+ }
+
+ public override string?
+ create_certificate_handle(GLib.TlsCertificate certificate) {
+ TrustContext? context = lookup_tls_certificate(certificate);
+ return (context != null)
+ ? context.id
+ : this.parent.create_certificate_handle(certificate);
+ }
+
+ public override GLib.TlsCertificate?
+ lookup_certificate_for_handle(string handle,
+ GLib.TlsInteraction? interaction,
+ GLib.TlsDatabaseLookupFlags flags,
+ GLib.Cancellable? cancellable = null)
+ throws GLib.Error {
+ TrustContext? context = lookup_id(handle);
+ return (context != null)
+ ? context.to_tls_certificate()
+ : this.parent.lookup_certificate_for_handle(
+ handle, interaction, flags, cancellable
+ );
+ }
+
+ public override async GLib.TlsCertificate
+ lookup_certificate_for_handle_async(string handle,
+ GLib.TlsInteraction? interaction,
+ GLib.TlsDatabaseLookupFlags flags,
+ GLib.Cancellable? cancellable = null)
+ throws GLib.Error {
+ TrustContext? context = lookup_id(handle);
+ return (context != null)
+ ? context.to_tls_certificate()
+ : yield this.parent.lookup_certificate_for_handle_async(
+ handle, interaction, flags, cancellable
+ );
+ }
+
+ public override GLib.TlsCertificate
+ lookup_certificate_issuer(GLib.TlsCertificate certificate,
+ GLib.TlsInteraction? interaction,
+ GLib.TlsDatabaseLookupFlags flags,
+ GLib.Cancellable? cancellable = null)
+ throws GLib.Error {
+ return this.parent.lookup_certificate_issuer(
+ certificate, interaction, flags, cancellable
+ );
+ }
+
+ public override async GLib.TlsCertificate
+ lookup_certificate_issuer_async(GLib.TlsCertificate certificate,
+ GLib.TlsInteraction? interaction,
+ GLib.TlsDatabaseLookupFlags flags,
+ GLib.Cancellable? cancellable = null)
+ throws GLib.Error {
+ return yield this.parent.lookup_certificate_issuer_async(
+ certificate, interaction, flags, cancellable
+ );
+ }
+
+ public override GLib.List<GLib.TlsCertificate>
+ lookup_certificates_issued_by(ByteArray issuer_raw_dn,
+ GLib.TlsInteraction? interaction,
+ GLib.TlsDatabaseLookupFlags flags,
+ GLib.Cancellable? cancellable = null)
+ throws GLib.Error {
+ return this.parent.lookup_certificates_issued_by(
+ issuer_raw_dn, interaction, flags, cancellable
+ );
+ }
+
+ public override async GLib.List<GLib.TlsCertificate>
+ lookup_certificates_issued_by_async(GLib.ByteArray issuer_raw_dn,
+ GLib.TlsInteraction? interaction,
+ GLib.TlsDatabaseLookupFlags flags,
+ GLib.Cancellable? cancellable = null)
+ throws GLib.Error {
+ return yield this.parent.lookup_certificates_issued_by_async(
+ issuer_raw_dn, interaction, flags, cancellable
+ );
+ }
+
+ public override GLib.TlsCertificateFlags
+ verify_chain(GLib.TlsCertificate chain,
+ string purpose,
+ GLib.SocketConnectable? identity,
+ GLib.TlsInteraction? interaction,
+ GLib.TlsDatabaseVerifyFlags flags,
+ GLib.Cancellable? cancellable = null)
+ throws GLib.Error {
+ debug("Verifying cert sync: %s: %s",
+ purpose,
+ identity != null ? identity.to_string() : "[no identity]");
+ GLib.TlsCertificateFlags ret = this.parent.verify_chain(
+ chain, purpose, identity, interaction, flags, cancellable
+ );
+ if (should_verify(ret, purpose, identity)) {
+ debug("Looking for pinned cert");
+ TrustContext? context = lookup_tls_certificate(chain);
+ if (context != null) {
+ debug("Have trust context with %d ids", context.pinned_identities.size);
+ }
+ if (context != null && context.matches_identity(identity)) {
+ ret = 0;
+ }
+ }
+ return ret;
+ }
+
+ public override async GLib.TlsCertificateFlags
+ verify_chain_async(GLib.TlsCertificate chain,
+ string purpose,
+ GLib.SocketConnectable? identity,
+ GLib.TlsInteraction? interaction,
+ GLib.TlsDatabaseVerifyFlags flags,
+ GLib.Cancellable? cancellable = null)
+ throws GLib.Error {
+ debug("Verifying cert async: %s: %s",
+ purpose,
+ identity != null ? identity.to_string() : "[no identity]");
+ GLib.TlsCertificateFlags ret = yield this.parent.verify_chain_async(
+ chain, purpose, identity, interaction, flags, cancellable
+ );
+ if (should_verify(ret, purpose, identity)) {
+ debug("Looking for pinned cert");
+ TrustContext? context = lookup_tls_certificate(chain);
+ if (context != null) {
+ debug("Have trust context with %d ids", context.pinned_identities.size);
+ }
+ if (context != null && context.matches_identity(identity)) {
+ ret = 0;
+ }
+ }
+ return ret;
+ }
+
+ private inline bool should_verify(GLib.TlsCertificateFlags parent_ret,
+ string purpose,
+ GLib.SocketConnectable? identity) {
+ // If the parent didn't verify, check for a locally pinned
+ // cert if it looks like we should, but always reject revoked
+ // certs
+ return (
+ parent_ret != 0 &&
+ !(GLib.TlsCertificateFlags.REVOKED in parent_ret) &&
+ purpose == GLib.TlsDatabase.PURPOSE_AUTHENTICATE_SERVER &&
+ identity != null
+ );
+ }
+
+ private TrustContext? lookup_id(string id) {
+ lock (this.contexts) {
+ return Geary.traverse(this.contexts).first_matching(
+ (ctx) => ctx.id == id
+ );
+ }
+ }
+
+ private TrustContext? lookup_tls_certificate(GLib.TlsCertificate tls) {
+ lock (this.contexts) {
+ return lookup_gcr_unlocked(
+ new Gcr.SimpleCertificate(tls.certificate.data)
+ );
+ }
+ }
+
+ private TrustContext? lookup_gcr_unlocked(Gcr.Certificate cert) {
+ debug("Looking for %s", cert.get_fingerprint_hex(GLib.ChecksumType.SHA1));
+ return Geary.traverse(this.contexts).first_matching(
+ (ctx) => Gcr.Certificate.compare(ctx.certificate, cert) == 0
+ );
+ }
+
+}
diff --git a/src/client/application/geary-controller.vala b/src/client/application/geary-controller.vala
index 4cdde211..62c289c6 100644
--- a/src/client/application/geary-controller.vala
+++ b/src/client/application/geary-controller.vala
@@ -112,6 +112,11 @@ public class GearyController : Geary.BaseObject {
public Accounts.Manager? account_manager { get; private set; default = null; }
+ /** Application-wide {@link Application.CertificateManager} instance. */
+ public Application.CertificateManager? certificate_manager {
+ get; private set; default = null;
+ }
+
public MainWindow? main_window { get; private set; default = null; }
public Geary.App.ConversationMonitor? current_conversations { get; private set; default = null; }
@@ -320,7 +325,10 @@ public class GearyController : Geary.BaseObject {
error("Error migrating configuration directories: %s", e.message);
}
- // Hook up accounts and credentials machinery
+ // Hook up cert, accounts and credentials machinery
+
+ this.certificate_manager = new Application.CertificateManager();
+
SecretMediator? libsecret = null;
try {
libsecret = yield new SecretMediator(this.application, cancellable);
@@ -797,6 +805,32 @@ public class GearyController : Geary.BaseObject {
}
context.tls_validation_prompting = true;
+ try {
+ yield this.certificate_manager.prompt_pin_certificate(
+ this.main_window,
+ context.account.information,
+ service,
+ endpoint,
+ true,
+ context.cancellable
+ );
+ context.tls_validation_failed = false;
+ } catch (Application.CertificateManagerError.UNTRUSTED err) {
+ // Don't report an error here, the user simply declined.
+ context.tls_validation_failed = true;
+ } catch (Application.CertificateManagerError err) {
+ // Assume validation is now good, but report the error
+ // since the cert may not have been saved
+ context.tls_validation_failed = false;
+ report_problem(
+ new Geary.ServiceProblemReport(
+ Geary.ProblemType.UNTRUSTED,
+ context.account.information,
+ service,
+ err
+ )
+ );
+ }
context.tls_validation_prompting = false;
update_account_status();
diff --git a/src/client/meson.build b/src/client/meson.build
index 0afae2c1..6136525c 100644
--- a/src/client/meson.build
+++ b/src/client/meson.build
@@ -1,8 +1,9 @@
# Geary client
geary_client_vala_sources = files(
'application/application-avatar-store.vala',
- 'application/autostart-manager.vala',
+ 'application/application-certficate-manager.vala',
'application/application-command.vala',
+ 'application/autostart-manager.vala',
'application/geary-application.vala',
'application/geary-args.vala',
'application/geary-config.vala',
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]