Skip to content

Commit

Permalink
[auth] Add utility method to validate a PKI bundle
Browse files Browse the repository at this point in the history
  • Loading branch information
elpaso committed Oct 26, 2017
1 parent f1eba3a commit 89166a0
Show file tree
Hide file tree
Showing 6 changed files with 101 additions and 29 deletions.
15 changes: 12 additions & 3 deletions python/core/auth/qgsauthcertutils.sip
Original file line number Diff line number Diff line change
Expand Up @@ -275,18 +275,27 @@ Get short strings describing an SSL error
%End


static QList<QSslError> validateCertChain( const QList<QSslCertificate> &certificateChain, const QString &hostName = QString(), bool addRootCa = false ) ;
static QList<QSslError> validateCertChain( const QList<QSslCertificate> &certificateChain,
const QString &hostName = QString(),
bool trustRootCa = false ) ;
%Docstring
validateCertChain validates the given ``certificateChain``
\param certificateChain list of certificates to be checked, with leaf first and with optional root CA last
\param hostName (optional) name of the host to be verified
\param addRootCa if true the CA will be added to the trusted CAs for this validation check
\param trustRootCa if true the CA will be added to the trusted CAs for this validation check
:return: list of QSslError, if the list is empty then the cert chain is valid
:rtype: list of QSslError
%End

static QStringList validatePKIBundle( QgsPkiBundle &bundle, bool useIntermediates = true, bool addRootCa = false );
static QStringList validatePKIBundle( QgsPkiBundle &bundle, bool useIntermediates = true, bool trustRootCa = false );
%Docstring
validatePKIBundle validate the PKI bundle by checking the certificate chain, the
expiration and effective dates, optionally trusts the root CA
\param bundle
\param useIntermediates if true the intermediate certs are also checked
\param trustRootCa if true the CA will be added to the trusted CAs for this validation check (if useIntermediates is false)
this option is ignored and set to false
:return: a list of error strings, if the list is empty then the PKI bundle is valid
:rtype: list of str
%End

Expand Down
3 changes: 0 additions & 3 deletions python/core/auth/qgsauthconfig.sip
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,6 @@ class QgsPkiBundle
const QString &keyPass = QString(),
const QList<QSslCertificate> &caChain = QList<QSslCertificate>() );
%Docstring
TORM
Construct a bundle of PKI components from PEM-formatted file paths
\param certPath Certificate file path
\param keyPath Private key path
Expand All @@ -233,7 +232,6 @@ class QgsPkiBundle
static const QgsPkiBundle fromPkcs12Paths( const QString &bundlepath,
const QString &bundlepass = QString() );
%Docstring
TORM
Construct a bundle of PKI components from a PKCS#12 file path
\param bundlepath Bundle file path
\param bundlepass Optional bundle passphrase
Expand Down Expand Up @@ -295,7 +293,6 @@ class QgsPkiConfigBundle
{
%Docstring
Storage set for constructed SSL certificate, key, associated with an authentication config
TODO: inherit from PKIBundle
%End

%TypeHeaderCode
Expand Down
32 changes: 27 additions & 5 deletions src/core/auth/qgsauthcertutils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1019,7 +1019,9 @@ QList<QPair<QSslError::SslError, QString> > QgsAuthCertUtils::sslErrorEnumString
return errenums;
}

QList<QSslError> QgsAuthCertUtils::validateCertChain( const QList<QSslCertificate> &certificateChain, const QString &hostName, bool addRootCa )
QList<QSslError> QgsAuthCertUtils::validateCertChain( const QList<QSslCertificate> &certificateChain,
const QString &hostName,
bool trustRootCa )
{
QList<QSslError> sslErrors;
QList<QSslCertificate> trustedChain;
Expand All @@ -1041,8 +1043,28 @@ QList<QSslError> QgsAuthCertUtils::validateCertChain( const QList<QSslCertificat
}
}

// Check that no certs in the chain are expired or not yet valid or blacklisted
const QList<QSslCertificate> constTrustedChain( trustedChain );
for ( const auto &cert : constTrustedChain )
{
// TODO: move all the checks to QgsAuthCertUtils::certIsViable( )
const QDateTime currentTime = QDateTime::currentDateTime();
if ( cert.expiryDate() <= currentTime )
{
sslErrors << QSslError( QSslError::SslError::CertificateExpired, cert );
}
if ( cert.effectiveDate() >= QDateTime::currentDateTime() )
{
sslErrors << QSslError( QSslError::SslError::CertificateNotYetValid, cert );
}
if ( cert.isBlacklisted() )
{
sslErrors << QSslError( QSslError::SslError::CertificateBlacklisted, cert );
}
}

// Merge in the root CA if present and asked for
if ( addRootCa && trustedChain.count() > 1 && trustedChain.last().isSelfSigned() )
if ( trustRootCa && trustedChain.count() > 1 && trustedChain.last().isSelfSigned() )
{
static QMutex sMutex;
QMutexLocker lock( &sMutex );
Expand All @@ -1060,15 +1082,15 @@ QList<QSslError> QgsAuthCertUtils::validateCertChain( const QList<QSslCertificat
return sslErrors;
}

QStringList QgsAuthCertUtils::validatePKIBundle( QgsPkiBundle &bundle, bool useIntermediates, bool addRootCa )
QStringList QgsAuthCertUtils::validatePKIBundle( QgsPkiBundle &bundle, bool useIntermediates, bool trustRootCa )
{
QStringList errors;
QList<QSslError> sslErrors;
if ( useIntermediates )
{
QList<QSslCertificate> certsList( bundle.caChain() );
certsList.insert( 0, bundle.clientCert( ) );
sslErrors = QgsAuthCertUtils::validateCertChain( certsList, QString(), addRootCa );
sslErrors = QgsAuthCertUtils::validateCertChain( certsList, QString(), trustRootCa );
}
else
{
Expand Down Expand Up @@ -1101,7 +1123,7 @@ QStringList QgsAuthCertUtils::validatePKIBundle( QgsPkiBundle &bundle, bool useI
}
else
{
// Log? Error? Note that elliptical curve is not supported by QCA
QgsDebugMsg( "Key is not DSA, RSA or DH: validation is not supported by Qt" );
}
if ( ! keyValid )
{
Expand Down
17 changes: 14 additions & 3 deletions src/core/auth/qgsauthcertutils.h
Original file line number Diff line number Diff line change
Expand Up @@ -301,12 +301,23 @@ class CORE_EXPORT QgsAuthCertUtils
* \brief validateCertChain validates the given \a certificateChain
* \param certificateChain list of certificates to be checked, with leaf first and with optional root CA last
* \param hostName (optional) name of the host to be verified
* \param addRootCa if true the CA will be added to the trusted CAs for this validation check
* \param trustRootCa if true the CA will be added to the trusted CAs for this validation check
* \return list of QSslError, if the list is empty then the cert chain is valid
*/
static QList<QSslError> validateCertChain( const QList<QSslCertificate> &certificateChain, const QString &hostName = QString(), bool addRootCa = false ) ;
static QList<QSslError> validateCertChain( const QList<QSslCertificate> &certificateChain,
const QString &hostName = QString(),
bool trustRootCa = false ) ;

static QStringList validatePKIBundle( QgsPkiBundle &bundle, bool useIntermediates = true, bool addRootCa = false );
/**
* \brief validatePKIBundle validate the PKI bundle by checking the certificate chain, the
* expiration and effective dates, optionally trusts the root CA
* \param bundle
* \param useIntermediates if true the intermediate certs are also checked
* \param trustRootCa if true the CA will be added to the trusted CAs for this validation check (if useIntermediates is false)
* this option is ignored and set to false
* \return a list of error strings, if the list is empty then the PKI bundle is valid
*/
static QStringList validatePKIBundle( QgsPkiBundle &bundle, bool useIntermediates = true, bool trustRootCa = false );

private:
static void appendDirSegment_( QStringList &dirname, const QString &segment, QString value );
Expand Down
3 changes: 0 additions & 3 deletions src/core/auth/qgsauthconfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,6 @@ class CORE_EXPORT QgsPkiBundle
const QList<QSslCertificate> &caChain = QList<QSslCertificate>() );

/**
* TORM
* Construct a bundle of PKI components from PEM-formatted file paths
* \param certPath Certificate file path
* \param keyPath Private key path
Expand All @@ -214,7 +213,6 @@ class CORE_EXPORT QgsPkiBundle
const QList<QSslCertificate> &caChain = QList<QSslCertificate>() );

/**
* TORM
* Construct a bundle of PKI components from a PKCS#12 file path
* \param bundlepath Bundle file path
* \param bundlepass Optional bundle passphrase
Expand Down Expand Up @@ -256,7 +254,6 @@ class CORE_EXPORT QgsPkiBundle
/**
* \ingroup core
* \brief Storage set for constructed SSL certificate, key, associated with an authentication config
* TODO: inherit from PKIBundle
*/
class CORE_EXPORT QgsPkiConfigBundle
{
Expand Down
60 changes: 48 additions & 12 deletions tests/src/python/test_qgsauthsystem.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
"""QGIS Unit tests for bindings to core authentication system classes
From build dir: ctest -R PyQgsAuthenticationSystem -V
From build dir: LC_ALL=en_US.UTF-8 ctest -R PyQgsAuthenticationSystem -V
.. note:: This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
Expand All @@ -17,19 +17,13 @@
import os
import tempfile

from qgis.core import QgsAuthManager, QgsAuthCertUtils, QgsPkiBundle, QgsAuthMethodConfig, QgsAuthMethod, QgsAuthConfigSslServer, QgsApplication
from qgis.core import QgsAuthCertUtils, QgsPkiBundle, QgsAuthMethodConfig, QgsAuthMethod, QgsAuthConfigSslServer, QgsApplication
from qgis.gui import QgsAuthEditorWidgets


from qgis.PyQt.QtCore import QFileInfo, qDebug
from qgis.PyQt.QtWidgets import QDialog, QVBoxLayout, QDialogButtonBox
from qgis.PyQt.QtTest import QTest
from qgis.PyQt.QtNetwork import QSsl, QSslError, QSslSocket

from qgis.testing import (
start_app,
unittest,
)
from qgis.PyQt.QtTest import QTest
from qgis.PyQt.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout
from qgis.testing import start_app, unittest

from utilities import unitTestDataPath

Expand Down Expand Up @@ -635,6 +629,7 @@ def testChain(path):

testChain(PKIDATA + '/chain_subissuer-issuer-root.pem')
testChain(PKIDATA + '/localhost_ssl_w-chain.pem')
testChain(PKIDATA + '/fra_w-chain.pem')

path = PKIDATA + '/localhost_ssl_w-chain.pem'

Expand All @@ -650,7 +645,48 @@ def testChain(path):
# and a right domain is set
self.assertTrue(len(QgsAuthCertUtils.validateCertChain(QgsAuthCertUtils.certsFromFile(path), 'localhost', False)) > 0)

testChain(PKIDATA + '/fra_w-chain.pem')
def test_validate_pki_bundle(self):
"""Text the pki bundle validation"""

def mkPEMBundle(client_cert, client_key, password, chain):
return QgsPkiBundle.fromPemPaths(PKIDATA + '/' + client_cert,
PKIDATA + '/' + client_key,
password,
QgsAuthCertUtils.certsFromFile(
PKIDATA + '/' + chain
))

# Valid bundle:
bundle = mkPEMBundle('fra_cert.pem', 'fra_key.pem', 'password', 'chain_subissuer-issuer-root.pem')

# Test valid bundle with intermediates and without trusted root
self.assertEqual(QgsAuthCertUtils.validatePKIBundle(bundle), ['The root certificate of the certificate chain is self-signed, and untrusted'])
# Test valid without intermediates
self.assertEqual(QgsAuthCertUtils.validatePKIBundle(bundle, False), ['The issuer certificate of a locally looked up certificate could not be found', 'No certificates could be verified'])
# Test valid with intermediates and trusted root
self.assertEqual(QgsAuthCertUtils.validatePKIBundle(bundle, True, True), [])

# Wrong chain
bundle = mkPEMBundle('fra_cert.pem', 'fra_key.pem', 'password', 'chain_issuer2-root2.pem')
# Test invalid bundle with intermediates and without trusted root
self.assertEqual(QgsAuthCertUtils.validatePKIBundle(bundle), ['The issuer certificate of a locally looked up certificate could not be found', 'No certificates could be verified'])
# Test valid without intermediates
self.assertEqual(QgsAuthCertUtils.validatePKIBundle(bundle, False), ['The issuer certificate of a locally looked up certificate could not be found', 'No certificates could be verified'])
# Test valid with intermediates and trusted root
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'])

# Wrong key
bundle = mkPEMBundle('fra_cert.pem', 'ptolemy_key.pem', 'password', 'chain_subissuer-issuer-root.pem')
# Test invalid bundle with intermediates and without trusted root
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.'])
# Test invalid without intermediates
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.'])
# Test invalid with intermediates and trusted root
self.assertEqual(QgsAuthCertUtils.validatePKIBundle(bundle, True, True), ['Private key does not match client certificate public key.'])

# TODO: Wrong root CA
# TODO: expired/not-yet-valid cert
# TODO: expired/not-yet-valid intermediate (is it possible to build a cert from one of those?)


if __name__ == '__main__':
Expand Down

0 comments on commit 89166a0

Please sign in to comment.