diff --git a/CMakeLists.txt b/CMakeLists.txt index d7d812933d9c..9e161950379f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -320,6 +320,11 @@ IF(WITH_CORE) FIND_QCAOSSL_PLUGIN_CPP(ENABLE_TESTS) ENDIF(NOT MSVC) + IF (APPLE) + # Libtasn1 is for DER-encoded PKI ASN.1 parsing/extracting workarounds + FIND_PACKAGE(Libtasn1 REQUIRED) + ENDIF (APPLE) + IF (SUPPRESS_QT_WARNINGS) # Newer versions of UseQt4.cmake include Qt with -isystem automatically # This can be used to force this behavior on older systems diff --git a/cmake/FindLibtasn1.cmake b/cmake/FindLibtasn1.cmake new file mode 100644 index 000000000000..c416e8747603 --- /dev/null +++ b/cmake/FindLibtasn1.cmake @@ -0,0 +1,45 @@ +# Find Libtasn1 +# ~~~~~~~~~~~~~~~ +# CMake module to search for Libtasn1 ASN.1 library and header(s) from: +# https://www.gnu.org/software/libtasn1/ +# +# If it's found it sets LIBTASN1_FOUND to TRUE +# and following variables are set: +# LIBTASN1_INCLUDE_DIR +# LIBTASN1_LIBRARY +# +# Copyright (c) 2017, Boundless Spatial +# Author: Larry Shaffer +# +# Redistribution and use is allowed according to the terms of the BSD license. +# For details see the accompanying COPYING-CMAKE-SCRIPTS file. + + +find_path(LIBTASN1_INCLUDE_DIR + NAMES libtasn1.h + PATHS + ${LIB_DIR}/include + "$ENV{LIB_DIR}/include" + $ENV{INCLUDE} + /usr/local/include + /usr/include +) + +find_library(LIBTASN1_LIBRARY + NAMES tasn1 + PATHS + ${LIB_DIR} + "$ENV{LIB_DIR}" + $ENV{LIB} + /usr/local/lib + /usr/lib +) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args( + Libtasn1 + REQUIRED_VARS LIBTASN1_INCLUDE_DIR LIBTASN1_LIBRARY + FOUND_VAR LIBTASN1_FOUND +) + +mark_as_advanced(LIBTASN1_INCLUDE_DIR LIBTASN1_LIBRARY) diff --git a/python/core/auth/qgsauthcertutils.sip b/python/core/auth/qgsauthcertutils.sip index 1d2520068d3a..560ce193b007 100644 --- a/python/core/auth/qgsauthcertutils.sip +++ b/python/core/auth/qgsauthcertutils.sip @@ -81,6 +81,15 @@ Map certificate sha1 to certificate as simple cache %End + static QByteArray fileData( const QString &path, bool astext = false ); +%Docstring + Return data from a local file via a read-only operation + \param path Path to file to read + \param astext Whether to open the file as text, otherwise as binary + :return: All data contained in file or empty contents if file does not exist + :rtype: QByteArray +%End + static QList certsFromFile( const QString &certspath ); %Docstring Return list of concatenated certs from a PEM or DER formatted file @@ -150,6 +159,16 @@ Return list of concatenated certs from a PEM Base64 text block :rtype: list of str %End + static bool pemIsPkcs8( const QString &keyPemTxt ); +%Docstring + Determine if the PEM-encoded text of a key is PKCS#8 format + \param keyPemTxt PEM-encoded text + :return: True if PKCS#8, otherwise false + :rtype: bool +%End + + + static QStringList pkcs12BundleToPem( const QString &bundlepath, const QString &bundlepass = QString(), bool reencrypt = true ); diff --git a/resources/CMakeLists.txt b/resources/CMakeLists.txt index b2ed42bb8239..fe9bbc4d1f91 100644 --- a/resources/CMakeLists.txt +++ b/resources/CMakeLists.txt @@ -9,3 +9,9 @@ INSTALL(DIRECTORY data DESTINATION ${QGIS_DATA_DIR}/resources) IF (WITH_SERVER) INSTALL(DIRECTORY server DESTINATION ${QGIS_DATA_DIR}/resources) ENDIF (WITH_SERVER) + +IF (APPLE) + # ASN.1 definition files of PKIX elements + INSTALL(FILES pkcs8.asn + DESTINATION ${QGIS_DATA_DIR}/resources) +ENDIF (APPLE) diff --git a/resources/pkcs8.asn b/resources/pkcs8.asn new file mode 100644 index 000000000000..2aee10089c80 --- /dev/null +++ b/resources/pkcs8.asn @@ -0,0 +1,63 @@ +PKCS-8 {iso(1) member-body(2) us(840) rsadsi(113549) pkcs(1) pkcs-8(8) + modules(1) pkcs-8(1)} + +-- $Revision: 1.5 $ + +-- This module has been checked for conformance with the ASN.1 +-- standard by the OSS ASN.1 Tools + +DEFINITIONS EXPLICIT TAGS ::= + +BEGIN + +-- EXPORTS All -- +-- All types and values defined in this module is exported for use in +-- other ASN.1 modules. + +-- attribute data types -- + +Attribute ::= SEQUENCE { + type AttributeType, + values SET OF AttributeValue + -- at least one value is required -- +} + +AttributeType ::= OBJECT IDENTIFIER + +AttributeValue ::= ANY DEFINED BY type + +AttributeTypeAndValue ::= SEQUENCE { + type AttributeType, + value AttributeValue } + +AlgorithmIdentifier ::= SEQUENCE { + algorithm OBJECT IDENTIFIER, + parameters ANY DEFINED BY algorithm OPTIONAL } + -- contains a value of the type + -- registered for use with the + -- algorithm object identifier value + +-- Private-key information syntax + +PrivateKeyInfo ::= SEQUENCE { + version Version, + privateKeyAlgorithm AlgorithmIdentifier, + privateKey PrivateKey, + attributes [0] Attributes OPTIONAL } + +Version ::= INTEGER {v1(0)} + +PrivateKey ::= OCTET STRING + +Attributes ::= SET OF Attribute + +-- Encrypted private-key information syntax + +EncryptedPrivateKeyInfo ::= SEQUENCE { + encryptionAlgorithm AlgorithmIdentifier, + encryptedData EncryptedData +} + +EncryptedData ::= OCTET STRING + +END diff --git a/src/auth/pkipkcs12/qgsauthpkcs12method.cpp b/src/auth/pkipkcs12/qgsauthpkcs12method.cpp index 53bd2956de03..57b5411f7543 100644 --- a/src/auth/pkipkcs12/qgsauthpkcs12method.cpp +++ b/src/auth/pkipkcs12/qgsauthpkcs12method.cpp @@ -282,6 +282,12 @@ QgsPkiConfigBundle *QgsAuthPkcs12Method::getPkiConfigBundle( const QString &auth QStringList bundlelist = QgsAuthCertUtils::pkcs12BundleToPem( mconfig.config( QStringLiteral( "bundlepath" ) ), mconfig.config( QStringLiteral( "bundlepass" ) ), false ); + if ( bundlelist.isEmpty() || bundlelist.size() < 2 ) + { + QgsDebugMsg( QString( "PKI bundle for authcfg %1: insert FAILED, PKCS#12 bundle parsing failed" ).arg( authcfg ) ); + return bundle; + } + // init client cert // Note: if this is not valid, no sense continuing QSslCertificate clientcert( bundlelist.at( 0 ).toLatin1() ); @@ -291,6 +297,11 @@ QgsPkiConfigBundle *QgsAuthPkcs12Method::getPkiConfigBundle( const QString &auth return bundle; } + // !!! DON'T LEAVE THESE UNCOMMENTED !!! + // QgsDebugMsg( QString( "PKI bundle key for authcfg: \n%1" ).arg( bundlelist.at( 1 ) ) ); + // QgsDebugMsg( QString( "PKI bundle key pass for authcfg: \n%1" ) + // .arg( !mconfig.config( QStringLiteral( "bundlepass" ) ).isNull() ? mconfig.config( QStringLiteral( "bundlepass" ) ) : QStringLiteral() ) ); + // init key QSslKey clientkey( bundlelist.at( 1 ).toLatin1(), QSsl::Rsa, diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 6dcce0d9ea60..d898a12a6e7c 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -1179,6 +1179,13 @@ INCLUDE_DIRECTORIES(SYSTEM ${QTKEYCHAIN_INCLUDE_DIR} ) +IF (APPLE) + # Libtasn1 is for DER-encoded PKI ASN.1 parsing/extracting workarounds + INCLUDE_DIRECTORIES(SYSTEM + ${LIBTASN1_INCLUDE_DIR} + ) +ENDIF (APPLE) + #for PAL classes IF (WIN32) @@ -1249,7 +1256,7 @@ IF (WIN32) ENDIF (WIN32) IF (APPLE) - TARGET_LINK_LIBRARIES(qgis_core qgis_native) + TARGET_LINK_LIBRARIES(qgis_core qgis_native ${LIBTASN1_LIBRARY}) ENDIF (APPLE) IF (NOT WITH_INTERNAL_QEXTSERIALPORT) diff --git a/src/core/auth/qgsauthcertutils.cpp b/src/core/auth/qgsauthcertutils.cpp index fc9273a96d52..0e378bb197f6 100644 --- a/src/core/auth/qgsauthcertutils.cpp +++ b/src/core/auth/qgsauthcertutils.cpp @@ -23,9 +23,16 @@ #include #include +#include "qgsapplication.h" #include "qgsauthmanager.h" #include "qgslogger.h" +#ifdef Q_OS_MAC +#include +#include "libtasn1.h" +#endif + + QString QgsAuthCertUtils::getSslProtocolName( QSsl::SslProtocol protocol ) { switch ( protocol ) @@ -94,22 +101,26 @@ QMap > QgsAuthCertUtils::sslConfigsGroupe return orgconfigs; } -static QByteArray fileData_( const QString &path, bool astext = false ) +QByteArray QgsAuthCertUtils::fileData( const QString &path, bool astext ) { QByteArray data; QFile file( path ); - if ( file.exists() ) + if ( !file.exists() ) { - QFile::OpenMode openflags( QIODevice::ReadOnly ); - if ( astext ) - openflags |= QIODevice::Text; - bool ret = file.open( openflags ); - if ( ret ) - { - data = file.readAll(); - } - file.close(); + QgsDebugMsg( QStringLiteral( "Read file error, file not found: %1" ).arg( path ) ); + return data; + } + // TODO: add checks for locked file, etc., to ensure it can be read + QFile::OpenMode openflags( QIODevice::ReadOnly ); + if ( astext ) + openflags |= QIODevice::Text; + bool ret = file.open( openflags ); + if ( ret ) + { + data = file.readAll(); } + file.close(); + return data; } @@ -117,7 +128,7 @@ QList QgsAuthCertUtils::certsFromFile( const QString &certspath { QList certs; bool pem = certspath.endsWith( QLatin1String( ".pem" ), Qt::CaseInsensitive ); - certs = QSslCertificate::fromData( fileData_( certspath, pem ), pem ? QSsl::Pem : QSsl::Der ); + certs = QSslCertificate::fromData( QgsAuthCertUtils::fileData( certspath, pem ), pem ? QSsl::Pem : QSsl::Der ); if ( certs.isEmpty() ) { QgsDebugMsg( QString( "Parsed cert(s) EMPTY for path: %1" ).arg( certspath ) ); @@ -181,7 +192,7 @@ QSslKey QgsAuthCertUtils::keyFromFile( const QString &keypath, QString *algtype ) { bool pem = keypath.endsWith( QLatin1String( ".pem" ), Qt::CaseInsensitive ); - QByteArray keydata( fileData_( keypath, pem ) ); + QByteArray keydata( QgsAuthCertUtils::fileData( keypath, pem ) ); QSslKey clientkey; clientkey = QSslKey( keydata, @@ -262,37 +273,249 @@ QStringList QgsAuthCertUtils::certKeyBundleToPem( const QString &certpath, return QStringList() << certpem << keypem << algtype; } +bool QgsAuthCertUtils::pemIsPkcs8( const QString &keyPemTxt ) +{ + QString pkcs8Header = QStringLiteral( "-----BEGIN PRIVATE KEY-----" ); + QString pkcs8Footer = QStringLiteral( "-----END PRIVATE KEY-----" ); + return keyPemTxt.contains( pkcs8Header ) && keyPemTxt.contains( pkcs8Footer ); +} + +#ifdef Q_OS_MAC +QByteArray QgsAuthCertUtils::pkcs8PrivateKey( QByteArray &pkcs8Der ) +{ + QByteArray pkcs1; + + if ( pkcs8Der.isEmpty() ) + { + QgsDebugMsg( QStringLiteral( "ERROR, passed DER is empty" ) ); + return pkcs1; + } + // Dump as unarmored PEM format, e.g. missing '-----BEGIN|END...' wrapper + //QgsDebugMsg ( QStringLiteral( "pkcs8Der: %1" ).arg( QString( pkcs8Der.toBase64() ) ) ); + + QFileInfo asnDefsRsrc( QgsApplication::pkgDataPath() + QStringLiteral( "/resources/pkcs8.asn" ) ); + if ( ! asnDefsRsrc.exists() ) + { + QgsDebugMsg( QStringLiteral( "ERROR, pkcs.asn resource file not found: %1" ).arg( asnDefsRsrc.filePath() ) ); + return pkcs1; + } + const char *asnDefsFile = asnDefsRsrc.absoluteFilePath().toLocal8Bit().constData(); + + int asn1_result = ASN1_SUCCESS, der_len = 0, oct_len = 0; + asn1_node definitions = NULL, structure = NULL; + char errorDescription[ASN1_MAX_ERROR_DESCRIPTION_SIZE], oct_data[1024]; + unsigned char *der = NULL; + unsigned int flags = 0; //TODO: see if any or all ASN1_DECODE_FLAG_* flags can be set + unsigned oct_etype; + + // Base PKCS#8 element to decode + QString typeName( QStringLiteral( "PKCS-8.PrivateKeyInfo" ) ); + + asn1_result = asn1_parser2tree( asnDefsFile, &definitions, errorDescription ); + + switch ( asn1_result ) + { + case ASN1_SUCCESS: + QgsDebugMsgLevel( QStringLiteral( "Parse: done.\n" ), 4 ); + break; + case ASN1_FILE_NOT_FOUND: + QgsDebugMsg( QStringLiteral( "ERROR, file not found: %1" ).arg( asnDefsFile ) ); + return pkcs1; + case ASN1_SYNTAX_ERROR: + case ASN1_IDENTIFIER_NOT_FOUND: + case ASN1_NAME_TOO_LONG: + QgsDebugMsg( QStringLiteral( "ERROR, asn1 parsing: %1" ).arg( errorDescription ) ); + return pkcs1; + default: + QgsDebugMsg( QStringLiteral( "ERROR, libtasn1: %1" ).arg( asn1_strerror( asn1_result ) ) ); + return pkcs1; + } + + // Generate the ASN.1 structure + asn1_result = asn1_create_element( definitions, typeName.toLatin1().constData(), &structure ); + + //asn1_print_structure( stdout, structure, "", ASN1_PRINT_ALL); + + if ( asn1_result != ASN1_SUCCESS ) + { + QgsDebugMsg( QStringLiteral( "ERROR, structure creation: %1" ).arg( asn1_strerror( asn1_result ) ) ); + goto PKCS1DONE; + } + + // Populate the ASN.1 structure with decoded DER data + der = reinterpret_cast( pkcs8Der.data() ); + der_len = pkcs8Der.size(); + + if ( flags != 0 ) + { + asn1_result = asn1_der_decoding2( &structure, der, &der_len, flags, errorDescription ); + } + else + { + asn1_result = asn1_der_decoding( &structure, der, der_len, errorDescription ); + } + + if ( asn1_result != ASN1_SUCCESS ) + { + QgsDebugMsg( QStringLiteral( "ERROR, decoding: %1" ).arg( errorDescription ) ); + goto PKCS1DONE; + } + else + { + QgsDebugMsgLevel( QStringLiteral( "Decoding: %1" ).arg( asn1_strerror( asn1_result ) ), 4 ); + } + + if ( QgsLogger::debugLevel() >= 4 ) + { + QgsDebugMsg( QStringLiteral( "DECODING RESULT:" ) ); + asn1_print_structure( stdout, structure, "", ASN1_PRINT_NAME_TYPE_VALUE ); + } + + // Validate and extract privateKey value + QgsDebugMsgLevel( QStringLiteral( "Validating privateKey type..." ), 4 ); + typeName.append( QStringLiteral( ".privateKey" ) ); + QgsDebugMsgLevel( QStringLiteral( "privateKey element name: %1" ).arg( typeName ), 4 ); + + asn1_result = asn1_read_value_type( structure, "privateKey", NULL, &oct_len, &oct_etype ); + + if ( asn1_result != ASN1_MEM_ERROR ) // not sure why ASN1_MEM_ERROR = success, but it does + { + QgsDebugMsg( QStringLiteral( "ERROR, asn1 read privateKey value type: %1" ).arg( asn1_strerror( asn1_result ) ) ); + goto PKCS1DONE; + } + + if ( oct_etype != ASN1_ETYPE_OCTET_STRING ) + { + QgsDebugMsg( QStringLiteral( "ERROR, asn1 privateKey value not octet string, but type: %1" ).arg( static_cast( oct_etype ) ) ); + goto PKCS1DONE; + } + + if ( oct_len == 0 ) + { + QgsDebugMsg( QStringLiteral( "ERROR, asn1 privateKey octet string empty" ) ); + goto PKCS1DONE; + } + + QgsDebugMsgLevel( QStringLiteral( "Reading privateKey value..." ), 4 ); + asn1_result = asn1_read_value( structure, "privateKey", oct_data, &oct_len ); + + if ( asn1_result != ASN1_SUCCESS ) + { + QgsDebugMsg( QStringLiteral( "ERROR, asn1 read privateKey value: %1" ).arg( asn1_strerror( asn1_result ) ) ); + goto PKCS1DONE; + } + + if ( oct_len == 0 ) + { + QgsDebugMsg( QStringLiteral( "ERROR, asn1 privateKey value octet string empty" ) ); + goto PKCS1DONE; + } + + pkcs1 = QByteArray( oct_data, oct_len ); + + // !!! SENSITIVE DATA - DO NOT LEAVE UNCOMMENTED !!! + //QgsDebugMsgLevel( QStringLiteral( "privateKey octet data as PEM: %1" ).arg( QString( pkcs1.toBase64() ) ), 4 ); + +PKCS1DONE: + + asn1_delete_structure( &structure ); + return pkcs1; +} +#endif + QStringList QgsAuthCertUtils::pkcs12BundleToPem( const QString &bundlepath, const QString &bundlepass, bool reencrypt ) { QStringList empty; if ( !QCA::isSupported( "pkcs12" ) ) + { + QgsDebugMsg( QString( "QCA does not support PKCS#12" ) ); return empty; + } QCA::KeyBundle bundle( QgsAuthCertUtils::qcaKeyBundle( bundlepath, bundlepass ) ); if ( bundle.isNull() ) + { + QgsDebugMsg( QString( "FAILED to convert PKCS#12 file to QCA key bundle: %1" ).arg( bundlepath ) ); return empty; + } QCA::SecureArray passarray; if ( reencrypt && !bundlepass.isEmpty() ) + { passarray = QCA::SecureArray( bundlepass.toUtf8() ); + } QString algtype; + QSsl::KeyAlgorithm keyalg = QSsl::Opaque; if ( bundle.privateKey().isRSA() ) { algtype = QStringLiteral( "rsa" ); + keyalg = QSsl::Rsa; } else if ( bundle.privateKey().isDSA() ) { algtype = QStringLiteral( "dsa" ); + keyalg = QSsl::Dsa; } else if ( bundle.privateKey().isDH() ) { algtype = QStringLiteral( "dh" ); } + // TODO: add support for EC keys, once QCA supports them + + // can currently only support RSA and DSA between QCA and Qt + if ( keyalg == QSsl::Opaque ) + { + QgsDebugMsg( QString( "FAILED to read PKCS#12 key (only RSA and DSA algorithms supported): %1" ).arg( bundlepath ) ); + return empty; + } + + QString keyPem; +#ifdef Q_OS_MAC + if ( keyalg == QSsl::Rsa && QgsAuthCertUtils::pemIsPkcs8( bundle.privateKey().toPEM() ) ) + { + QgsDebugMsgLevel( QString( "Private key is PKCS#8: attempting conversion to PKCS#1..." ), 4 ); + // if RSA, convert from PKCS#8 key to 'traditional' OpenSSL RSA format, which Qt prefers + // note: QCA uses OpenSSL, regardless of the Qt SSL backend, and 1.0.2+ OpenSSL versions return + // RSA private keys as PKCS#8, which choke Qt upon QSslKey creation + + QByteArray pkcs8Der = bundle.privateKey().toDER().toByteArray(); + if ( pkcs8Der.isEmpty() ) + { + QgsDebugMsg( QString( "FAILED to convert PKCS#12 key to DER-encoded format: %1" ).arg( bundlepath ) ); + return empty; + } + + QByteArray pkcs1Der = QgsAuthCertUtils::pkcs8PrivateKey( pkcs8Der ); + if ( pkcs1Der.isEmpty() ) + { + QgsDebugMsg( QString( "FAILED to convert PKCS#12 key from PKCS#8 to PKCS#1: %1" ).arg( bundlepath ) ); + return empty; + } + + QSslKey pkcs1Key( pkcs1Der, QSsl::Rsa, QSsl::Der, QSsl::PrivateKey ); + if ( pkcs1Key.isNull() ) + { + QgsDebugMsg( QString( "FAILED to convert PKCS#12 key from PKCS#8 to PKCS#1 QSslKey: %1" ).arg( bundlepath ) ); + return empty; + } + keyPem = QString( pkcs1Key.toPem( passarray.toByteArray() ) ); + } + else + { + keyPem = bundle.privateKey().toPEM( passarray ); + } +#else + keyPem = bundle.privateKey().toPEM( passarray ); +#endif + + QgsDebugMsgLevel( QString( "PKCS#12 cert as PEM:\n%1" ).arg( QString( bundle.certificateChain().primary().toPEM() ) ), 4 ); + // !!! SENSITIVE DATA - DO NOT LEAVE UNCOMMENTED !!! + //QgsDebugMsgLevel( QString( "PKCS#12 key as PEM:\n%1" ).arg( QString( keyPem ) ), 4 ); - return QStringList() << bundle.certificateChain().primary().toPEM() << bundle.privateKey().toPEM( passarray ) << algtype; + return QStringList() << bundle.certificateChain().primary().toPEM() << keyPem << algtype; } QList QgsAuthCertUtils::pkcs12BundleCas( const QString &bundlepath, const QString &bundlepass ) diff --git a/src/core/auth/qgsauthcertutils.h b/src/core/auth/qgsauthcertutils.h index 5b2932a21702..719d579c9941 100644 --- a/src/core/auth/qgsauthcertutils.h +++ b/src/core/auth/qgsauthcertutils.h @@ -104,6 +104,14 @@ class CORE_EXPORT QgsAuthCertUtils */ static QMap< QString, QList > sslConfigsGroupedByOrg( const QList &configs ) SIP_SKIP; + /** + * Return data from a local file via a read-only operation + * \param path Path to file to read + * \param astext Whether to open the file as text, otherwise as binary + * \returns All data contained in file or empty contents if file does not exist + */ + static QByteArray fileData( const QString &path, bool astext = false ); + //! Return list of concatenated certs from a PEM or DER formatted file static QList certsFromFile( const QString &certspath ); @@ -157,6 +165,31 @@ class CORE_EXPORT QgsAuthCertUtils const QString &keypass = QString(), bool reencrypt = true ); + /** + * Determine if the PEM-encoded text of a key is PKCS#8 format + * \param keyPemTxt PEM-encoded text + * \returns True if PKCS#8, otherwise false + */ + static bool pemIsPkcs8( const QString &keyPemTxt ); + +#ifdef Q_OS_MAC + + /** + * Extract the PrivateKey ASN.1 element of a DER-encoded PKCS#8 private key + * \param pkcs8Der PKCS#8 DER-encoded private key data + * \returns DER-encoded private key on success or an empty QByteArray upon failure + * \note On some platforms, e.g. macOS, where the default SSL backend is not OpenSSL, a QSslKey + * can not be created using PKCS#8-formatted data. However, PKCS#8 private key ASN.1 structures + * contain the key data inside a wrapper describing the algorithm used, e.g. RSA, DSA, ECC etc. + * Extracted PrivateKey ASN.1 data can be used to create a compatible QSslKey, + * e.g. 'traditional' SSLeay RSA-specific PKCS#1. + * By default OpenSSL 1.0.0+ returns private keys as PKCS#8, previously it was PKCS#1. + * \note This function requires 'libtasn1' development files and library, which is used + * to parse and extract the PrivateKey element from an ASN.1 PKCS#8 structure. + */ + static QByteArray pkcs8PrivateKey( QByteArray &pkcs8Der ) SIP_SKIP; +#endif + /** * Return list of certificate, private key and algorithm (as PEM text) for a PKCS#12 bundle * \param bundlepath File path to the PKCS bundle diff --git a/src/core/auth/qgsauthconfig.cpp b/src/core/auth/qgsauthconfig.cpp index 177422ad30fa..e1f8b4df2741 100644 --- a/src/core/auth/qgsauthconfig.cpp +++ b/src/core/auth/qgsauthconfig.cpp @@ -23,6 +23,8 @@ #include #include +#include "qgsauthcertutils.h" + ////////////////////////////////////////////// // QgsAuthMethodConfig @@ -172,25 +174,6 @@ QgsPkiBundle::QgsPkiBundle( const QSslCertificate &clientCert, setClientKey( clientKey ); } -static QByteArray fileData_( const QString &path, bool astext = false ) -{ - QByteArray data; - QFile file( path ); - if ( file.exists() ) - { - QFile::OpenMode openflags( QIODevice::ReadOnly ); - if ( astext ) - openflags |= QIODevice::Text; - bool ret = file.open( openflags ); - if ( ret ) - { - data = file.readAll(); - } - file.close(); - } - return data; -} - const QgsPkiBundle QgsPkiBundle::fromPemPaths( const QString &certPath, const QString &keyPath, const QString &keyPass, @@ -207,12 +190,12 @@ const QgsPkiBundle QgsPkiBundle::fromPemPaths( const QString &certPath, { // client cert bool pem = certPath.endsWith( QLatin1String( ".pem" ), Qt::CaseInsensitive ); - QSslCertificate clientcert( fileData_( certPath, pem ), pem ? QSsl::Pem : QSsl::Der ); + QSslCertificate clientcert( QgsAuthCertUtils::fileData( certPath, pem ), pem ? QSsl::Pem : QSsl::Der ); pkibundle.setClientCert( clientcert ); // client key bool pem_key = keyPath.endsWith( QLatin1String( ".pem" ), Qt::CaseInsensitive ); - QByteArray keydata( fileData_( keyPath, pem_key ) ); + QByteArray keydata( QgsAuthCertUtils::fileData( keyPath, pem_key ) ); QSslKey clientkey; clientkey = QSslKey( keydata, diff --git a/src/gui/auth/qgsauthimportidentitydialog.cpp b/src/gui/auth/qgsauthimportidentitydialog.cpp index c819ff9b0951..481cda0eaf20 100644 --- a/src/gui/auth/qgsauthimportidentitydialog.cpp +++ b/src/gui/auth/qgsauthimportidentitydialog.cpp @@ -29,26 +29,6 @@ #include "qgslogger.h" -static QByteArray fileData_( const QString &path, bool astext = false ) -{ - QByteArray data; - QFile file( path ); - if ( file.exists() ) - { - QFile::OpenMode openflags( QIODevice::ReadOnly ); - if ( astext ) - openflags |= QIODevice::Text; - bool ret = file.open( openflags ); - if ( ret ) - { - data = file.readAll(); - } - file.close(); - } - return data; -} - - QgsAuthImportIdentityDialog::QgsAuthImportIdentityDialog( QgsAuthImportIdentityDialog::IdentityType identitytype, QWidget *parent ) : QDialog( parent ) @@ -306,7 +286,7 @@ bool QgsAuthImportIdentityDialog::validatePkiPaths() // check for valid private key and that any supplied password works bool keypem = keypath.endsWith( QLatin1String( ".pem" ), Qt::CaseInsensitive ); - QByteArray keydata( fileData_( keypath, keypem ) ); + QByteArray keydata( QgsAuthCertUtils::fileData( keypath, keypem ) ); QSslKey clientkey; QString keypass = lePkiPathsKeyPass->text(); diff --git a/tests/src/core/CMakeLists.txt b/tests/src/core/CMakeLists.txt index 5c47b3882490..7a463a1561e5 100755 --- a/tests/src/core/CMakeLists.txt +++ b/tests/src/core/CMakeLists.txt @@ -74,6 +74,7 @@ SET(TESTS testqgsapplication.cpp testqgsatlascomposition.cpp testqgsauthcrypto.cpp + testqgsauthcertutils.cpp testqgsauthconfig.cpp testqgsauthmanager.cpp testqgsblendmodes.cpp diff --git a/tests/src/core/testqgsauthcertutils.cpp b/tests/src/core/testqgsauthcertutils.cpp new file mode 100644 index 000000000000..68ef765fb46a --- /dev/null +++ b/tests/src/core/testqgsauthcertutils.cpp @@ -0,0 +1,135 @@ +/*************************************************************************** + TestQgsAuthCertUtils.cpp + ---------------------- + Date : October 2017 + Copyright : (C) 2017 by Boundless Spatial, Inc. USA + Author : Larry Shaffer + Email : lshaffer at boundlessgeo dot com + *************************************************************************** + * * + * 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 * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgstest.h" +#include +#include +#include +#include + +#include "qgsapplication.h" +#include "qgsauthcrypto.h" +#include "qgsauthcertutils.h" +#include "qgslogger.h" + +/** + * \ingroup UnitTests + * Unit tests for QgsAuthCertUtils static functions + */ +class TestQgsAuthCertUtils: public QObject +{ + Q_OBJECT + + private slots: + void initTestCase(); + void cleanupTestCase(); + void init() {} + void cleanup() {} + + void testPkcsUtils(); + + private: + static QString sPkiData; +}; + +QString TestQgsAuthCertUtils::sPkiData = QStringLiteral( TEST_DATA_DIR ) + "/auth_system/certs_keys"; + +void TestQgsAuthCertUtils::initTestCase() +{ + QgsApplication::init(); + QgsApplication::initQgis(); + if ( QgsAuthCrypto::isDisabled() ) + QSKIP( "QCA's qca-ossl plugin is missing, skipping test case", SkipAll ); +} + +void TestQgsAuthCertUtils::cleanupTestCase() +{ + QgsApplication::exitQgis(); +} + +void TestQgsAuthCertUtils::testPkcsUtils() +{ + QByteArray pkcs; + + pkcs = QgsAuthCertUtils::fileData( sPkiData + "/gerardus_key.pem", false ); + QVERIFY( !pkcs.isEmpty() ); + QVERIFY( !QgsAuthCertUtils::pemIsPkcs8( QString( pkcs ) ) ); + + pkcs.clear(); + pkcs = QgsAuthCertUtils::fileData( sPkiData + "/gerardus_key-pkcs8-rsa.pem", false ); + QVERIFY( !pkcs.isEmpty() ); + QVERIFY( QgsAuthCertUtils::pemIsPkcs8( QString( pkcs ) ) ); + + +#ifdef Q_OS_MAC + QByteArray pkcs1; + pkcs.clear(); + + // Nothing should return nothing + pkcs1 = QgsAuthCertUtils::pkcs8PrivateKey( pkcs ); + QVERIFY( pkcs1.isEmpty() ); + + pkcs.clear(); + pkcs1.clear(); + // Is actually a PKCS#1 key, not #8 + pkcs = QgsAuthCertUtils::fileData( sPkiData + "/gerardus_key.der", false ); + QVERIFY( !pkcs.isEmpty() ); + pkcs1 = QgsAuthCertUtils::pkcs8PrivateKey( pkcs ); + QVERIFY( pkcs1.isEmpty() ); + + pkcs.clear(); + pkcs1.clear(); + // Is PKCS#1 PEM text, not DER + pkcs = QgsAuthCertUtils::fileData( sPkiData + "/gerardus_key.pem", false ); + QVERIFY( !pkcs.isEmpty() ); + pkcs1 = QgsAuthCertUtils::pkcs8PrivateKey( pkcs ); + QVERIFY( pkcs1.isEmpty() ); + + pkcs.clear(); + pkcs1.clear(); + // Is PKCS#8 PEM text, not DER + pkcs = QgsAuthCertUtils::fileData( sPkiData + "/gerardus_key-pkcs8-rsa.pem", false ); + QVERIFY( !pkcs.isEmpty() ); + pkcs1 = QgsAuthCertUtils::pkcs8PrivateKey( pkcs ); + QVERIFY( pkcs1.isEmpty() ); + + pkcs.clear(); + pkcs1.clear(); + // Correct PKCS#8 DER input + pkcs = QgsAuthCertUtils::fileData( sPkiData + "/gerardus_key-pkcs8-rsa.der", false ); + QVERIFY( !pkcs.isEmpty() ); + pkcs1 = QgsAuthCertUtils::pkcs8PrivateKey( pkcs ); + QVERIFY( !pkcs1.isEmpty() ); + + // PKCS#8 DER format should fail, and the reason for QgsAuthCertUtils::pkcs8PrivateKey + // (as of Qt5.9.0, and where macOS Qt5 SSL backend is not OpenSSL, and + // where PKCS#8 is *still* unsupported for macOS) + QSslKey pkcs8Key( pkcs, QSsl::Rsa, QSsl::Der, QSsl::PrivateKey ); + QVERIFY( pkcs8Key.isNull() ); + + // PKCS#1 DER format should work + QSslKey pkcs1Key( pkcs1, QSsl::Rsa, QSsl::Der, QSsl::PrivateKey ); + QVERIFY( !pkcs1Key.isNull() ); + + // Converted PKCS#8 DER should match PKCS#1 PEM + QByteArray pkcs1PemRef = QgsAuthCertUtils::fileData( sPkiData + "/gerardus_key.pem", true ); + QVERIFY( !pkcs1PemRef.isEmpty() ); + QCOMPARE( pkcs1Key.toPem(), pkcs1PemRef ); +#endif +} + +QGSTEST_MAIN( TestQgsAuthCertUtils ) +#include "testqgsauthcertutils.moc" diff --git a/tests/testdata/auth_system/certs_keys/fra_key-pkcs8-rsa.der b/tests/testdata/auth_system/certs_keys/fra_key-pkcs8-rsa.der new file mode 100644 index 000000000000..cd0638a71ec8 Binary files /dev/null and b/tests/testdata/auth_system/certs_keys/fra_key-pkcs8-rsa.der differ diff --git a/tests/testdata/auth_system/certs_keys/fra_key-pkcs8-rsa.pem b/tests/testdata/auth_system/certs_keys/fra_key-pkcs8-rsa.pem new file mode 100644 index 000000000000..84185ddf38f0 --- /dev/null +++ b/tests/testdata/auth_system/certs_keys/fra_key-pkcs8-rsa.pem @@ -0,0 +1,16 @@ +-----BEGIN PRIVATE KEY----- +MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAOOA/yLAj0TOL6Z3 +OGY+2JxaSmStwl5veQjp+VoAOoxDVjQDOxuBNihZ1gGUVPc2cQm8HS+nMizw9SiC +l0ZiP23QqkL9Xgd1+scE1Hhxf8cvTp2Ek3QKbKlfol3wGZgGElwkrVed77+l7PjX +VxLd2UbnZEF8TURntOyMKeYwi53vAgMBAAECgYA9/tIH41dnVZSQlV5uJmQav1QU +eXFFELV342KKzxMlU9gy1kqOJTjf6BM0XPqGX3SQRY3ihXpb2tHD10pn6LAFtiOR +ymfPJ+fs3TiPUn+Ut7TkedKkTxu5IT5C5Nu0FllcTo9mpi5ytfu6D+gkrB8fX/fZ +5+jGdevrd9WWU+v5wQJBAP8AerrTiFLCJRocP/jIdwg+gmEdcPYg5cmeNVpAUuAN +CSa5QYIQ9xB3ERUVo4ODCEGQFdYDZaPPvGp5wICo9U8CQQDkZPaaj4UegQZp/Vkf +7fQBmRzVhccxewV/HlEqJR1iQydjN3SfTU3cI0QmZL805emSN0f2sgT4lV4tdLbJ +ueVhAkEAk2C+jf21u0bz1IxhOLL7gKtIBULTx5yp0gX7BedJPq6qDFRjlP2jHUQD +fnEcKOTxP5s7043xD2T/m3Y0mOeNpwJAaFDI5Y05otYRhOVnCJNZSEWTit7APRRQ +TWAeeB5djlzXp5RTmtLnBe3BmbuYLWP5S4QeRUnHxXYLfr15IyfZ4QJBAMxGWwet +yoR03gyOwSagP53hcV5wGWu1ThKlmzrLl6ulJYb/3lwbYeNCaI5ZzGaSiycWC/8K +9zIREiwz1u/iupk= +-----END PRIVATE KEY----- diff --git a/tests/testdata/auth_system/certs_keys/gerardus_key-pkcs8-rsa.der b/tests/testdata/auth_system/certs_keys/gerardus_key-pkcs8-rsa.der new file mode 100644 index 000000000000..41ce8280105c Binary files /dev/null and b/tests/testdata/auth_system/certs_keys/gerardus_key-pkcs8-rsa.der differ diff --git a/tests/testdata/auth_system/certs_keys/gerardus_key-pkcs8-rsa.pem b/tests/testdata/auth_system/certs_keys/gerardus_key-pkcs8-rsa.pem new file mode 100644 index 000000000000..27313ab3b802 --- /dev/null +++ b/tests/testdata/auth_system/certs_keys/gerardus_key-pkcs8-rsa.pem @@ -0,0 +1,16 @@ +-----BEGIN PRIVATE KEY----- +MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBALrXW9BbnHSp96kA +naNMFr7Wu4xUd0HwPiLmkSpriRqSMI9EQF6+Bib50BIHYGpmAXdsH44FoD8k2r9q +C5sjiJpQY7pba3IB/n0pGufQJe4rw2lKWBKvl3ADjVoqPfCBT5mnpyEppDmXxOJr +ANH+ojsnjZh44fkq6emgEfganGcrAgMBAAECgYEAuO2jQG0MRCRernWfkRskgCrF +YrXPfAIvXhfboqLhBt2fFo41MBDgwf8MRGvssCLaXLs12DoVS6pMoJxzdFANSQyl +Oqf2wLtiTMzjPslv7x8R2ho/0uLxeccP5xHpLSxypbcXF3PzCxp4gNpnZWDvwx9V +Ofgrsjx//toTiMcSYiECQQDurSJmr+wDoBblptvFbO8KrfnkvMlTlQqeYQXmceGg +Mdxcygm3nqAw/Tiqd5LUGLgQP3R/Sot8ZjSPtLme6ilTAkEAyGcSiFhx/eIYwWmp +L1AJ4DSp9MQI3nVxxwrKWwyq48zxrDcSZcUJYFMphgfgzTwupTMoDNwxPiNdkUxN +SdaXyQJAIUSyydt1q1+yMVqbwZ4Yh8WOUoraCTN6Im9lsiRnjbvFeo2S4yxSKeHx +9xjpt3Smm2Us6N1MKg/Y/br0MKl1DwJBAIIGrnWcvUl3G4zSm51BF0dLpEJVt1Nv +bEUy8RymWXK4lM2iZeN2NqEzFCwMjIVdWP6C9KdzbtfcZmdR1IvmGlECQQDDzyIT +6g6z5IxBF1zQJAct34UZyLR+gjcTnT4CAjensHbpEbUvBuKT4D8S+No643rCwRQz +mgvgSjp6glQuamby +-----END PRIVATE KEY----- diff --git a/tests/testdata/auth_system/certs_keys/nicholas_key-pkcs8-rsa.der b/tests/testdata/auth_system/certs_keys/nicholas_key-pkcs8-rsa.der new file mode 100644 index 000000000000..55cc41591c41 Binary files /dev/null and b/tests/testdata/auth_system/certs_keys/nicholas_key-pkcs8-rsa.der differ diff --git a/tests/testdata/auth_system/certs_keys/nicholas_key-pkcs8-rsa.pem b/tests/testdata/auth_system/certs_keys/nicholas_key-pkcs8-rsa.pem new file mode 100644 index 000000000000..b31733e44ad6 --- /dev/null +++ b/tests/testdata/auth_system/certs_keys/nicholas_key-pkcs8-rsa.pem @@ -0,0 +1,16 @@ +-----BEGIN PRIVATE KEY----- +MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAMuZd8GDiWvpMU7W +LJBSeDL/bXzdbnmL35RmF+gDOQOS5zZFFz6Nn2PD6xPJxPTSzG46BLjeXdvZVsNN +nx8JvLZ4XdehXGdue9C8iHTeNS5di/E9rwBYRaHK9D8NUN54xQLs7SL4v5Y//x3/ +1p2pRWzhMsJm4o4xi2A0pi4qQmthAgMBAAECgYBIMrvM26A3rBHYKwrSguws6Xch ++EPcxkUakrmXhM0K/2UOUaHUhNQoxKjv83TsfHQSAnD6PaB6/a9Owo/SqdlJGXv3 +f+lvsHx283/PCD/fO/P0NE6L/9S0yYKbYd5r1PvbWBM66+AKS/1u6GevIp9UCpOJ +f7P3xwTOcNSIPgP2AQJBAO9vyckXBNcmtASzOUikQk+K4qyQEqzysS1HpuLt7y/w +r75BB2sM8h7cXimYIlSPTbPrcMXzoA0rB6iHrhq1sIkCQQDZrwjd/SC9y/9fi+LB +MxNnai/f9h+nQzP2VWLNIakDIHXcHaqWp2GjvQW+M6XH/pMJ8g7iFbXi2YtVL2iA +6z4ZAkAvXfkYXAJsIc75Iw+RDFXF8J7ZLoNTTYu5fnRIbnOkE0RhKfIyvlPjwQqr +xdn8yoC/uDMOJh0incGdGIJb7FepAkB18c2XIdB8paw/Y7a/wWHRFYrNCTkLUnE0 +Ff2LcaJ2jD7vva8xI43WvtL+xFMdsoSOzfVccDD1sbM5u48e0tb5AkA24I2q6BE8 +dkW3irynMHLu2y19C6k/QeZRExix8dEpLJh0MRPwtIqeijMav+YSzHLO2h7sHhB3 +LDQWscGKrSRu +-----END PRIVATE KEY----- diff --git a/tests/testdata/auth_system/certs_keys/ptolemy_key-pkcs8-rsa.der b/tests/testdata/auth_system/certs_keys/ptolemy_key-pkcs8-rsa.der new file mode 100644 index 000000000000..4f170ec062d9 Binary files /dev/null and b/tests/testdata/auth_system/certs_keys/ptolemy_key-pkcs8-rsa.der differ diff --git a/tests/testdata/auth_system/certs_keys/ptolemy_key-pkcs8-rsa.pem b/tests/testdata/auth_system/certs_keys/ptolemy_key-pkcs8-rsa.pem new file mode 100644 index 000000000000..9ca8ca3c4217 --- /dev/null +++ b/tests/testdata/auth_system/certs_keys/ptolemy_key-pkcs8-rsa.pem @@ -0,0 +1,16 @@ +-----BEGIN PRIVATE KEY----- +MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAK7KI7uoxRgWQ4AI +dAqsGao/M1Mg0C8poMQuv4k2ORplioPLlna5fTlhBFZBiB9f86QLoonwS0iEwhs3 +5VdJtlvEorjud7HmQesU6SWk7bNBQTdWbJPJgM48ivIjZSjVvBj3zEDJL5u948uJ +67zP1gQ5qqOrXDCV5KpXGOkRDDubAgMBAAECgYAgNQcYkSSgJ5oQgX5AaS3hfPvM +GYPC7Py+qY6JjgA/qO45Es6K2esFI6dU7YZToa6XT72HhUuZ9Tx/H3GW//Il8MJh +WEmiy6hB+yX3yEgq+CuUCgxnZd6BhX6H3O4dRBFxHaTEUjQJpZWrIS0vAzbdgJuM +vDbNDYuYgF/ZAMwGYQJBANbxDJzrYQxIelamwiEO9uPGvOoHRopLoIaFZPneSnpp +kIyLoCqikAjQuqeqVEOFXlYFfR8wVT9aq/RXqaw0A00CQQDQLZnGwhiaptBn8BL+ +6RjJM1Rmc1jmjiSNkp+ow573ttJhdgHnC0+CjOcwQu5Db2nzDkT+kkOLm5aCzOuZ +/XaHAkEAgAUOWCAxq1k31Ih6M6pwDnZ+an1u3EvzDmxBGjn17jcV6z/2Y65zT2zS +364phhXXfDDEt2DYRWXB6USVQIWyOQJAdEkEnQHOvJRx1Z1E/x81uS3y90d3YVIF +GQ/OH3cmVTjKS6afaW/n+gS7HzpD3Wdex2YxJAKPuGwwpt/QuzPaAQJAGP8nj5g9 +oYxzD+x018fxSf/BsTjXU2S6SrbIg5D4B5s5kYFXOvLUS4rfjGWxssyHdtZuFf7T +VMmSp5bTS7YAfQ== +-----END PRIVATE KEY-----