1# -*- coding: utf-8 -*-
2"""QGIS Unit tests for bindings to core authentication system classes
3
4From build dir: LC_ALL=en_US.UTF-8 ctest -R PyQgsAuthenticationSystem -V
5
6.. note:: This program is free software; you can redistribute it and/or modify
7it under the terms of the GNU General Public License as published by
8the Free Software Foundation; either version 2 of the License, or
9(at your option) any later version.
10"""
11__author__ = 'Larry Shaffer'
12__date__ = '2014/11/05'
13__copyright__ = 'Copyright 2014, Boundless Spatial, Inc.'
14
15import os
16import tempfile
17
18from qgis.core import QgsAuthCertUtils, QgsPkiBundle, QgsAuthMethodConfig, QgsAuthMethod, QgsAuthConfigSslServer, QgsApplication
19from qgis.gui import QgsAuthEditorWidgets
20from qgis.PyQt.QtCore import QFileInfo, qDebug
21from qgis.PyQt.QtNetwork import QSsl, QSslError, QSslCertificate, QSslSocket
22from qgis.PyQt.QtTest import QTest
23from qgis.PyQt.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout
24from qgis.testing import start_app, unittest
25
26from utilities import unitTestDataPath
27
28AUTHDBDIR = tempfile.mkdtemp()
29os.environ['QGIS_AUTH_DB_DIR_PATH'] = AUTHDBDIR
30start_app()
31
32TESTDATA = os.path.join(unitTestDataPath(), 'auth_system')
33PKIDATA = os.path.join(TESTDATA, 'certs_keys')
34
35
36class TestQgsAuthManager(unittest.TestCase):
37
38    @classmethod
39    def setUpClass(cls):
40        cls.authm = QgsApplication.authManager()
41        assert not cls.authm.isDisabled(), cls.authm.disabledMessage()
42
43        cls.mpass = 'pass'  # master password
44
45        db1 = QFileInfo(cls.authm.authenticationDatabasePath()).canonicalFilePath()
46        db2 = QFileInfo(AUTHDBDIR + '/qgis-auth.db').canonicalFilePath()
47        msg = 'Auth db temp path does not match db path of manager'
48        assert db1 == db2, msg
49
50    def setUp(self):
51        testid = self.id().split('.')
52        testheader = '\n#####_____ {0}.{1} _____#####\n'. \
53            format(testid[1], testid[2])
54        qDebug(testheader)
55
56        if (not self.authm.masterPasswordIsSet() or
57                not self.authm.masterPasswordHashInDatabase()):
58            self.set_master_password()
59
60    def widget_dialog(self, widget):
61        dlg = QDialog()
62        widget.setParent(dlg)
63        layout = QVBoxLayout()
64        layout.addWidget(widget)
65        layout.setMargin(6)
66        button_box = QDialogButtonBox(QDialogButtonBox.Close)
67        button_box.rejected.connect(dlg.close)
68        layout.addWidget(button_box)
69        dlg.setLayout(layout)
70        return dlg
71
72    def mkPEMBundle(self, client_cert, client_key, password, chain):
73        return QgsPkiBundle.fromPemPaths(PKIDATA + '/' + client_cert,
74                                         PKIDATA + '/' + client_key,
75                                         password,
76                                         QgsAuthCertUtils.certsFromFile(
77                                             PKIDATA + '/' + chain
78                                         ))
79
80    def show_editors_widget(self):
81        editors = QgsAuthEditorWidgets()
82        dlg = self.widget_dialog(editors)
83        dlg.exec_()
84
85    def set_master_password(self):
86        msg = 'Failed to store and verify master password in auth db'
87        assert self.authm.setMasterPassword(self.mpass, True), msg
88
89    def test_010_master_password(self):
90        msg = 'Master password is not set'
91        self.assertTrue(self.authm.masterPasswordIsSet(), msg)
92
93        msg = 'Master password hash is not in database'
94        self.assertTrue(self.authm.masterPasswordHashInDatabase(), msg)
95
96        msg = 'Master password not verified against hash in database'
97        self.assertTrue(self.authm.verifyMasterPassword(), msg)
98
99        msg = 'Master password comparison dissimilar'
100        self.assertTrue(self.authm.masterPasswordSame(self.mpass), msg)
101
102        msg = 'Master password not unset'
103        self.authm.clearMasterPassword()
104        self.assertFalse(self.authm.masterPasswordIsSet(), msg)
105
106        msg = 'Master password not reset and validated'
107        self.assertTrue(self.authm.setMasterPassword(self.mpass, True), msg)
108
109        # NOTE: reset of master password is in auth db test unit
110
111    def test_020_cert_utilities(self):
112        pass
113
114    def test_030_auth_settings(self):
115        pass
116
117    def test_040_authorities(self):
118
119        def rebuild_caches():
120            m = 'Authorities cache could not be rebuilt'
121            self.assertTrue(self.authm.rebuildCaCertsCache(), m)
122
123            m = 'Authorities trust policy cache could not be rebuilt'
124            self.assertTrue(self.authm.rebuildTrustedCaCertsCache(), m)
125
126        def trusted_ca_certs():
127            tr_certs = self.authm.trustedCaCerts()
128            m = 'Trusted authorities cache is empty'
129            self.assertIsNotNone(tr_certs, m)
130            return tr_certs
131
132        msg = 'No system root CAs'
133        self.assertIsNotNone(self.authm.systemRootCAs())
134
135        # TODO: add more tests
136        full_chain = 'chains_subissuer-issuer-root_issuer2-root2.pem'
137        full_chain_path = os.path.join(PKIDATA, full_chain)
138
139        # load CA file authorities for later comparison
140        # noinspection PyTypeChecker
141        # ca_certs = QSslCertificate.fromPath(full_chain_path)
142        ca_certs = QgsAuthCertUtils.certsFromFile(full_chain_path)
143        msg = 'Authorities file could not be parsed'
144        self.assertIsNotNone(ca_certs, msg)
145
146        msg = 'Authorities file parsed count is incorrect'
147        self.assertEqual(len(ca_certs), 5, msg)
148
149        # first test CA file can be set and loaded
150        msg = 'Authority file path setting could not be stored'
151        self.assertTrue(
152            self.authm.storeAuthSetting('cafile', full_chain_path), msg)
153
154        msg = "Authority file 'allow invalids' setting could not be stored"
155        self.assertTrue(
156            self.authm.storeAuthSetting('cafileallowinvalid', False), msg)
157
158        rebuild_caches()
159        trusted_certs = trusted_ca_certs()
160
161        not_cached = any([ca not in trusted_certs for ca in ca_certs])
162        msg = 'Authorities not in trusted authorities cache'
163        self.assertFalse(not_cached, msg)
164
165        # test CA file can be unset
166        msg = 'Authority file path setting could not be removed'
167        self.assertTrue(self.authm.removeAuthSetting('cafile'), msg)
168
169        msg = "Authority file 'allow invalids' setting could not be removed"
170        self.assertTrue(
171            self.authm.removeAuthSetting('cafileallowinvalid'), msg)
172
173        rebuild_caches()
174        trusted_certs = trusted_ca_certs()
175
176        still_cached = any([ca in trusted_certs for ca in ca_certs])
177        msg = 'Authorities still in trusted authorities cache'
178        self.assertFalse(still_cached, msg)
179
180        # test CAs can be stored in database
181        msg = "Authority certs could not be stored in database"
182        self.assertTrue(self.authm.storeCertAuthorities(ca_certs))
183
184        rebuild_caches()
185        trusted_certs = trusted_ca_certs()
186
187        not_cached = any([ca not in trusted_certs for ca in ca_certs])
188        msg = 'Stored authorities not in trusted authorities cache'
189        self.assertFalse(not_cached, msg)
190
191        # dlg = QgsAuthTrustedCAsDialog()
192        # dlg.exec_()
193
194    def test_050_trust_policy(self):
195        pass
196
197    # noinspection PyArgumentList
198    def test_060_identities(self):
199        client_cert_path = os.path.join(PKIDATA, 'fra_cert.pem')
200        client_key_path = os.path.join(PKIDATA, 'fra_key_w-pass.pem')
201        client_key_pass = 'password'
202        client_p12_path = os.path.join(PKIDATA, 'gerardus_w-chain.p12')
203        client_p12_pass = 'password'
204
205        # store regular PEM cert/key and generate config
206        # noinspection PyTypeChecker
207        bundle1 = QgsPkiBundle.fromPemPaths(client_cert_path, client_key_path,
208                                            client_key_pass)
209        bundle1_cert = bundle1.clientCert()
210        bundle1_key = bundle1.clientKey()
211        bundle1_ca_chain = bundle1.caChain()
212        bundle1_cert_sha = bundle1.certId()
213
214        # with open(client_key_path, 'r') as f:
215        #     key_data = f.read()
216        #
217        # client_cert = QgsAuthCertUtils.certsFromFile(client_cert_path)[0]
218        msg = 'Identity PEM certificate is null'
219        self.assertFalse(bundle1_cert.isNull(), msg)
220
221        # cert_sha = QgsAuthCertUtils.shaHexForCert(client_cert)
222        #
223        # client_key = QSslKey(key_data, QSsl.Rsa, QSsl.Pem,
224        #                      QSsl.PrivateKey, client_key_pass)
225        msg = 'Identity PEM key is null'
226        self.assertFalse(bundle1_key.isNull(), msg)
227
228        msg = 'Identity PEM certificate chain is not empty'
229        self.assertEqual(len(bundle1_ca_chain), 0, msg)
230
231        msg = "Identity PEM could not be stored in database"
232        self.assertTrue(
233            self.authm.storeCertIdentity(bundle1_cert, bundle1_key), msg)
234
235        msg = "Identity PEM not found in database"
236        self.assertTrue(self.authm.existsCertIdentity(bundle1_cert_sha), msg)
237
238        config1 = QgsAuthMethodConfig()
239        config1.setName('IdentityCert - PEM')
240        config1.setMethod('Identity-Cert')
241        config1.setConfig('certid', bundle1_cert_sha)
242
243        msg = 'Could not store PEM identity config'
244        self.assertTrue(self.authm.storeAuthenticationConfig(config1), msg)
245
246        configid1 = config1.id()
247        msg = 'Could not retrieve PEM identity config id from store op'
248        self.assertIsNotNone(configid1, msg)
249
250        config2 = QgsAuthMethodConfig()
251        msg = 'Could not load PEM identity config'
252        self.assertTrue(
253            self.authm.loadAuthenticationConfig(configid1, config2, True),
254            msg)
255
256        # store PKCS#12 bundled cert/key and generate config
257        # bundle = QgsPkcsBundle(client_p12_path, client_p12_pass)
258        # noinspection PyTypeChecker
259        bundle = QgsPkiBundle.fromPkcs12Paths(client_p12_path, client_p12_pass)
260        bundle_cert = bundle.clientCert()
261        bundle_key = bundle.clientKey()
262        bundle_ca_chain = bundle.caChain()
263        bundle_cert_sha = QgsAuthCertUtils.shaHexForCert(bundle_cert)
264
265        msg = 'Identity bundle certificate is null'
266        self.assertFalse(bundle_cert.isNull(), msg)
267
268        msg = 'Identity bundle key is null'
269        self.assertFalse(bundle_key.isNull(), msg)
270
271        msg = 'Identity bundle CA chain is not correct depth'
272        self.assertEqual(len(bundle_ca_chain), 3, msg)
273
274        msg = "Identity bundle could not be stored in database"
275        self.assertTrue(
276            self.authm.storeCertIdentity(bundle_cert, bundle_key), msg)
277
278        msg = "Identity bundle not found in database"
279        self.assertTrue(self.authm.existsCertIdentity(bundle_cert_sha), msg)
280
281        bundle_config = QgsAuthMethodConfig()
282        bundle_config.setName('IdentityCert - Bundle')
283        bundle_config.setMethod('Identity-Cert')
284        bundle_config.setConfig('certid', bundle_cert_sha)
285
286        msg = 'Could not store bundle identity config'
287        self.assertTrue(
288            self.authm.storeAuthenticationConfig(bundle_config), msg)
289
290        bundle_configid = bundle_config.id()
291        msg = 'Could not retrieve bundle identity config id from store op'
292        self.assertIsNotNone(bundle_configid, msg)
293
294        bundle_config2 = QgsAuthMethodConfig()
295        msg = 'Could not load bundle identity config'
296        self.assertTrue(
297            self.authm.loadAuthenticationConfig(bundle_configid,
298                                                bundle_config2,
299                                                True),
300            msg)
301
302        # TODO: add more tests
303        # self.show_editors_widget()
304
305        msg = 'Could not remove PEM identity config'
306        self.assertTrue(self.authm.removeAuthenticationConfig(configid1), msg)
307
308        msg = 'Could not remove bundle identity config'
309        self.assertTrue(
310            self.authm.removeAuthenticationConfig(bundle_configid), msg)
311
312    def test_070_servers(self):
313        # return
314        ssl_cert_path = os.path.join(PKIDATA, 'localhost_ssl_cert.pem')
315
316        ssl_cert = QgsAuthCertUtils.certsFromFile(ssl_cert_path)[0]
317        msg = 'SSL server certificate is null'
318        self.assertFalse(ssl_cert.isNull(), msg)
319
320        cert_sha = QgsAuthCertUtils.shaHexForCert(ssl_cert)
321
322        hostport = 'localhost:8443'
323        config = QgsAuthConfigSslServer()
324        config.setSslCertificate(ssl_cert)
325        config.setSslHostPort(hostport)
326        config.setSslIgnoredErrorEnums([QSslError.SelfSignedCertificate])
327        config.setSslPeerVerifyMode(QSslSocket.VerifyNone)
328        config.setSslPeerVerifyDepth(3)
329        config.setSslProtocol(QSsl.TlsV1_1)
330
331        msg = 'SSL config is null'
332        self.assertFalse(config.isNull(), msg)
333
334        msg = 'Could not store SSL config'
335        self.assertTrue(self.authm.storeSslCertCustomConfig(config), msg)
336
337        msg = 'Could not verify storage of SSL config'
338        self.assertTrue(
339            self.authm.existsSslCertCustomConfig(cert_sha, hostport), msg)
340
341        msg = 'Could not verify SSL config in all configs'
342        self.assertIsNotNone(self.authm.sslCertCustomConfigs(), msg)
343
344        msg = 'Could not retrieve SSL config'
345        config2 = self.authm.sslCertCustomConfig(cert_sha, hostport)
346        """:type: QgsAuthConfigSslServer"""
347        self.assertFalse(config2.isNull(), msg)
348
349        msg = 'Certificate of retrieved SSL config does not match'
350        self.assertEqual(config.sslCertificate(), config2.sslCertificate(), msg)
351
352        msg = 'HostPort of retrieved SSL config does not match'
353        self.assertEqual(config.sslHostPort(), config2.sslHostPort(), msg)
354
355        msg = 'IgnoredErrorEnums of retrieved SSL config does not match'
356        enums = config2.sslIgnoredErrorEnums()
357        self.assertTrue(QSslError.SelfSignedCertificate in enums, msg)
358
359        msg = 'PeerVerifyMode of retrieved SSL config does not match'
360        self.assertEqual(config.sslPeerVerifyMode(),
361                         config2.sslPeerVerifyMode(), msg)
362
363        msg = 'PeerVerifyDepth of retrieved SSL config does not match'
364        self.assertEqual(config.sslPeerVerifyDepth(),
365                         config2.sslPeerVerifyDepth(), msg)
366
367        msg = 'Protocol of retrieved SSL config does not match'
368        self.assertEqual(config.sslProtocol(), config2.sslProtocol(), msg)
369
370        # dlg = QgsAuthSslConfigDialog(None, ssl_cert, hostport)
371        # dlg.exec_()
372
373        msg = 'Could not remove SSL config'
374        self.assertTrue(
375            self.authm.removeSslCertCustomConfig(cert_sha, hostport), msg)
376
377        msg = 'Could not verify removal of SSL config'
378        self.assertFalse(
379            self.authm.existsSslCertCustomConfig(cert_sha, hostport), msg)
380
381    def test_080_auth_configid(self):
382        msg = 'Could not generate a config id'
383        self.assertIsNotNone(self.authm.uniqueConfigId(), msg)
384
385        uids = []
386        for _ in range(50):
387            # time.sleep(0.01)  # or else the salt is not random enough
388            uids.append(self.authm.uniqueConfigId())
389        msg = 'Generated 50 config ids are not unique:\n{0}\n{1}'.format(
390            uids,
391            list(set(uids))
392        )
393        self.assertEqual(len(uids), len(list(set(uids))), msg)
394
395    def config_list(self):
396        return ['Basic', 'PKI-Paths', 'PKI-PKCS#12']
397
398    def config_obj(self, kind, base=True):
399        config = QgsAuthMethodConfig()
400        config.setName(kind)
401        config.setMethod(kind)
402        config.setUri('http://example.com')
403        if base:
404            return config
405
406        if kind == 'Basic':
407            config.setConfig('username', 'username')
408            config.setConfig('password', 'password')
409            config.setConfig('realm', 'Realm')
410        elif kind == 'PKI-Paths':
411            config.setConfig('certpath',
412                             os.path.join(PKIDATA, 'gerardus_cert.pem'))
413            config.setConfig('keypath',
414                             os.path.join(PKIDATA, 'gerardus_key_w-pass.pem'))
415            config.setConfig('keypass', 'password')
416        elif kind == 'PKI-PKCS#12':
417            config.setConfig('bundlepath',
418                             os.path.join(PKIDATA, 'gerardus.p12'))
419            config.setConfig('bundlepass', 'password')
420
421        return config
422
423    def config_values_valid(self, kind, config):
424        """:type config: QgsAuthMethodConfig"""
425        if (config.name() != kind or
426                config.method() != kind or
427                config.uri() != 'http://example.com'):
428            return False
429        if kind == 'Basic':
430            return (
431                config.config('username') == 'username' and
432                config.config('password') == 'password' and
433                config.config('realm') == 'Realm'
434            )
435        elif kind == 'PKI-Paths':
436            return (
437                config.config('certpath') ==
438                os.path.join(PKIDATA, 'gerardus_cert.pem') and
439                config.config('keypath') ==
440                os.path.join(PKIDATA, 'gerardus_key_w-pass.pem') and
441                config.config('keypass') == 'password'
442            )
443        elif kind == 'PKI-PKCS#12':
444            return (
445                config.config('bundlepath') ==
446                os.path.join(PKIDATA, 'gerardus.p12') and
447                config.config('bundlepass') == 'password'
448            )
449
450    def test_090_auth_configs(self):
451        # these list items need to match the QgsAuthType provider type strings
452        for kind in self.config_list():
453            config = self.config_obj(kind, base=False)
454            msg = 'Could not validate {0} config'.format(kind)
455            self.assertTrue(config.isValid(), msg)
456
457            msg = 'Could not store {0} config'.format(kind)
458            self.assertTrue(self.authm.storeAuthenticationConfig(config), msg)
459
460            configid = config.id()
461            msg = 'Could not retrieve {0} config id from store op'.format(kind)
462            self.assertIsNotNone(configid, msg)
463
464            msg = 'Config id {0} not in db'.format(configid)
465            self.assertFalse(self.authm.configIdUnique(configid), msg)
466
467            msg = 'Could not retrieve {0} config id from db'.format(kind)
468            self.assertTrue(configid in self.authm.configIds(), msg)
469
470            msg = 'Could not retrieve method key for {0} config'.format(kind)
471            self.assertTrue(
472                self.authm.configAuthMethodKey(configid) == kind, msg)
473
474            msg = 'Could not retrieve method ptr for {0} config'.format(kind)
475            self.assertTrue(
476                isinstance(self.authm.configAuthMethod(configid),
477                           QgsAuthMethod), msg)
478
479            config2 = self.config_obj(kind, base=True)
480            msg = 'Could not load {0} config'.format(kind)
481            self.assertTrue(
482                self.authm.loadAuthenticationConfig(configid, config2, True),
483                msg)
484
485            msg = 'Could not validate loaded {0} config values'.format(kind)
486            self.assertTrue(self.config_values_valid(kind, config2), msg)
487
488            # values haven't been changed, but the db update still takes place
489            msg = 'Could not update {0} config values'.format(kind)
490            self.assertTrue(self.authm.updateAuthenticationConfig(config2), msg)
491
492            config3 = self.config_obj(kind, base=True)
493            msg = 'Could not load updated {0} config'.format(kind)
494            self.assertTrue(
495                self.authm.loadAuthenticationConfig(configid, config3, True),
496                msg)
497
498            msg = 'Could not validate updated {0} config values'.format(kind)
499            self.assertTrue(self.config_values_valid(kind, config3), msg)
500
501            msg = 'Could not remove {0} config (by id) from db'.format(kind)
502            self.assertTrue(
503                self.authm.removeAuthenticationConfig(configid), msg)
504
505            msg = 'Did not remove {0} config id from db'.format(kind)
506            self.assertFalse(configid in self.authm.configIds(), msg)
507
508    def test_100_auth_db(self):
509
510        for kind in self.config_list():
511            config = self.config_obj(kind, base=False)
512            msg = 'Could not store {0} config'.format(kind)
513            self.assertTrue(self.authm.storeAuthenticationConfig(config), msg)
514
515        msg = 'Could not store a sample of all configs in auth db'
516        self.assertTrue(
517            (len(self.authm.configIds()) == len(self.config_list())), msg)
518
519        msg = 'Could not retrieve available configs from auth db'
520        self.assertTrue(len(self.authm.availableAuthMethodConfigs()) > 0, msg)
521
522        backup = None
523        resetpass, backup = self.authm.resetMasterPassword(
524            'newpass', self.mpass, True, backup)
525        msg = 'Could not reset master password and/or re-encrypt configs'
526        self.assertTrue(resetpass, msg)
527
528        # qDebug('Backup db path: {0}'.format(backup))
529        msg = 'Could not retrieve backup path for reset master password op'
530        self.assertIsNotNone(backup)
531        self.assertTrue(backup != self.authm.authenticationDatabasePath(), msg)
532
533        msg = 'Could not verify reset master password'
534        self.assertTrue(self.authm.setMasterPassword('newpass', True), msg)
535
536        msg = 'Could not remove all configs from auth db'
537        self.assertTrue(self.authm.removeAllAuthenticationConfigs(), msg)
538
539        msg = 'Configs were not removed from auth db'
540        self.assertTrue(len(self.authm.configIds()) == 0, msg)
541
542        msg = 'Auth db does not exist'
543        self.assertTrue(os.path.exists(self.authm.authenticationDatabasePath()), msg)
544
545        QTest.qSleep(1000)  # necessary for new backup to have different name
546
547        msg = 'Could not erase auth db'
548        backup = None
549        reserase, backup = \
550            self.authm.eraseAuthenticationDatabase(True, backup)
551        self.assertTrue(reserase, msg)
552
553        # qDebug('Erase db backup db path: {0}'.format(backup))
554        msg = 'Could not retrieve backup path for erase db op'
555        self.assertIsNotNone(backup)
556        self.assertTrue(backup != self.authm.authenticationDatabasePath(), msg)
557
558        msg = 'Master password not erased from auth db'
559        self.assertTrue(not self.authm.masterPasswordIsSet() and
560                        not self.authm.masterPasswordHashInDatabase(), msg)
561
562        self.set_master_password()
563
564    def test_110_pkcs12_cas(self):
565        """Test if CAs can be read from a pkcs12 bundle"""
566        path = PKIDATA + '/fra_w-chain.p12'
567        cas = QgsAuthCertUtils.pkcs12BundleCas(path, 'password')
568
569        self.assertEqual(cas[0].issuerInfo(b'CN'), ['QGIS Test Root CA'])
570        self.assertEqual(cas[0].subjectInfo(b'CN'), ['QGIS Test Issuer CA'])
571        self.assertEqual(cas[0].serialNumber(), b'02')
572        self.assertEqual(cas[1].issuerInfo(b'CN'), ['QGIS Test Root CA'])
573        self.assertEqual(cas[1].subjectInfo(b'CN'), ['QGIS Test Root CA'])
574        self.assertEqual(cas[1].serialNumber(), b'01')
575
576    def test_120_pem_cas_from_file(self):
577        """Test if CAs can be read from a pem bundle"""
578        path = PKIDATA + '/fra_w-chain.pem'
579        cas = QgsAuthCertUtils.casFromFile(path)
580
581        self.assertEqual(cas[0].issuerInfo(b'CN'), ['QGIS Test Root CA'])
582        self.assertEqual(cas[0].subjectInfo(b'CN'), ['QGIS Test Issuer CA'])
583        self.assertEqual(cas[0].serialNumber(), b'02')
584        self.assertEqual(cas[1].issuerInfo(b'CN'), ['QGIS Test Root CA'])
585        self.assertEqual(cas[1].subjectInfo(b'CN'), ['QGIS Test Root CA'])
586        self.assertEqual(cas[1].serialNumber(), b'01')
587
588    def test_130_cas_merge(self):
589        """Test CAs merge """
590        trusted_path = PKIDATA + '/subissuer_ca_cert.pem'
591        extra_path = PKIDATA + '/fra_w-chain.pem'
592
593        trusted = QgsAuthCertUtils.casFromFile(trusted_path)
594        extra = QgsAuthCertUtils.casFromFile(extra_path)
595        merged = QgsAuthCertUtils.casMerge(trusted, extra)
596
597        self.assertEqual(len(trusted), 1)
598        self.assertEqual(len(extra), 2)
599        self.assertEqual(len(merged), 3)
600
601        for c in extra:
602            self.assertTrue(c in merged)
603
604        self.assertTrue(trusted[0] in merged)
605
606    def test_140_cas_remove_self_signed(self):
607        """Test CAs merge """
608        extra_path = PKIDATA + '/fra_w-chain.pem'
609
610        extra = QgsAuthCertUtils.casFromFile(extra_path)
611        filtered = QgsAuthCertUtils.casRemoveSelfSigned(extra)
612
613        self.assertEqual(len(filtered), 1)
614        self.assertEqual(len(extra), 2)
615
616        self.assertTrue(extra[1].isSelfSigned())
617
618        for c in filtered:
619            self.assertFalse(c.isSelfSigned())
620
621    def test_150_verify_keychain(self):
622        """Test the verify keychain function"""
623
624        def testChain(path):
625
626            # Test that a chain with an untrusted CA is not valid
627            self.assertTrue(len(QgsAuthCertUtils.validateCertChain(QgsAuthCertUtils.certsFromFile(path))) > 0)
628
629            # Test that a chain with an untrusted CA is valid when the addRootCa argument is true
630            self.assertTrue(len(QgsAuthCertUtils.validateCertChain(QgsAuthCertUtils.certsFromFile(path), None, True)) == 0)
631
632            # Test that a chain with an untrusted CA is not valid when the addRootCa argument is true
633            # and a wrong domainis true
634            self.assertTrue(len(QgsAuthCertUtils.validateCertChain(QgsAuthCertUtils.certsFromFile(path), 'my.wrong.domain', True)) > 0)
635
636        testChain(PKIDATA + '/chain_subissuer-issuer-root.pem')
637        testChain(PKIDATA + '/localhost_ssl_w-chain.pem')
638        testChain(PKIDATA + '/fra_w-chain.pem')
639
640        path = PKIDATA + '/localhost_ssl_w-chain.pem'
641
642        # Test that a chain with an untrusted CA is not valid when the addRootCa argument is true
643        # and a wrong domain is set
644        self.assertTrue(len(QgsAuthCertUtils.validateCertChain(QgsAuthCertUtils.certsFromFile(path), 'my.wrong.domain', True)) > 0)
645
646        # Test that a chain with an untrusted CA is valid when the addRootCa argument is true
647        # and a right domain is set
648        self.assertTrue(len(QgsAuthCertUtils.validateCertChain(QgsAuthCertUtils.certsFromFile(path), 'localhost', True)) == 0)
649
650        # Test that a chain with an untrusted CA is not valid when the addRootCa argument is false
651        # and a right domain is set
652        self.assertTrue(len(QgsAuthCertUtils.validateCertChain(QgsAuthCertUtils.certsFromFile(path), 'localhost', False)) > 0)
653
654    def test_validate_pki_bundle(self):
655        """Text the pki bundle validation"""
656
657        # Valid bundle:
658        bundle = self.mkPEMBundle('fra_cert.pem', 'fra_key.pem', 'password', 'chain_subissuer-issuer-root.pem')
659
660        # Test valid bundle with intermediates and without trusted root
661        self.assertEqual(QgsAuthCertUtils.validatePKIBundle(bundle), ['The root certificate of the certificate chain is self-signed, and untrusted'])
662        # Test valid without intermediates
663        self.assertEqual(QgsAuthCertUtils.validatePKIBundle(bundle, False), ['The issuer certificate of a locally looked up certificate could not be found', 'No certificates could be verified'])
664        # Test valid with intermediates and trusted root
665        self.assertEqual(QgsAuthCertUtils.validatePKIBundle(bundle, True, True), [])
666
667        # Wrong chain
668        bundle = self.mkPEMBundle('fra_cert.pem', 'fra_key.pem', 'password', 'chain_issuer2-root2.pem')
669        # Test invalid bundle with intermediates and without trusted root
670        self.assertEqual(QgsAuthCertUtils.validatePKIBundle(bundle), ['The issuer certificate of a locally looked up certificate could not be found', 'No certificates could be verified'])
671        # Test valid without intermediates
672        self.assertEqual(QgsAuthCertUtils.validatePKIBundle(bundle, False), ['The issuer certificate of a locally looked up certificate could not be found', 'No certificates could be verified'])
673        # Test valid with intermediates and trusted root
674        self.assertEqual(QgsAuthCertUtils.validatePKIBundle(bundle, True, True), ['The issuer certificate of a locally looked up certificate could not be found', 'No certificates could be verified'])
675
676        # Wrong key
677        bundle = self.mkPEMBundle('fra_cert.pem', 'ptolemy_key.pem', 'password', 'chain_subissuer-issuer-root.pem')
678        # Test invalid bundle with intermediates and without trusted root
679        self.assertEqual(QgsAuthCertUtils.validatePKIBundle(bundle), ['The root certificate of the certificate chain is self-signed, and untrusted', 'Private key does not match client certificate public key.'])
680        # Test invalid without intermediates
681        self.assertEqual(QgsAuthCertUtils.validatePKIBundle(bundle, False), ['The issuer certificate of a locally looked up certificate could not be found', 'No certificates could be verified', 'Private key does not match client certificate public key.'])
682        # Test invalid with intermediates and trusted root
683        self.assertEqual(QgsAuthCertUtils.validatePKIBundle(bundle, True, True), ['Private key does not match client certificate public key.'])
684
685        # Expired root CA
686        bundle = self.mkPEMBundle('piri_cert.pem', 'piri_key.pem', 'password', 'chain_issuer3-root3-EXPIRED.pem')
687        self.assertEqual(QgsAuthCertUtils.validatePKIBundle(bundle), ['The root certificate of the certificate chain is self-signed, and untrusted', 'The certificate has expired'])
688        self.assertEqual(QgsAuthCertUtils.validatePKIBundle(bundle, False), ['The issuer certificate of a locally looked up certificate could not be found', 'No certificates could be verified'])
689        self.assertEqual(QgsAuthCertUtils.validatePKIBundle(bundle, True, True), ['The root certificate of the certificate chain is self-signed, and untrusted', 'The certificate has expired'])
690
691        # Expired intermediate CA
692        bundle = self.mkPEMBundle('marinus_cert-EXPIRED.pem', 'marinus_key_w-pass.pem', 'password', 'chain_issuer2-root2.pem')
693        self.assertEqual(QgsAuthCertUtils.validatePKIBundle(bundle), ['The root certificate of the certificate chain is self-signed, and untrusted', 'The certificate has expired'])
694        self.assertEqual(QgsAuthCertUtils.validatePKIBundle(bundle, False), ['The issuer certificate of a locally looked up certificate could not be found', 'No certificates could be verified'])
695        self.assertEqual(QgsAuthCertUtils.validatePKIBundle(bundle, True, True), ['The certificate has expired'])
696
697        # Expired client cert
698        bundle = self.mkPEMBundle('henricus_cert.pem', 'henricus_key_w-pass.pem', 'password', 'chain_issuer4-EXPIRED-root2.pem')
699        self.assertEqual(QgsAuthCertUtils.validatePKIBundle(bundle), ['The root certificate of the certificate chain is self-signed, and untrusted', 'The certificate has expired'])
700        self.assertEqual(QgsAuthCertUtils.validatePKIBundle(bundle, False), ['The issuer certificate of a locally looked up certificate could not be found', 'No certificates could be verified'])
701        self.assertEqual(QgsAuthCertUtils.validatePKIBundle(bundle, True, True), ['The certificate has expired'])
702
703        # Untrusted root, positive test before untrust is applied
704        bundle = self.mkPEMBundle('nicholas_cert.pem', 'nicholas_key.pem', 'password', 'chain_issuer2-root2.pem')
705        # Test valid with intermediates and trusted root
706        self.assertEqual(QgsAuthCertUtils.validatePKIBundle(bundle, True, True), [])
707        # Untrust this root
708        root2 = QgsAuthCertUtils.certFromFile(PKIDATA + '/' + 'root2_ca_cert.pem')
709        QgsApplication.authManager().storeCertAuthority(root2)
710        self.assertTrue(QgsApplication.authManager().storeCertTrustPolicy(root2, QgsAuthCertUtils.Untrusted))
711        QgsApplication.authManager().rebuildCaCertsCache()
712        # Test valid with intermediates and untrusted root
713        self.assertEqual(QgsAuthCertUtils.validatePKIBundle(bundle, True, True), ['The issuer certificate of a locally looked up certificate could not be found'])
714
715    def test_160_cert_viable(self):
716        """Text the viability of a given certificate"""
717
718        # null cert
719        cert = QSslCertificate()
720        self.assertFalse(QgsAuthCertUtils.certIsCurrent(cert))
721        res = QgsAuthCertUtils.certViabilityErrors(cert)
722        self.assertTrue(len(res) == 0)
723        self.assertFalse(QgsAuthCertUtils.certIsViable(cert))
724
725        cert.clear()
726        res.clear()
727        # valid cert
728        cert = QgsAuthCertUtils.certFromFile(PKIDATA + '/gerardus_cert.pem')
729        self.assertTrue(QgsAuthCertUtils.certIsCurrent(cert))
730        res = QgsAuthCertUtils.certViabilityErrors(cert)
731        self.assertTrue(len(res) == 0)
732        self.assertTrue(QgsAuthCertUtils.certIsViable(cert))
733
734        cert.clear()
735        res.clear()
736        # expired cert
737        cert = QgsAuthCertUtils.certFromFile(PKIDATA + '/marinus_cert-EXPIRED.pem')
738        self.assertFalse(QgsAuthCertUtils.certIsCurrent(cert))
739        res = QgsAuthCertUtils.certViabilityErrors(cert)
740        self.assertTrue(len(res) > 0)
741        self.assertTrue(QSslError(QSslError.CertificateExpired, cert) in res)
742        self.assertFalse(QgsAuthCertUtils.certIsViable(cert))
743
744    def test_170_pki_key_encoding(self):
745        """Test that a DER/PEM RSA/DSA/EC keys can be opened whatever the extension is"""
746
747        self.assertFalse(QgsAuthCertUtils.keyFromFile(PKIDATA + '/' + 'ptolemy_key.pem').isNull())
748        self.assertFalse(QgsAuthCertUtils.keyFromFile(PKIDATA + '/' + 'ptolemy_key.der').isNull())
749        self.assertFalse(QgsAuthCertUtils.keyFromFile(PKIDATA + '/' + 'ptolemy_key_pem.key').isNull())
750        self.assertFalse(QgsAuthCertUtils.keyFromFile(PKIDATA + '/' + 'ptolemy_key_der.key').isNull())
751        self.assertFalse(QgsAuthCertUtils.keyFromFile(PKIDATA + '/' + 'donald_key_EC.pem').isNull())
752        self.assertFalse(QgsAuthCertUtils.keyFromFile(PKIDATA + '/' + 'donald_key_EC.der').isNull())
753        self.assertFalse(QgsAuthCertUtils.keyFromFile(PKIDATA + '/' + 'donald_key_DSA.pem').isNull())
754        self.assertFalse(QgsAuthCertUtils.keyFromFile(PKIDATA + '/' + 'donald_key_DSA.der').isNull())
755        self.assertFalse(QgsAuthCertUtils.keyFromFile(PKIDATA + '/' + 'donald_key_DSA_crlf.pem').isNull())
756        self.assertFalse(QgsAuthCertUtils.keyFromFile(PKIDATA + '/' + 'donald_key_DSA_nonl.pem').isNull())
757        donald_dsa = QgsAuthCertUtils.keyFromFile(PKIDATA + '/' + 'donald_key_DSA.pem').toPem()
758        self.assertEqual(donald_dsa, QgsAuthCertUtils.keyFromFile(PKIDATA + '/' + 'donald_key_DSA.der').toPem())
759        self.assertEqual(donald_dsa, QgsAuthCertUtils.keyFromFile(PKIDATA + '/' + 'donald_key_DSA_crlf.pem').toPem())
760        self.assertEqual(donald_dsa, QgsAuthCertUtils.keyFromFile(PKIDATA + '/' + 'donald_key_DSA_nonl.pem').toPem())
761
762        self.assertEqual(QgsAuthCertUtils.validatePKIBundle(self.mkPEMBundle('ptolemy_cert.pem', 'ptolemy_key.pem', 'password', 'chain_subissuer-issuer-root.pem'), True, True), [])
763        self.assertEqual(QgsAuthCertUtils.validatePKIBundle(self.mkPEMBundle('ptolemy_cert.pem', 'ptolemy_key.der', 'password', 'chain_subissuer-issuer-root.pem'), True, True), [])
764        self.assertEqual(QgsAuthCertUtils.validatePKIBundle(self.mkPEMBundle('ptolemy_cert.pem', 'ptolemy_key_pem.key', 'password', 'chain_subissuer-issuer-root.pem'), True, True), [])
765        self.assertEqual(QgsAuthCertUtils.validatePKIBundle(self.mkPEMBundle('ptolemy_cert.pem', 'ptolemy_key_der.key', 'password', 'chain_subissuer-issuer-root.pem'), True, True), [])
766
767
768if __name__ == '__main__':
769    unittest.main()
770