~hkdb/geary/disco-3.34.1

« back to all changes in this revision

Viewing changes to src/client/application/application-certificate-manager.vala

  • Committer: hkdb
  • Date: 2019-10-08 10:54:21 UTC
  • Revision ID: hkdb@3df.io-20191008105421-3dkwnpnhcamm77to
Tags: upstream-3.34.1-disco
ImportĀ upstreamĀ versionĀ 3.34.1-disco

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
/*
 
2
 * Copyright 2016 Software Freedom Conservancy Inc.
 
3
 * Copyright 2019 Michael Gratton <mike@vee.net>
 
4
 *
 
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.
 
7
 */
 
8
 
 
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(
 
12
    Gcr.Certificate cert,
 
13
    string purpose,
 
14
    string peer,
 
15
    Cancellable? cancellable
 
16
) throws Error;
 
17
extern bool gcr_trust_is_certificate_pinned(
 
18
    Gcr.Certificate cert,
 
19
    string purpose,
 
20
    string peer,
 
21
    Cancellable? cancellable
 
22
) throws Error;
 
23
 
 
24
 
 
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.
 
30
 
 
31
/** Errors thrown by {@link CertificateManager}. */
 
32
public errordomain Application.CertificateManagerError {
 
33
 
 
34
    /** The certificate was not trusted by the user. */
 
35
    UNTRUSTED,
 
36
 
 
37
    /** The certificate could not be saved. */
 
38
    STORE_FAILED;
 
39
 
 
40
}
 
41
 
 
42
/**
 
43
 * Managing TLS certificate prompting and storage.
 
44
 */
 
45
public class Application.CertificateManager : GLib.Object {
 
46
 
 
47
 
 
48
    // PCKS11 flag value lifted from pkcs11.h
 
49
    private const ulong CKF_WRITE_PROTECTED = 1UL << 1;
 
50
 
 
51
 
 
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;
 
59
        try {
 
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);
 
63
        }
 
64
 
 
65
        bool has_uris = false;
 
66
        if (init_okay) {
 
67
            has_uris = (
 
68
                !Geary.String.is_empty(Gcr.pkcs11_get_trust_store_uri()) &&
 
69
                Gcr.pkcs11_get_trust_lookup_uris().length > 0
 
70
            );
 
71
            debug("GCR slot URIs found: %s", has_uris.to_string());
 
72
        }
 
73
 
 
74
        bool has_rw_store = false;
 
75
        if (has_uris) {
 
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());
 
79
        }
 
80
 
 
81
        return has_rw_store;
 
82
    }
 
83
 
 
84
 
 
85
    private TlsDatabase? pinning_database;
 
86
 
 
87
 
 
88
    /**
 
89
     * Constructs a new instance, globally installing the pinning database.
 
90
     */
 
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(),
 
96
            store_dir,
 
97
            use_gcr
 
98
        );
 
99
        Geary.Endpoint.default_tls_database = this.pinning_database;
 
100
    }
 
101
 
 
102
    /**
 
103
     * Destroys an instance, de-installs the pinning database.
 
104
     */
 
105
    ~CertificateManager() {
 
106
        Geary.Endpoint.default_tls_database = null;
 
107
    }
 
108
 
 
109
 
 
110
    /**
 
111
     * Prompts the user to trust the certificate for a service.
 
112
     *
 
113
     * Returns true if the user accepted the certificate.
 
114
     */
 
115
    public async void prompt_pin_certificate(Gtk.Window parent,
 
116
                                             Geary.AccountInformation account,
 
117
                                             Geary.ServiceInformation service,
 
118
                                             Geary.Endpoint endpoint,
 
119
                                             bool is_validation,
 
120
                                             GLib.Cancellable? cancellable)
 
121
        throws CertificateManagerError {
 
122
        CertificateWarningDialog dialog = new CertificateWarningDialog(
 
123
            parent, account, service, endpoint, is_validation
 
124
        );
 
125
 
 
126
        bool save = false;
 
127
        switch (dialog.run()) {
 
128
        case CertificateWarningDialog.Result.TRUST:
 
129
            // noop
 
130
            break;
 
131
 
 
132
        case CertificateWarningDialog.Result.ALWAYS_TRUST:
 
133
            save = true;
 
134
            break;
 
135
 
 
136
        default:
 
137
            throw new CertificateManagerError.UNTRUSTED("User declined");
 
138
        }
 
139
 
 
140
        debug("Pinning certificate for %s...", endpoint.remote.to_string());
 
141
        try {
 
142
            yield this.pinning_database.pin_certificate(
 
143
                endpoint.untrusted_certificate,
 
144
                endpoint.remote,
 
145
                save,
 
146
                cancellable
 
147
            );
 
148
        } catch (GLib.Error err) {
 
149
            throw new CertificateManagerError.STORE_FAILED(err.message);
 
150
        }
 
151
    }
 
152
 
 
153
}
 
154
 
 
155
 
 
156
/** TLS database that observes locally pinned certs. */
 
157
private class Application.TlsDatabase : GLib.TlsDatabase {
 
158
 
 
159
 
 
160
    /** A certificate and the identities it is trusted for. */
 
161
    private class TrustContext : Geary.BaseObject {
 
162
 
 
163
 
 
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";
 
169
 
 
170
 
 
171
        public string id;
 
172
        public GLib.TlsCertificate certificate;
 
173
 
 
174
 
 
175
        public TrustContext(GLib.TlsCertificate certificate) {
 
176
            this.id = GLib.Checksum.compute_for_data(
 
177
                ID_TYPE, certificate.certificate.data
 
178
            );
 
179
            this.certificate = certificate;
 
180
        }
 
181
 
 
182
        public TrustContext.lookup(GLib.File dir,
 
183
                                   string identity,
 
184
                                   GLib.Cancellable? cancellable)
 
185
            throws GLib.Error {
 
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);
 
192
            bool eof = false;
 
193
            while (!eof) {
 
194
                size_t filled = buf.fill(-1, cancellable);
 
195
                if (filled > 0) {
 
196
                    cert_pem.append(buf.peek_buffer());
 
197
                    buf.skip(filled, cancellable);
 
198
                } else {
 
199
                    eof = true;
 
200
                }
 
201
            }
 
202
            buf.close(cancellable);
 
203
 
 
204
            this(new GLib.TlsCertificate.from_pem((string) cert_pem.data, -1));
 
205
        }
 
206
 
 
207
        public async void save(GLib.File dir,
 
208
                               string identity,
 
209
                               GLib.Cancellable? cancellable)
 
210
            throws GLib.Error {
 
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
 
215
            );
 
216
            GLib.BufferedOutputStream buf = new GLib.BufferedOutputStream(f_out);
 
217
 
 
218
            size_t written = 0;
 
219
            yield buf.write_all_async(
 
220
                this.certificate.certificate_pem.data,
 
221
                IO_PRIO,
 
222
                cancellable,
 
223
                out written
 
224
            );
 
225
            yield buf.close_async(IO_PRIO, cancellable);
 
226
        }
 
227
 
 
228
    }
 
229
 
 
230
 
 
231
    private static string to_name(GLib.SocketConnectable id) {
 
232
        GLib.NetworkAddress? name = id as GLib.NetworkAddress;
 
233
        if (name != null) {
 
234
            return name.hostname;
 
235
        }
 
236
 
 
237
        GLib.NetworkService? service = id as GLib.NetworkService;
 
238
        if (service != null) {
 
239
            return service.domain;
 
240
        }
 
241
 
 
242
        GLib.InetSocketAddress? inet = id as GLib.InetSocketAddress;
 
243
        if (inet != null) {
 
244
            return inet.address.to_string();
 
245
        }
 
246
 
 
247
        return id.to_string();
 
248
    }
 
249
 
 
250
 
 
251
    private GLib.TlsDatabase parent { get; private set; }
 
252
    private GLib.File store_dir;
 
253
    private bool use_gcr;
 
254
 
 
255
    private Gee.Map<string,TrustContext> pinned_certs =
 
256
        new Gee.HashMap<string,TrustContext>();
 
257
 
 
258
 
 
259
        public TlsDatabase(GLib.TlsDatabase parent,
 
260
                           GLib.File store_dir,
 
261
                           bool use_gcr) {
 
262
        this.parent = parent;
 
263
        this.store_dir = store_dir;
 
264
        this.use_gcr = use_gcr;
 
265
    }
 
266
 
 
267
    public async void pin_certificate(GLib.TlsCertificate certificate,
 
268
                                      GLib.SocketConnectable identity,
 
269
                                      bool save,
 
270
                                      GLib.Cancellable? cancellable = null)
 
271
        throws GLib.Error {
 
272
        string id = to_name(identity);
 
273
        TrustContext context = new TrustContext(certificate);
 
274
        lock (this.pinned_certs) {
 
275
            this.pinned_certs.set(id, context);
 
276
        }
 
277
        if (save) {
 
278
            if (this.use_gcr) {
 
279
                yield gcr_trust_add_pinned_certificate_async(
 
280
                    new Gcr.SimpleCertificate(certificate.certificate.data),
 
281
                    GLib.TlsDatabase.PURPOSE_AUTHENTICATE_SERVER,
 
282
                    id,
 
283
                    cancellable
 
284
                );
 
285
            } else {
 
286
                yield context.save(
 
287
                    this.store_dir, to_name(identity), cancellable
 
288
                );
 
289
            }
 
290
        }
 
291
    }
 
292
 
 
293
    public override string?
 
294
        create_certificate_handle(GLib.TlsCertificate certificate) {
 
295
        TrustContext? context = lookup_tls_certificate(certificate);
 
296
        return (context != null)
 
297
            ? context.id
 
298
            : this.parent.create_certificate_handle(certificate);
 
299
    }
 
300
 
 
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)
 
306
        throws GLib.Error {
 
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
 
312
            );
 
313
    }
 
314
 
 
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)
 
320
        throws GLib.Error {
 
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
 
326
            );
 
327
    }
 
328
 
 
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)
 
334
        throws GLib.Error {
 
335
        return this.parent.lookup_certificate_issuer(
 
336
            certificate, interaction, flags, cancellable
 
337
        );
 
338
    }
 
339
 
 
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)
 
345
        throws GLib.Error {
 
346
        return yield this.parent.lookup_certificate_issuer_async(
 
347
            certificate, interaction, flags, cancellable
 
348
        );
 
349
    }
 
350
 
 
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)
 
356
        throws GLib.Error {
 
357
        return this.parent.lookup_certificates_issued_by(
 
358
            issuer_raw_dn, interaction, flags, cancellable
 
359
        );
 
360
    }
 
361
 
 
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)
 
367
        throws GLib.Error {
 
368
        return yield this.parent.lookup_certificates_issued_by_async(
 
369
            issuer_raw_dn, interaction, flags, cancellable
 
370
        );
 
371
    }
 
372
 
 
373
    public override GLib.TlsCertificateFlags
 
374
        verify_chain(GLib.TlsCertificate chain,
 
375
                     string purpose,
 
376
                     GLib.SocketConnectable? identity,
 
377
                     GLib.TlsInteraction? interaction,
 
378
                     GLib.TlsDatabaseVerifyFlags flags,
 
379
                     GLib.Cancellable? cancellable = null)
 
380
        throws GLib.Error {
 
381
        GLib.TlsCertificateFlags ret = this.parent.verify_chain(
 
382
            chain, purpose, identity, interaction, flags, cancellable
 
383
        );
 
384
        if (should_verify(ret, purpose, identity) &&
 
385
            verify(chain, identity, cancellable)) {
 
386
            ret = 0;
 
387
        }
 
388
        return ret;
 
389
    }
 
390
 
 
391
    public override async GLib.TlsCertificateFlags
 
392
        verify_chain_async(GLib.TlsCertificate chain,
 
393
                           string purpose,
 
394
                           GLib.SocketConnectable? identity,
 
395
                           GLib.TlsInteraction? interaction,
 
396
                           GLib.TlsDatabaseVerifyFlags flags,
 
397
                           GLib.Cancellable? cancellable = null)
 
398
        throws GLib.Error {
 
399
        GLib.TlsCertificateFlags ret = yield this.parent.verify_chain_async(
 
400
            chain, purpose, identity, interaction, flags, cancellable
 
401
        );
 
402
        if (should_verify(ret, purpose, identity) &&
 
403
            yield verify_async(chain, identity, cancellable)) {
 
404
            ret = 0;
 
405
        }
 
406
        return ret;
 
407
    }
 
408
 
 
409
    private inline bool should_verify(GLib.TlsCertificateFlags parent_ret,
 
410
                                      string purpose,
 
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
 
414
        // certs
 
415
        return (
 
416
            parent_ret != 0 &&
 
417
            !(GLib.TlsCertificateFlags.REVOKED in parent_ret) &&
 
418
            purpose == GLib.TlsDatabase.PURPOSE_AUTHENTICATE_SERVER &&
 
419
            identity != null
 
420
        );
 
421
    }
 
422
 
 
423
    private bool verify(GLib.TlsCertificate chain,
 
424
                        GLib.SocketConnectable identity,
 
425
                        GLib.Cancellable? cancellable)
 
426
        throws GLib.Error {
 
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) {
 
433
                is_verified = true;
 
434
            } else {
 
435
                // Cert not found in memory, check with GCR if
 
436
                // enabled.
 
437
                if (this.use_gcr) {
 
438
                    is_verified = gcr_trust_is_certificate_pinned(
 
439
                        new Gcr.SimpleCertificate(chain.certificate.data),
 
440
                        GLib.TlsDatabase.PURPOSE_AUTHENTICATE_SERVER,
 
441
                        id,
 
442
                        cancellable
 
443
                    );
 
444
                }
 
445
 
 
446
                if (!is_verified) {
 
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
 
451
                    try {
 
452
                        context = new TrustContext.lookup(
 
453
                            this.store_dir, id, cancellable
 
454
                        );
 
455
                        this.pinned_certs.set(id, context);
 
456
                        is_verified = true;
 
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());
 
464
                    }
 
465
                }
 
466
            }
 
467
        }
 
468
        return is_verified;
 
469
    }
 
470
 
 
471
    private async bool verify_async(GLib.TlsCertificate chain,
 
472
                                    GLib.SocketConnectable identity,
 
473
                                    GLib.Cancellable? cancellable)
 
474
        throws GLib.Error {
 
475
        bool is_valid = false;
 
476
        yield Geary.Nonblocking.Concurrent.global.schedule_async(() => {
 
477
                is_valid = verify(chain, identity, cancellable);
 
478
            }, cancellable);
 
479
        return is_valid;
 
480
    }
 
481
 
 
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
 
486
            );
 
487
        }
 
488
    }
 
489
 
 
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)
 
494
            );
 
495
        }
 
496
    }
 
497
 
 
498
}