2
* Copyright 2016 Software Freedom Conservancy Inc.
3
* Copyright 2019 Michael Gratton <mike@vee.net>
5
* This software is licensed under the GNU Lesser General Public License
6
* (version 2.1 or later). See the COPYING file in this distribution.
9
// Required because GCR's VAPI is behind-the-times. See:
10
// https://gitlab.gnome.org/GNOME/gcr/merge_requests/7
11
extern async bool gcr_trust_add_pinned_certificate_async(
15
Cancellable? cancellable
17
extern bool gcr_trust_is_certificate_pinned(
21
Cancellable? cancellable
25
// All of the below basically exists since cert pinning using GCR
26
// stopped working (GNOME/gcr#10) after gnome-keyring stopped
27
// advertising its PKCS11 module (GNOME/gnome-keyring#20). To work
28
// around, this piggy-backs off of the GIO infrastructure and adds a
29
// custom pinned cert store.
31
/** Errors thrown by {@link CertificateManager}. */
32
public errordomain Application.CertificateManagerError {
34
/** The certificate was not trusted by the user. */
37
/** The certificate could not be saved. */
43
* Managing TLS certificate prompting and storage.
45
public class Application.CertificateManager : GLib.Object {
48
// PCKS11 flag value lifted from pkcs11.h
49
private const ulong CKF_WRITE_PROTECTED = 1UL << 1;
52
private static async bool is_gcr_enabled(GLib.Cancellable? cancellable) {
53
// Use GCR if it looks like it should be able to be
54
// used. Specifically, if we can initialise the trust store
55
// must have both lookup and store PKCS11 slot URIs or else it
56
// won't be able to lookup or store pinned certs, secondly,
57
// there must be at least a read-write store slot available.
58
bool init_okay = false;
60
init_okay = yield Gcr.pkcs11_initialize_async(cancellable);
61
} catch (GLib.Error err) {
62
warning("Failed to initialise GCR PCKS#11 modules: %s", err.message);
65
bool has_uris = false;
68
!Geary.String.is_empty(Gcr.pkcs11_get_trust_store_uri()) &&
69
Gcr.pkcs11_get_trust_lookup_uris().length > 0
71
debug("GCR slot URIs found: %s", has_uris.to_string());
74
bool has_rw_store = false;
76
Gck.Slot? store = Gcr.pkcs11_get_trust_store_slot();
77
has_rw_store = !store.has_flags(CKF_WRITE_PROTECTED);
78
debug("GCR store is R/W: %s", has_rw_store.to_string());
85
private TlsDatabase? pinning_database;
89
* Constructs a new instance, globally installing the pinning database.
91
public async CertificateManager(GLib.File store_dir,
92
GLib.Cancellable? cancellable) {
93
bool use_gcr = yield is_gcr_enabled(cancellable);
94
this.pinning_database = new TlsDatabase(
95
GLib.TlsBackend.get_default().get_default_database(),
99
Geary.Endpoint.default_tls_database = this.pinning_database;
103
* Destroys an instance, de-installs the pinning database.
105
~CertificateManager() {
106
Geary.Endpoint.default_tls_database = null;
111
* Prompts the user to trust the certificate for a service.
113
* Returns true if the user accepted the certificate.
115
public async void prompt_pin_certificate(Gtk.Window parent,
116
Geary.AccountInformation account,
117
Geary.ServiceInformation service,
118
Geary.Endpoint endpoint,
120
GLib.Cancellable? cancellable)
121
throws CertificateManagerError {
122
CertificateWarningDialog dialog = new CertificateWarningDialog(
123
parent, account, service, endpoint, is_validation
127
switch (dialog.run()) {
128
case CertificateWarningDialog.Result.TRUST:
132
case CertificateWarningDialog.Result.ALWAYS_TRUST:
137
throw new CertificateManagerError.UNTRUSTED("User declined");
140
debug("Pinning certificate for %s...", endpoint.remote.to_string());
142
yield this.pinning_database.pin_certificate(
143
endpoint.untrusted_certificate,
148
} catch (GLib.Error err) {
149
throw new CertificateManagerError.STORE_FAILED(err.message);
156
/** TLS database that observes locally pinned certs. */
157
private class Application.TlsDatabase : GLib.TlsDatabase {
160
/** A certificate and the identities it is trusted for. */
161
private class TrustContext : Geary.BaseObject {
164
// Perform IO at high priority since UI and network
165
// connections depend on it
166
private const int IO_PRIO = GLib.Priority.HIGH;
167
private const GLib.ChecksumType ID_TYPE = GLib.ChecksumType.SHA384;
168
private const string FILENAME_FORMAT = "%s.pem";
172
public GLib.TlsCertificate certificate;
175
public TrustContext(GLib.TlsCertificate certificate) {
176
this.id = GLib.Checksum.compute_for_data(
177
ID_TYPE, certificate.certificate.data
179
this.certificate = certificate;
182
public TrustContext.lookup(GLib.File dir,
184
GLib.Cancellable? cancellable)
186
// This isn't async so that we can support both
187
// verify_chain and verify_chain_async with the same call
188
GLib.File storage = dir.get_child(FILENAME_FORMAT.printf(identity));
189
GLib.FileInputStream f_in = storage.read(cancellable);
190
GLib.BufferedInputStream buf = new GLib.BufferedInputStream(f_in);
191
GLib.ByteArray cert_pem = new GLib.ByteArray.sized(buf.buffer_size);
194
size_t filled = buf.fill(-1, cancellable);
196
cert_pem.append(buf.peek_buffer());
197
buf.skip(filled, cancellable);
202
buf.close(cancellable);
204
this(new GLib.TlsCertificate.from_pem((string) cert_pem.data, -1));
207
public async void save(GLib.File dir,
209
GLib.Cancellable? cancellable)
211
yield Geary.Files.make_directory_with_parents(dir, cancellable);
212
GLib.File storage = dir.get_child(FILENAME_FORMAT.printf(identity));
213
GLib.FileOutputStream f_out = yield storage.replace_async(
214
null, false, GLib.FileCreateFlags.NONE, IO_PRIO, cancellable
216
GLib.BufferedOutputStream buf = new GLib.BufferedOutputStream(f_out);
219
yield buf.write_all_async(
220
this.certificate.certificate_pem.data,
225
yield buf.close_async(IO_PRIO, cancellable);
231
private static string to_name(GLib.SocketConnectable id) {
232
GLib.NetworkAddress? name = id as GLib.NetworkAddress;
234
return name.hostname;
237
GLib.NetworkService? service = id as GLib.NetworkService;
238
if (service != null) {
239
return service.domain;
242
GLib.InetSocketAddress? inet = id as GLib.InetSocketAddress;
244
return inet.address.to_string();
247
return id.to_string();
251
private GLib.TlsDatabase parent { get; private set; }
252
private GLib.File store_dir;
253
private bool use_gcr;
255
private Gee.Map<string,TrustContext> pinned_certs =
256
new Gee.HashMap<string,TrustContext>();
259
public TlsDatabase(GLib.TlsDatabase parent,
262
this.parent = parent;
263
this.store_dir = store_dir;
264
this.use_gcr = use_gcr;
267
public async void pin_certificate(GLib.TlsCertificate certificate,
268
GLib.SocketConnectable identity,
270
GLib.Cancellable? cancellable = null)
272
string id = to_name(identity);
273
TrustContext context = new TrustContext(certificate);
274
lock (this.pinned_certs) {
275
this.pinned_certs.set(id, context);
279
yield gcr_trust_add_pinned_certificate_async(
280
new Gcr.SimpleCertificate(certificate.certificate.data),
281
GLib.TlsDatabase.PURPOSE_AUTHENTICATE_SERVER,
287
this.store_dir, to_name(identity), cancellable
293
public override string?
294
create_certificate_handle(GLib.TlsCertificate certificate) {
295
TrustContext? context = lookup_tls_certificate(certificate);
296
return (context != null)
298
: this.parent.create_certificate_handle(certificate);
301
public override GLib.TlsCertificate?
302
lookup_certificate_for_handle(string handle,
303
GLib.TlsInteraction? interaction,
304
GLib.TlsDatabaseLookupFlags flags,
305
GLib.Cancellable? cancellable = null)
307
TrustContext? context = lookup_id(handle);
308
return (context != null)
309
? context.certificate
310
: this.parent.lookup_certificate_for_handle(
311
handle, interaction, flags, cancellable
315
public override async GLib.TlsCertificate
316
lookup_certificate_for_handle_async(string handle,
317
GLib.TlsInteraction? interaction,
318
GLib.TlsDatabaseLookupFlags flags,
319
GLib.Cancellable? cancellable = null)
321
TrustContext? context = lookup_id(handle);
322
return (context != null)
323
? context.certificate
324
: yield this.parent.lookup_certificate_for_handle_async(
325
handle, interaction, flags, cancellable
329
public override GLib.TlsCertificate
330
lookup_certificate_issuer(GLib.TlsCertificate certificate,
331
GLib.TlsInteraction? interaction,
332
GLib.TlsDatabaseLookupFlags flags,
333
GLib.Cancellable? cancellable = null)
335
return this.parent.lookup_certificate_issuer(
336
certificate, interaction, flags, cancellable
340
public override async GLib.TlsCertificate
341
lookup_certificate_issuer_async(GLib.TlsCertificate certificate,
342
GLib.TlsInteraction? interaction,
343
GLib.TlsDatabaseLookupFlags flags,
344
GLib.Cancellable? cancellable = null)
346
return yield this.parent.lookup_certificate_issuer_async(
347
certificate, interaction, flags, cancellable
351
public override GLib.List<GLib.TlsCertificate>
352
lookup_certificates_issued_by(ByteArray issuer_raw_dn,
353
GLib.TlsInteraction? interaction,
354
GLib.TlsDatabaseLookupFlags flags,
355
GLib.Cancellable? cancellable = null)
357
return this.parent.lookup_certificates_issued_by(
358
issuer_raw_dn, interaction, flags, cancellable
362
public override async GLib.List<GLib.TlsCertificate>
363
lookup_certificates_issued_by_async(GLib.ByteArray issuer_raw_dn,
364
GLib.TlsInteraction? interaction,
365
GLib.TlsDatabaseLookupFlags flags,
366
GLib.Cancellable? cancellable = null)
368
return yield this.parent.lookup_certificates_issued_by_async(
369
issuer_raw_dn, interaction, flags, cancellable
373
public override GLib.TlsCertificateFlags
374
verify_chain(GLib.TlsCertificate chain,
376
GLib.SocketConnectable? identity,
377
GLib.TlsInteraction? interaction,
378
GLib.TlsDatabaseVerifyFlags flags,
379
GLib.Cancellable? cancellable = null)
381
GLib.TlsCertificateFlags ret = this.parent.verify_chain(
382
chain, purpose, identity, interaction, flags, cancellable
384
if (should_verify(ret, purpose, identity) &&
385
verify(chain, identity, cancellable)) {
391
public override async GLib.TlsCertificateFlags
392
verify_chain_async(GLib.TlsCertificate chain,
394
GLib.SocketConnectable? identity,
395
GLib.TlsInteraction? interaction,
396
GLib.TlsDatabaseVerifyFlags flags,
397
GLib.Cancellable? cancellable = null)
399
GLib.TlsCertificateFlags ret = yield this.parent.verify_chain_async(
400
chain, purpose, identity, interaction, flags, cancellable
402
if (should_verify(ret, purpose, identity) &&
403
yield verify_async(chain, identity, cancellable)) {
409
private inline bool should_verify(GLib.TlsCertificateFlags parent_ret,
411
GLib.SocketConnectable? identity) {
412
// If the parent didn't verify, check for a locally pinned
413
// cert if it looks like we should, but always reject revoked
417
!(GLib.TlsCertificateFlags.REVOKED in parent_ret) &&
418
purpose == GLib.TlsDatabase.PURPOSE_AUTHENTICATE_SERVER &&
423
private bool verify(GLib.TlsCertificate chain,
424
GLib.SocketConnectable identity,
425
GLib.Cancellable? cancellable)
427
bool is_verified = false;
428
string id = to_name(identity);
429
TrustContext? context = null;
430
lock (this.pinned_certs) {
431
context = this.pinned_certs.get(id);
432
if (context != null) {
435
// Cert not found in memory, check with GCR if
438
is_verified = gcr_trust_is_certificate_pinned(
439
new Gcr.SimpleCertificate(chain.certificate.data),
440
GLib.TlsDatabase.PURPOSE_AUTHENTICATE_SERVER,
447
// Cert is not pinned in memory or in GCR, so look
448
// for it on disk. Do this even if GCR support is
449
// enabled, since if the cert was previously saved
450
// to disk, it should still be able to be used
452
context = new TrustContext.lookup(
453
this.store_dir, id, cancellable
455
this.pinned_certs.set(id, context);
457
} catch (GLib.IOError.NOT_FOUND err) {
458
// Cert was not found saved, so it not pinned
459
} catch (GLib.Error err) {
460
Geary.ErrorContext err_context =
461
new Geary.ErrorContext(err);
462
debug("Error loading pinned certificate: %s",
463
err_context.format_full_error());
471
private async bool verify_async(GLib.TlsCertificate chain,
472
GLib.SocketConnectable identity,
473
GLib.Cancellable? cancellable)
475
bool is_valid = false;
476
yield Geary.Nonblocking.Concurrent.global.schedule_async(() => {
477
is_valid = verify(chain, identity, cancellable);
482
private TrustContext? lookup_id(string id) {
483
lock (this.pinned_certs) {
484
return Geary.traverse(this.pinned_certs.values).first_matching(
485
(ctx) => ctx.id == id
490
private TrustContext? lookup_tls_certificate(GLib.TlsCertificate cert) {
491
lock (this.pinned_certs) {
492
return Geary.traverse(this.pinned_certs.values).first_matching(
493
(ctx) => ctx.certificate.is_same(cert)