Skip to content

Commit 3210f85

Browse files
authoredOct 26, 2017
Merge pull request #5401 from boundlessgeo/pkcs8-to-pkcs1
[auth] Convert PKCS8 to PKCS1 private keys (for macOS)
2 parents 1ae0857 + ef7a29d commit 3210f85

21 files changed

+632
-57
lines changed
 

‎CMakeLists.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,11 @@ IF(WITH_CORE)
320320
FIND_QCAOSSL_PLUGIN_CPP(ENABLE_TESTS)
321321
ENDIF(NOT MSVC)
322322

323+
IF (APPLE)
324+
# Libtasn1 is for DER-encoded PKI ASN.1 parsing/extracting workarounds
325+
FIND_PACKAGE(Libtasn1 REQUIRED)
326+
ENDIF (APPLE)
327+
323328
IF (SUPPRESS_QT_WARNINGS)
324329
# Newer versions of UseQt4.cmake include Qt with -isystem automatically
325330
# This can be used to force this behavior on older systems

‎cmake/FindLibtasn1.cmake

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Find Libtasn1
2+
# ~~~~~~~~~~~~~~~
3+
# CMake module to search for Libtasn1 ASN.1 library and header(s) from:
4+
# https://www.gnu.org/software/libtasn1/
5+
#
6+
# If it's found it sets LIBTASN1_FOUND to TRUE
7+
# and following variables are set:
8+
# LIBTASN1_INCLUDE_DIR
9+
# LIBTASN1_LIBRARY
10+
#
11+
# Copyright (c) 2017, Boundless Spatial
12+
# Author: Larry Shaffer <lshaffer (at) boundlessgeo (dot) com>
13+
#
14+
# Redistribution and use is allowed according to the terms of the BSD license.
15+
# For details see the accompanying COPYING-CMAKE-SCRIPTS file.
16+
17+
18+
find_path(LIBTASN1_INCLUDE_DIR
19+
NAMES libtasn1.h
20+
PATHS
21+
${LIB_DIR}/include
22+
"$ENV{LIB_DIR}/include"
23+
$ENV{INCLUDE}
24+
/usr/local/include
25+
/usr/include
26+
)
27+
28+
find_library(LIBTASN1_LIBRARY
29+
NAMES tasn1
30+
PATHS
31+
${LIB_DIR}
32+
"$ENV{LIB_DIR}"
33+
$ENV{LIB}
34+
/usr/local/lib
35+
/usr/lib
36+
)
37+
38+
include(FindPackageHandleStandardArgs)
39+
find_package_handle_standard_args(
40+
Libtasn1
41+
REQUIRED_VARS LIBTASN1_INCLUDE_DIR LIBTASN1_LIBRARY
42+
FOUND_VAR LIBTASN1_FOUND
43+
)
44+
45+
mark_as_advanced(LIBTASN1_INCLUDE_DIR LIBTASN1_LIBRARY)

‎python/core/auth/qgsauthcertutils.sip

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,15 @@ Map certificate sha1 to certificate as simple cache
8181
%End
8282

8383

84+
static QByteArray fileData( const QString &path, bool astext = false );
85+
%Docstring
86+
Return data from a local file via a read-only operation
87+
\param path Path to file to read
88+
\param astext Whether to open the file as text, otherwise as binary
89+
:return: All data contained in file or empty contents if file does not exist
90+
:rtype: QByteArray
91+
%End
92+
8493
static QList<QSslCertificate> certsFromFile( const QString &certspath );
8594
%Docstring
8695
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
150159
:rtype: list of str
151160
%End
152161

162+
static bool pemIsPkcs8( const QString &keyPemTxt );
163+
%Docstring
164+
Determine if the PEM-encoded text of a key is PKCS#8 format
165+
\param keyPemTxt PEM-encoded text
166+
:return: True if PKCS#8, otherwise false
167+
:rtype: bool
168+
%End
169+
170+
171+
153172
static QStringList pkcs12BundleToPem( const QString &bundlepath,
154173
const QString &bundlepass = QString(),
155174
bool reencrypt = true );

‎resources/CMakeLists.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,9 @@ INSTALL(DIRECTORY data DESTINATION ${QGIS_DATA_DIR}/resources)
99
IF (WITH_SERVER)
1010
INSTALL(DIRECTORY server DESTINATION ${QGIS_DATA_DIR}/resources)
1111
ENDIF (WITH_SERVER)
12+
13+
IF (APPLE)
14+
# ASN.1 definition files of PKIX elements
15+
INSTALL(FILES pkcs8.asn
16+
DESTINATION ${QGIS_DATA_DIR}/resources)
17+
ENDIF (APPLE)

‎resources/pkcs8.asn

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
PKCS-8 {iso(1) member-body(2) us(840) rsadsi(113549) pkcs(1) pkcs-8(8)
2+
modules(1) pkcs-8(1)}
3+
4+
-- $Revision: 1.5 $
5+
6+
-- This module has been checked for conformance with the ASN.1
7+
-- standard by the OSS ASN.1 Tools
8+
9+
DEFINITIONS EXPLICIT TAGS ::=
10+
11+
BEGIN
12+
13+
-- EXPORTS All --
14+
-- All types and values defined in this module is exported for use in
15+
-- other ASN.1 modules.
16+
17+
-- attribute data types --
18+
19+
Attribute ::= SEQUENCE {
20+
type AttributeType,
21+
values SET OF AttributeValue
22+
-- at least one value is required --
23+
}
24+
25+
AttributeType ::= OBJECT IDENTIFIER
26+
27+
AttributeValue ::= ANY DEFINED BY type
28+
29+
AttributeTypeAndValue ::= SEQUENCE {
30+
type AttributeType,
31+
value AttributeValue }
32+
33+
AlgorithmIdentifier ::= SEQUENCE {
34+
algorithm OBJECT IDENTIFIER,
35+
parameters ANY DEFINED BY algorithm OPTIONAL }
36+
-- contains a value of the type
37+
-- registered for use with the
38+
-- algorithm object identifier value
39+
40+
-- Private-key information syntax
41+
42+
PrivateKeyInfo ::= SEQUENCE {
43+
version Version,
44+
privateKeyAlgorithm AlgorithmIdentifier,
45+
privateKey PrivateKey,
46+
attributes [0] Attributes OPTIONAL }
47+
48+
Version ::= INTEGER {v1(0)}
49+
50+
PrivateKey ::= OCTET STRING
51+
52+
Attributes ::= SET OF Attribute
53+
54+
-- Encrypted private-key information syntax
55+
56+
EncryptedPrivateKeyInfo ::= SEQUENCE {
57+
encryptionAlgorithm AlgorithmIdentifier,
58+
encryptedData EncryptedData
59+
}
60+
61+
EncryptedData ::= OCTET STRING
62+
63+
END

‎src/auth/pkipkcs12/qgsauthpkcs12method.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,12 @@ QgsPkiConfigBundle *QgsAuthPkcs12Method::getPkiConfigBundle( const QString &auth
283283
QStringList bundlelist = QgsAuthCertUtils::pkcs12BundleToPem( mconfig.config( QStringLiteral( "bundlepath" ) ),
284284
mconfig.config( QStringLiteral( "bundlepass" ) ), false );
285285

286+
if ( bundlelist.isEmpty() || bundlelist.size() < 2 )
287+
{
288+
QgsDebugMsg( QString( "PKI bundle for authcfg %1: insert FAILED, PKCS#12 bundle parsing failed" ).arg( authcfg ) );
289+
return bundle;
290+
}
291+
286292
// init client cert
287293
// Note: if this is not valid, no sense continuing
288294
QSslCertificate clientcert( bundlelist.at( 0 ).toLatin1() );
@@ -292,6 +298,11 @@ QgsPkiConfigBundle *QgsAuthPkcs12Method::getPkiConfigBundle( const QString &auth
292298
return bundle;
293299
}
294300

301+
// !!! DON'T LEAVE THESE UNCOMMENTED !!!
302+
// QgsDebugMsg( QString( "PKI bundle key for authcfg: \n%1" ).arg( bundlelist.at( 1 ) ) );
303+
// QgsDebugMsg( QString( "PKI bundle key pass for authcfg: \n%1" )
304+
// .arg( !mconfig.config( QStringLiteral( "bundlepass" ) ).isNull() ? mconfig.config( QStringLiteral( "bundlepass" ) ) : QStringLiteral() ) );
305+
295306
// init key
296307
QSslKey clientkey( bundlelist.at( 1 ).toLatin1(),
297308
QSsl::Rsa,

‎src/core/CMakeLists.txt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1181,6 +1181,13 @@ INCLUDE_DIRECTORIES(SYSTEM
11811181
${QTKEYCHAIN_INCLUDE_DIR}
11821182
)
11831183

1184+
IF (APPLE)
1185+
# Libtasn1 is for DER-encoded PKI ASN.1 parsing/extracting workarounds
1186+
INCLUDE_DIRECTORIES(SYSTEM
1187+
${LIBTASN1_INCLUDE_DIR}
1188+
)
1189+
ENDIF (APPLE)
1190+
11841191

11851192
#for PAL classes
11861193
IF (WIN32)
@@ -1251,7 +1258,7 @@ IF (WIN32)
12511258
ENDIF (WIN32)
12521259

12531260
IF (APPLE)
1254-
TARGET_LINK_LIBRARIES(qgis_core qgis_native)
1261+
TARGET_LINK_LIBRARIES(qgis_core qgis_native ${LIBTASN1_LIBRARY})
12551262
ENDIF (APPLE)
12561263

12571264
IF (NOT WITH_INTERNAL_QEXTSERIALPORT)

‎src/core/auth/qgsauthcertutils.cpp

Lines changed: 237 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,17 @@
2323
#include <QSslCertificate>
2424
#include <QUuid>
2525

26+
#include "qgsapplication.h"
2627
#include "qgsauthmanager.h"
2728
#include "qgslogger.h"
2829
#include "qgsapplication.h"
2930

31+
#ifdef Q_OS_MAC
32+
#include <string.h>
33+
#include "libtasn1.h"
34+
#endif
35+
36+
3037
QString QgsAuthCertUtils::getSslProtocolName( QSsl::SslProtocol protocol )
3138
{
3239
switch ( protocol )
@@ -95,30 +102,34 @@ QMap<QString, QList<QgsAuthConfigSslServer> > QgsAuthCertUtils::sslConfigsGroupe
95102
return orgconfigs;
96103
}
97104

98-
static QByteArray fileData_( const QString &path, bool astext = false )
105+
QByteArray QgsAuthCertUtils::fileData( const QString &path, bool astext )
99106
{
100107
QByteArray data;
101108
QFile file( path );
102-
if ( file.exists() )
109+
if ( !file.exists() )
103110
{
104-
QFile::OpenMode openflags( QIODevice::ReadOnly );
105-
if ( astext )
106-
openflags |= QIODevice::Text;
107-
bool ret = file.open( openflags );
108-
if ( ret )
109-
{
110-
data = file.readAll();
111-
}
112-
file.close();
111+
QgsDebugMsg( QStringLiteral( "Read file error, file not found: %1" ).arg( path ) );
112+
return data;
113113
}
114+
// TODO: add checks for locked file, etc., to ensure it can be read
115+
QFile::OpenMode openflags( QIODevice::ReadOnly );
116+
if ( astext )
117+
openflags |= QIODevice::Text;
118+
bool ret = file.open( openflags );
119+
if ( ret )
120+
{
121+
data = file.readAll();
122+
}
123+
file.close();
124+
114125
return data;
115126
}
116127

117128
QList<QSslCertificate> QgsAuthCertUtils::certsFromFile( const QString &certspath )
118129
{
119130
QList<QSslCertificate> certs;
120131
bool pem = certspath.endsWith( QLatin1String( ".pem" ), Qt::CaseInsensitive );
121-
certs = QSslCertificate::fromData( fileData_( certspath, pem ), pem ? QSsl::Pem : QSsl::Der );
132+
certs = QSslCertificate::fromData( QgsAuthCertUtils::fileData( certspath, pem ), pem ? QSsl::Pem : QSsl::Der );
122133
if ( certs.isEmpty() )
123134
{
124135
QgsDebugMsg( QString( "Parsed cert(s) EMPTY for path: %1" ).arg( certspath ) );
@@ -182,7 +193,7 @@ QSslKey QgsAuthCertUtils::keyFromFile( const QString &keypath,
182193
QString *algtype )
183194
{
184195
bool pem = keypath.endsWith( QLatin1String( ".pem" ), Qt::CaseInsensitive );
185-
QByteArray keydata( fileData_( keypath, pem ) );
196+
QByteArray keydata( QgsAuthCertUtils::fileData( keypath, pem ) );
186197

187198
QSslKey clientkey;
188199
clientkey = QSslKey( keydata,
@@ -263,37 +274,249 @@ QStringList QgsAuthCertUtils::certKeyBundleToPem( const QString &certpath,
263274
return QStringList() << certpem << keypem << algtype;
264275
}
265276

277+
bool QgsAuthCertUtils::pemIsPkcs8( const QString &keyPemTxt )
278+
{
279+
QString pkcs8Header = QStringLiteral( "-----BEGIN PRIVATE KEY-----" );
280+
QString pkcs8Footer = QStringLiteral( "-----END PRIVATE KEY-----" );
281+
return keyPemTxt.contains( pkcs8Header ) && keyPemTxt.contains( pkcs8Footer );
282+
}
283+
284+
#ifdef Q_OS_MAC
285+
QByteArray QgsAuthCertUtils::pkcs8PrivateKey( QByteArray &pkcs8Der )
286+
{
287+
QByteArray pkcs1;
288+
289+
if ( pkcs8Der.isEmpty() )
290+
{
291+
QgsDebugMsg( QStringLiteral( "ERROR, passed DER is empty" ) );
292+
return pkcs1;
293+
}
294+
// Dump as unarmored PEM format, e.g. missing '-----BEGIN|END...' wrapper
295+
//QgsDebugMsg ( QStringLiteral( "pkcs8Der: %1" ).arg( QString( pkcs8Der.toBase64() ) ) );
296+
297+
QFileInfo asnDefsRsrc( QgsApplication::pkgDataPath() + QStringLiteral( "/resources/pkcs8.asn" ) );
298+
if ( ! asnDefsRsrc.exists() )
299+
{
300+
QgsDebugMsg( QStringLiteral( "ERROR, pkcs.asn resource file not found: %1" ).arg( asnDefsRsrc.filePath() ) );
301+
return pkcs1;
302+
}
303+
const char *asnDefsFile = asnDefsRsrc.absoluteFilePath().toLocal8Bit().constData();
304+
305+
int asn1_result = ASN1_SUCCESS, der_len = 0, oct_len = 0;
306+
asn1_node definitions = NULL, structure = NULL;
307+
char errorDescription[ASN1_MAX_ERROR_DESCRIPTION_SIZE], oct_data[1024];
308+
unsigned char *der = NULL;
309+
unsigned int flags = 0; //TODO: see if any or all ASN1_DECODE_FLAG_* flags can be set
310+
unsigned oct_etype;
311+
312+
// Base PKCS#8 element to decode
313+
QString typeName( QStringLiteral( "PKCS-8.PrivateKeyInfo" ) );
314+
315+
asn1_result = asn1_parser2tree( asnDefsFile, &definitions, errorDescription );
316+
317+
switch ( asn1_result )
318+
{
319+
case ASN1_SUCCESS:
320+
QgsDebugMsgLevel( QStringLiteral( "Parse: done.\n" ), 4 );
321+
break;
322+
case ASN1_FILE_NOT_FOUND:
323+
QgsDebugMsg( QStringLiteral( "ERROR, file not found: %1" ).arg( asnDefsFile ) );
324+
return pkcs1;
325+
case ASN1_SYNTAX_ERROR:
326+
case ASN1_IDENTIFIER_NOT_FOUND:
327+
case ASN1_NAME_TOO_LONG:
328+
QgsDebugMsg( QStringLiteral( "ERROR, asn1 parsing: %1" ).arg( errorDescription ) );
329+
return pkcs1;
330+
default:
331+
QgsDebugMsg( QStringLiteral( "ERROR, libtasn1: %1" ).arg( asn1_strerror( asn1_result ) ) );
332+
return pkcs1;
333+
}
334+
335+
// Generate the ASN.1 structure
336+
asn1_result = asn1_create_element( definitions, typeName.toLatin1().constData(), &structure );
337+
338+
//asn1_print_structure( stdout, structure, "", ASN1_PRINT_ALL);
339+
340+
if ( asn1_result != ASN1_SUCCESS )
341+
{
342+
QgsDebugMsg( QStringLiteral( "ERROR, structure creation: %1" ).arg( asn1_strerror( asn1_result ) ) );
343+
goto PKCS1DONE;
344+
}
345+
346+
// Populate the ASN.1 structure with decoded DER data
347+
der = reinterpret_cast<unsigned char *>( pkcs8Der.data() );
348+
der_len = pkcs8Der.size();
349+
350+
if ( flags != 0 )
351+
{
352+
asn1_result = asn1_der_decoding2( &structure, der, &der_len, flags, errorDescription );
353+
}
354+
else
355+
{
356+
asn1_result = asn1_der_decoding( &structure, der, der_len, errorDescription );
357+
}
358+
359+
if ( asn1_result != ASN1_SUCCESS )
360+
{
361+
QgsDebugMsg( QStringLiteral( "ERROR, decoding: %1" ).arg( errorDescription ) );
362+
goto PKCS1DONE;
363+
}
364+
else
365+
{
366+
QgsDebugMsgLevel( QStringLiteral( "Decoding: %1" ).arg( asn1_strerror( asn1_result ) ), 4 );
367+
}
368+
369+
if ( QgsLogger::debugLevel() >= 4 )
370+
{
371+
QgsDebugMsg( QStringLiteral( "DECODING RESULT:" ) );
372+
asn1_print_structure( stdout, structure, "", ASN1_PRINT_NAME_TYPE_VALUE );
373+
}
374+
375+
// Validate and extract privateKey value
376+
QgsDebugMsgLevel( QStringLiteral( "Validating privateKey type..." ), 4 );
377+
typeName.append( QStringLiteral( ".privateKey" ) );
378+
QgsDebugMsgLevel( QStringLiteral( "privateKey element name: %1" ).arg( typeName ), 4 );
379+
380+
asn1_result = asn1_read_value_type( structure, "privateKey", NULL, &oct_len, &oct_etype );
381+
382+
if ( asn1_result != ASN1_MEM_ERROR ) // not sure why ASN1_MEM_ERROR = success, but it does
383+
{
384+
QgsDebugMsg( QStringLiteral( "ERROR, asn1 read privateKey value type: %1" ).arg( asn1_strerror( asn1_result ) ) );
385+
goto PKCS1DONE;
386+
}
387+
388+
if ( oct_etype != ASN1_ETYPE_OCTET_STRING )
389+
{
390+
QgsDebugMsg( QStringLiteral( "ERROR, asn1 privateKey value not octet string, but type: %1" ).arg( static_cast<int>( oct_etype ) ) );
391+
goto PKCS1DONE;
392+
}
393+
394+
if ( oct_len == 0 )
395+
{
396+
QgsDebugMsg( QStringLiteral( "ERROR, asn1 privateKey octet string empty" ) );
397+
goto PKCS1DONE;
398+
}
399+
400+
QgsDebugMsgLevel( QStringLiteral( "Reading privateKey value..." ), 4 );
401+
asn1_result = asn1_read_value( structure, "privateKey", oct_data, &oct_len );
402+
403+
if ( asn1_result != ASN1_SUCCESS )
404+
{
405+
QgsDebugMsg( QStringLiteral( "ERROR, asn1 read privateKey value: %1" ).arg( asn1_strerror( asn1_result ) ) );
406+
goto PKCS1DONE;
407+
}
408+
409+
if ( oct_len == 0 )
410+
{
411+
QgsDebugMsg( QStringLiteral( "ERROR, asn1 privateKey value octet string empty" ) );
412+
goto PKCS1DONE;
413+
}
414+
415+
pkcs1 = QByteArray( oct_data, oct_len );
416+
417+
// !!! SENSITIVE DATA - DO NOT LEAVE UNCOMMENTED !!!
418+
//QgsDebugMsgLevel( QStringLiteral( "privateKey octet data as PEM: %1" ).arg( QString( pkcs1.toBase64() ) ), 4 );
419+
420+
PKCS1DONE:
421+
422+
asn1_delete_structure( &structure );
423+
return pkcs1;
424+
}
425+
#endif
426+
266427
QStringList QgsAuthCertUtils::pkcs12BundleToPem( const QString &bundlepath,
267428
const QString &bundlepass,
268429
bool reencrypt )
269430
{
270431
QStringList empty;
271432
if ( !QCA::isSupported( "pkcs12" ) )
433+
{
434+
QgsDebugMsg( QString( "QCA does not support PKCS#12" ) );
272435
return empty;
436+
}
273437

274438
QCA::KeyBundle bundle( QgsAuthCertUtils::qcaKeyBundle( bundlepath, bundlepass ) );
275439
if ( bundle.isNull() )
440+
{
441+
QgsDebugMsg( QString( "FAILED to convert PKCS#12 file to QCA key bundle: %1" ).arg( bundlepath ) );
276442
return empty;
443+
}
277444

278445
QCA::SecureArray passarray;
279446
if ( reencrypt && !bundlepass.isEmpty() )
447+
{
280448
passarray = QCA::SecureArray( bundlepass.toUtf8() );
449+
}
281450

282451
QString algtype;
452+
QSsl::KeyAlgorithm keyalg = QSsl::Opaque;
283453
if ( bundle.privateKey().isRSA() )
284454
{
285455
algtype = QStringLiteral( "rsa" );
456+
keyalg = QSsl::Rsa;
286457
}
287458
else if ( bundle.privateKey().isDSA() )
288459
{
289460
algtype = QStringLiteral( "dsa" );
461+
keyalg = QSsl::Dsa;
290462
}
291463
else if ( bundle.privateKey().isDH() )
292464
{
293465
algtype = QStringLiteral( "dh" );
294466
}
467+
// TODO: add support for EC keys, once QCA supports them
468+
469+
// can currently only support RSA and DSA between QCA and Qt
470+
if ( keyalg == QSsl::Opaque )
471+
{
472+
QgsDebugMsg( QString( "FAILED to read PKCS#12 key (only RSA and DSA algorithms supported): %1" ).arg( bundlepath ) );
473+
return empty;
474+
}
475+
476+
QString keyPem;
477+
#ifdef Q_OS_MAC
478+
if ( keyalg == QSsl::Rsa && QgsAuthCertUtils::pemIsPkcs8( bundle.privateKey().toPEM() ) )
479+
{
480+
QgsDebugMsgLevel( QString( "Private key is PKCS#8: attempting conversion to PKCS#1..." ), 4 );
481+
// if RSA, convert from PKCS#8 key to 'traditional' OpenSSL RSA format, which Qt prefers
482+
// note: QCA uses OpenSSL, regardless of the Qt SSL backend, and 1.0.2+ OpenSSL versions return
483+
// RSA private keys as PKCS#8, which choke Qt upon QSslKey creation
484+
485+
QByteArray pkcs8Der = bundle.privateKey().toDER().toByteArray();
486+
if ( pkcs8Der.isEmpty() )
487+
{
488+
QgsDebugMsg( QString( "FAILED to convert PKCS#12 key to DER-encoded format: %1" ).arg( bundlepath ) );
489+
return empty;
490+
}
491+
492+
QByteArray pkcs1Der = QgsAuthCertUtils::pkcs8PrivateKey( pkcs8Der );
493+
if ( pkcs1Der.isEmpty() )
494+
{
495+
QgsDebugMsg( QString( "FAILED to convert PKCS#12 key from PKCS#8 to PKCS#1: %1" ).arg( bundlepath ) );
496+
return empty;
497+
}
498+
499+
QSslKey pkcs1Key( pkcs1Der, QSsl::Rsa, QSsl::Der, QSsl::PrivateKey );
500+
if ( pkcs1Key.isNull() )
501+
{
502+
QgsDebugMsg( QString( "FAILED to convert PKCS#12 key from PKCS#8 to PKCS#1 QSslKey: %1" ).arg( bundlepath ) );
503+
return empty;
504+
}
505+
keyPem = QString( pkcs1Key.toPem( passarray.toByteArray() ) );
506+
}
507+
else
508+
{
509+
keyPem = bundle.privateKey().toPEM( passarray );
510+
}
511+
#else
512+
keyPem = bundle.privateKey().toPEM( passarray );
513+
#endif
514+
515+
QgsDebugMsgLevel( QString( "PKCS#12 cert as PEM:\n%1" ).arg( QString( bundle.certificateChain().primary().toPEM() ) ), 4 );
516+
// !!! SENSITIVE DATA - DO NOT LEAVE UNCOMMENTED !!!
517+
//QgsDebugMsgLevel( QString( "PKCS#12 key as PEM:\n%1" ).arg( QString( keyPem ) ), 4 );
295518

296-
return QStringList() << bundle.certificateChain().primary().toPEM() << bundle.privateKey().toPEM( passarray ) << algtype;
519+
return QStringList() << bundle.certificateChain().primary().toPEM() << keyPem << algtype;
297520
}
298521

299522
QList<QSslCertificate> QgsAuthCertUtils::pkcs12BundleCas( const QString &bundlepath, const QString &bundlepass )

‎src/core/auth/qgsauthcertutils.h

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,14 @@ class CORE_EXPORT QgsAuthCertUtils
104104
*/
105105
static QMap< QString, QList<QgsAuthConfigSslServer> > sslConfigsGroupedByOrg( const QList<QgsAuthConfigSslServer> &configs ) SIP_SKIP;
106106

107+
/**
108+
* Return data from a local file via a read-only operation
109+
* \param path Path to file to read
110+
* \param astext Whether to open the file as text, otherwise as binary
111+
* \returns All data contained in file or empty contents if file does not exist
112+
*/
113+
static QByteArray fileData( const QString &path, bool astext = false );
114+
107115
//! Return list of concatenated certs from a PEM or DER formatted file
108116
static QList<QSslCertificate> certsFromFile( const QString &certspath );
109117

@@ -157,6 +165,31 @@ class CORE_EXPORT QgsAuthCertUtils
157165
const QString &keypass = QString(),
158166
bool reencrypt = true );
159167

168+
/**
169+
* Determine if the PEM-encoded text of a key is PKCS#8 format
170+
* \param keyPemTxt PEM-encoded text
171+
* \returns True if PKCS#8, otherwise false
172+
*/
173+
static bool pemIsPkcs8( const QString &keyPemTxt );
174+
175+
#ifdef Q_OS_MAC
176+
177+
/**
178+
* Extract the PrivateKey ASN.1 element of a DER-encoded PKCS#8 private key
179+
* \param pkcs8Der PKCS#8 DER-encoded private key data
180+
* \returns DER-encoded private key on success or an empty QByteArray upon failure
181+
* \note On some platforms, e.g. macOS, where the default SSL backend is not OpenSSL, a QSslKey
182+
* can not be created using PKCS#8-formatted data. However, PKCS#8 private key ASN.1 structures
183+
* contain the key data inside a wrapper describing the algorithm used, e.g. RSA, DSA, ECC etc.
184+
* Extracted PrivateKey ASN.1 data can be used to create a compatible QSslKey,
185+
* e.g. 'traditional' SSLeay RSA-specific PKCS#1.
186+
* By default OpenSSL 1.0.0+ returns private keys as PKCS#8, previously it was PKCS#1.
187+
* \note This function requires 'libtasn1' development files and library, which is used
188+
* to parse and extract the PrivateKey element from an ASN.1 PKCS#8 structure.
189+
*/
190+
static QByteArray pkcs8PrivateKey( QByteArray &pkcs8Der ) SIP_SKIP;
191+
#endif
192+
160193
/**
161194
* Return list of certificate, private key and algorithm (as PEM text) for a PKCS#12 bundle
162195
* \param bundlepath File path to the PKCS bundle

‎src/core/auth/qgsauthconfig.cpp

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
#include <QCryptographicHash>
2424
#include <QUrl>
2525

26+
#include "qgsauthcertutils.h"
27+
2628

2729
//////////////////////////////////////////////
2830
// QgsAuthMethodConfig
@@ -172,25 +174,6 @@ QgsPkiBundle::QgsPkiBundle( const QSslCertificate &clientCert,
172174
setClientKey( clientKey );
173175
}
174176

175-
static QByteArray fileData_( const QString &path, bool astext = false )
176-
{
177-
QByteArray data;
178-
QFile file( path );
179-
if ( file.exists() )
180-
{
181-
QFile::OpenMode openflags( QIODevice::ReadOnly );
182-
if ( astext )
183-
openflags |= QIODevice::Text;
184-
bool ret = file.open( openflags );
185-
if ( ret )
186-
{
187-
data = file.readAll();
188-
}
189-
file.close();
190-
}
191-
return data;
192-
}
193-
194177
const QgsPkiBundle QgsPkiBundle::fromPemPaths( const QString &certPath,
195178
const QString &keyPath,
196179
const QString &keyPass,
@@ -207,12 +190,12 @@ const QgsPkiBundle QgsPkiBundle::fromPemPaths( const QString &certPath,
207190
{
208191
// client cert
209192
bool pem = certPath.endsWith( QLatin1String( ".pem" ), Qt::CaseInsensitive );
210-
QSslCertificate clientcert( fileData_( certPath, pem ), pem ? QSsl::Pem : QSsl::Der );
193+
QSslCertificate clientcert( QgsAuthCertUtils::fileData( certPath, pem ), pem ? QSsl::Pem : QSsl::Der );
211194
pkibundle.setClientCert( clientcert );
212195

213196
// client key
214197
bool pem_key = keyPath.endsWith( QLatin1String( ".pem" ), Qt::CaseInsensitive );
215-
QByteArray keydata( fileData_( keyPath, pem_key ) );
198+
QByteArray keydata( QgsAuthCertUtils::fileData( keyPath, pem_key ) );
216199

217200
QSslKey clientkey;
218201
clientkey = QSslKey( keydata,

‎src/gui/auth/qgsauthimportidentitydialog.cpp

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -30,26 +30,6 @@
3030
#include "qgsapplication.h"
3131

3232

33-
static QByteArray fileData_( const QString &path, bool astext = false )
34-
{
35-
QByteArray data;
36-
QFile file( path );
37-
if ( file.exists() )
38-
{
39-
QFile::OpenMode openflags( QIODevice::ReadOnly );
40-
if ( astext )
41-
openflags |= QIODevice::Text;
42-
bool ret = file.open( openflags );
43-
if ( ret )
44-
{
45-
data = file.readAll();
46-
}
47-
file.close();
48-
}
49-
return data;
50-
}
51-
52-
5333
QgsAuthImportIdentityDialog::QgsAuthImportIdentityDialog( QgsAuthImportIdentityDialog::IdentityType identitytype,
5434
QWidget *parent )
5535
: QDialog( parent )
@@ -307,7 +287,7 @@ bool QgsAuthImportIdentityDialog::validatePkiPaths()
307287

308288
// check for valid private key and that any supplied password works
309289
bool keypem = keypath.endsWith( QLatin1String( ".pem" ), Qt::CaseInsensitive );
310-
QByteArray keydata( fileData_( keypath, keypem ) );
290+
QByteArray keydata( QgsAuthCertUtils::fileData( keypath, keypem ) );
311291

312292
QSslKey clientkey;
313293
QString keypass = lePkiPathsKeyPass->text();

‎tests/src/core/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ SET(TESTS
7474
testqgsapplication.cpp
7575
testqgsatlascomposition.cpp
7676
testqgsauthcrypto.cpp
77+
testqgsauthcertutils.cpp
7778
testqgsauthconfig.cpp
7879
testqgsauthmanager.cpp
7980
testqgsblendmodes.cpp
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/***************************************************************************
2+
TestQgsAuthCertUtils.cpp
3+
----------------------
4+
Date : October 2017
5+
Copyright : (C) 2017 by Boundless Spatial, Inc. USA
6+
Author : Larry Shaffer
7+
Email : lshaffer at boundlessgeo dot com
8+
***************************************************************************
9+
* *
10+
* This program is free software; you can redistribute it and/or modify *
11+
* it under the terms of the GNU General Public License as published by *
12+
* the Free Software Foundation; either version 2 of the License, or *
13+
* (at your option) any later version. *
14+
* *
15+
***************************************************************************/
16+
17+
#include "qgstest.h"
18+
#include <QObject>
19+
#include <QSslKey>
20+
#include <QString>
21+
#include <QStringList>
22+
23+
#include "qgsapplication.h"
24+
#include "qgsauthcrypto.h"
25+
#include "qgsauthcertutils.h"
26+
#include "qgslogger.h"
27+
28+
/**
29+
* \ingroup UnitTests
30+
* Unit tests for QgsAuthCertUtils static functions
31+
*/
32+
class TestQgsAuthCertUtils: public QObject
33+
{
34+
Q_OBJECT
35+
36+
private slots:
37+
void initTestCase();
38+
void cleanupTestCase();
39+
void init() {}
40+
void cleanup() {}
41+
42+
void testPkcsUtils();
43+
44+
private:
45+
static QString sPkiData;
46+
};
47+
48+
QString TestQgsAuthCertUtils::sPkiData = QStringLiteral( TEST_DATA_DIR ) + "/auth_system/certs_keys";
49+
50+
void TestQgsAuthCertUtils::initTestCase()
51+
{
52+
QgsApplication::init();
53+
QgsApplication::initQgis();
54+
if ( QgsAuthCrypto::isDisabled() )
55+
QSKIP( "QCA's qca-ossl plugin is missing, skipping test case", SkipAll );
56+
}
57+
58+
void TestQgsAuthCertUtils::cleanupTestCase()
59+
{
60+
QgsApplication::exitQgis();
61+
}
62+
63+
void TestQgsAuthCertUtils::testPkcsUtils()
64+
{
65+
QByteArray pkcs;
66+
67+
pkcs = QgsAuthCertUtils::fileData( sPkiData + "/gerardus_key.pem", false );
68+
QVERIFY( !pkcs.isEmpty() );
69+
QVERIFY( !QgsAuthCertUtils::pemIsPkcs8( QString( pkcs ) ) );
70+
71+
pkcs.clear();
72+
pkcs = QgsAuthCertUtils::fileData( sPkiData + "/gerardus_key-pkcs8-rsa.pem", false );
73+
QVERIFY( !pkcs.isEmpty() );
74+
QVERIFY( QgsAuthCertUtils::pemIsPkcs8( QString( pkcs ) ) );
75+
76+
77+
#ifdef Q_OS_MAC
78+
QByteArray pkcs1;
79+
pkcs.clear();
80+
81+
// Nothing should return nothing
82+
pkcs1 = QgsAuthCertUtils::pkcs8PrivateKey( pkcs );
83+
QVERIFY( pkcs1.isEmpty() );
84+
85+
pkcs.clear();
86+
pkcs1.clear();
87+
// Is actually a PKCS#1 key, not #8
88+
pkcs = QgsAuthCertUtils::fileData( sPkiData + "/gerardus_key.der", false );
89+
QVERIFY( !pkcs.isEmpty() );
90+
pkcs1 = QgsAuthCertUtils::pkcs8PrivateKey( pkcs );
91+
QVERIFY( pkcs1.isEmpty() );
92+
93+
pkcs.clear();
94+
pkcs1.clear();
95+
// Is PKCS#1 PEM text, not DER
96+
pkcs = QgsAuthCertUtils::fileData( sPkiData + "/gerardus_key.pem", false );
97+
QVERIFY( !pkcs.isEmpty() );
98+
pkcs1 = QgsAuthCertUtils::pkcs8PrivateKey( pkcs );
99+
QVERIFY( pkcs1.isEmpty() );
100+
101+
pkcs.clear();
102+
pkcs1.clear();
103+
// Is PKCS#8 PEM text, not DER
104+
pkcs = QgsAuthCertUtils::fileData( sPkiData + "/gerardus_key-pkcs8-rsa.pem", false );
105+
QVERIFY( !pkcs.isEmpty() );
106+
pkcs1 = QgsAuthCertUtils::pkcs8PrivateKey( pkcs );
107+
QVERIFY( pkcs1.isEmpty() );
108+
109+
pkcs.clear();
110+
pkcs1.clear();
111+
// Correct PKCS#8 DER input
112+
pkcs = QgsAuthCertUtils::fileData( sPkiData + "/gerardus_key-pkcs8-rsa.der", false );
113+
QVERIFY( !pkcs.isEmpty() );
114+
pkcs1 = QgsAuthCertUtils::pkcs8PrivateKey( pkcs );
115+
QVERIFY( !pkcs1.isEmpty() );
116+
117+
// PKCS#8 DER format should fail, and the reason for QgsAuthCertUtils::pkcs8PrivateKey
118+
// (as of Qt5.9.0, and where macOS Qt5 SSL backend is not OpenSSL, and
119+
// where PKCS#8 is *still* unsupported for macOS)
120+
QSslKey pkcs8Key( pkcs, QSsl::Rsa, QSsl::Der, QSsl::PrivateKey );
121+
QVERIFY( pkcs8Key.isNull() );
122+
123+
// PKCS#1 DER format should work
124+
QSslKey pkcs1Key( pkcs1, QSsl::Rsa, QSsl::Der, QSsl::PrivateKey );
125+
QVERIFY( !pkcs1Key.isNull() );
126+
127+
// Converted PKCS#8 DER should match PKCS#1 PEM
128+
QByteArray pkcs1PemRef = QgsAuthCertUtils::fileData( sPkiData + "/gerardus_key.pem", true );
129+
QVERIFY( !pkcs1PemRef.isEmpty() );
130+
QCOMPARE( pkcs1Key.toPem(), pkcs1PemRef );
131+
#endif
132+
}
133+
134+
QGSTEST_MAIN( TestQgsAuthCertUtils )
135+
#include "testqgsauthcertutils.moc"
Binary file not shown.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
-----BEGIN PRIVATE KEY-----
2+
MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAOOA/yLAj0TOL6Z3
3+
OGY+2JxaSmStwl5veQjp+VoAOoxDVjQDOxuBNihZ1gGUVPc2cQm8HS+nMizw9SiC
4+
l0ZiP23QqkL9Xgd1+scE1Hhxf8cvTp2Ek3QKbKlfol3wGZgGElwkrVed77+l7PjX
5+
VxLd2UbnZEF8TURntOyMKeYwi53vAgMBAAECgYA9/tIH41dnVZSQlV5uJmQav1QU
6+
eXFFELV342KKzxMlU9gy1kqOJTjf6BM0XPqGX3SQRY3ihXpb2tHD10pn6LAFtiOR
7+
ymfPJ+fs3TiPUn+Ut7TkedKkTxu5IT5C5Nu0FllcTo9mpi5ytfu6D+gkrB8fX/fZ
8+
5+jGdevrd9WWU+v5wQJBAP8AerrTiFLCJRocP/jIdwg+gmEdcPYg5cmeNVpAUuAN
9+
CSa5QYIQ9xB3ERUVo4ODCEGQFdYDZaPPvGp5wICo9U8CQQDkZPaaj4UegQZp/Vkf
10+
7fQBmRzVhccxewV/HlEqJR1iQydjN3SfTU3cI0QmZL805emSN0f2sgT4lV4tdLbJ
11+
ueVhAkEAk2C+jf21u0bz1IxhOLL7gKtIBULTx5yp0gX7BedJPq6qDFRjlP2jHUQD
12+
fnEcKOTxP5s7043xD2T/m3Y0mOeNpwJAaFDI5Y05otYRhOVnCJNZSEWTit7APRRQ
13+
TWAeeB5djlzXp5RTmtLnBe3BmbuYLWP5S4QeRUnHxXYLfr15IyfZ4QJBAMxGWwet
14+
yoR03gyOwSagP53hcV5wGWu1ThKlmzrLl6ulJYb/3lwbYeNCaI5ZzGaSiycWC/8K
15+
9zIREiwz1u/iupk=
16+
-----END PRIVATE KEY-----
Binary file not shown.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
-----BEGIN PRIVATE KEY-----
2+
MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBALrXW9BbnHSp96kA
3+
naNMFr7Wu4xUd0HwPiLmkSpriRqSMI9EQF6+Bib50BIHYGpmAXdsH44FoD8k2r9q
4+
C5sjiJpQY7pba3IB/n0pGufQJe4rw2lKWBKvl3ADjVoqPfCBT5mnpyEppDmXxOJr
5+
ANH+ojsnjZh44fkq6emgEfganGcrAgMBAAECgYEAuO2jQG0MRCRernWfkRskgCrF
6+
YrXPfAIvXhfboqLhBt2fFo41MBDgwf8MRGvssCLaXLs12DoVS6pMoJxzdFANSQyl
7+
Oqf2wLtiTMzjPslv7x8R2ho/0uLxeccP5xHpLSxypbcXF3PzCxp4gNpnZWDvwx9V
8+
Ofgrsjx//toTiMcSYiECQQDurSJmr+wDoBblptvFbO8KrfnkvMlTlQqeYQXmceGg
9+
Mdxcygm3nqAw/Tiqd5LUGLgQP3R/Sot8ZjSPtLme6ilTAkEAyGcSiFhx/eIYwWmp
10+
L1AJ4DSp9MQI3nVxxwrKWwyq48zxrDcSZcUJYFMphgfgzTwupTMoDNwxPiNdkUxN
11+
SdaXyQJAIUSyydt1q1+yMVqbwZ4Yh8WOUoraCTN6Im9lsiRnjbvFeo2S4yxSKeHx
12+
9xjpt3Smm2Us6N1MKg/Y/br0MKl1DwJBAIIGrnWcvUl3G4zSm51BF0dLpEJVt1Nv
13+
bEUy8RymWXK4lM2iZeN2NqEzFCwMjIVdWP6C9KdzbtfcZmdR1IvmGlECQQDDzyIT
14+
6g6z5IxBF1zQJAct34UZyLR+gjcTnT4CAjensHbpEbUvBuKT4D8S+No643rCwRQz
15+
mgvgSjp6glQuamby
16+
-----END PRIVATE KEY-----
Binary file not shown.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
-----BEGIN PRIVATE KEY-----
2+
MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAMuZd8GDiWvpMU7W
3+
LJBSeDL/bXzdbnmL35RmF+gDOQOS5zZFFz6Nn2PD6xPJxPTSzG46BLjeXdvZVsNN
4+
nx8JvLZ4XdehXGdue9C8iHTeNS5di/E9rwBYRaHK9D8NUN54xQLs7SL4v5Y//x3/
5+
1p2pRWzhMsJm4o4xi2A0pi4qQmthAgMBAAECgYBIMrvM26A3rBHYKwrSguws6Xch
6+
+EPcxkUakrmXhM0K/2UOUaHUhNQoxKjv83TsfHQSAnD6PaB6/a9Owo/SqdlJGXv3
7+
f+lvsHx283/PCD/fO/P0NE6L/9S0yYKbYd5r1PvbWBM66+AKS/1u6GevIp9UCpOJ
8+
f7P3xwTOcNSIPgP2AQJBAO9vyckXBNcmtASzOUikQk+K4qyQEqzysS1HpuLt7y/w
9+
r75BB2sM8h7cXimYIlSPTbPrcMXzoA0rB6iHrhq1sIkCQQDZrwjd/SC9y/9fi+LB
10+
MxNnai/f9h+nQzP2VWLNIakDIHXcHaqWp2GjvQW+M6XH/pMJ8g7iFbXi2YtVL2iA
11+
6z4ZAkAvXfkYXAJsIc75Iw+RDFXF8J7ZLoNTTYu5fnRIbnOkE0RhKfIyvlPjwQqr
12+
xdn8yoC/uDMOJh0incGdGIJb7FepAkB18c2XIdB8paw/Y7a/wWHRFYrNCTkLUnE0
13+
Ff2LcaJ2jD7vva8xI43WvtL+xFMdsoSOzfVccDD1sbM5u48e0tb5AkA24I2q6BE8
14+
dkW3irynMHLu2y19C6k/QeZRExix8dEpLJh0MRPwtIqeijMav+YSzHLO2h7sHhB3
15+
LDQWscGKrSRu
16+
-----END PRIVATE KEY-----
Binary file not shown.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
-----BEGIN PRIVATE KEY-----
2+
MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAK7KI7uoxRgWQ4AI
3+
dAqsGao/M1Mg0C8poMQuv4k2ORplioPLlna5fTlhBFZBiB9f86QLoonwS0iEwhs3
4+
5VdJtlvEorjud7HmQesU6SWk7bNBQTdWbJPJgM48ivIjZSjVvBj3zEDJL5u948uJ
5+
67zP1gQ5qqOrXDCV5KpXGOkRDDubAgMBAAECgYAgNQcYkSSgJ5oQgX5AaS3hfPvM
6+
GYPC7Py+qY6JjgA/qO45Es6K2esFI6dU7YZToa6XT72HhUuZ9Tx/H3GW//Il8MJh
7+
WEmiy6hB+yX3yEgq+CuUCgxnZd6BhX6H3O4dRBFxHaTEUjQJpZWrIS0vAzbdgJuM
8+
vDbNDYuYgF/ZAMwGYQJBANbxDJzrYQxIelamwiEO9uPGvOoHRopLoIaFZPneSnpp
9+
kIyLoCqikAjQuqeqVEOFXlYFfR8wVT9aq/RXqaw0A00CQQDQLZnGwhiaptBn8BL+
10+
6RjJM1Rmc1jmjiSNkp+ow573ttJhdgHnC0+CjOcwQu5Db2nzDkT+kkOLm5aCzOuZ
11+
/XaHAkEAgAUOWCAxq1k31Ih6M6pwDnZ+an1u3EvzDmxBGjn17jcV6z/2Y65zT2zS
12+
364phhXXfDDEt2DYRWXB6USVQIWyOQJAdEkEnQHOvJRx1Z1E/x81uS3y90d3YVIF
13+
GQ/OH3cmVTjKS6afaW/n+gS7HzpD3Wdex2YxJAKPuGwwpt/QuzPaAQJAGP8nj5g9
14+
oYxzD+x018fxSf/BsTjXU2S6SrbIg5D4B5s5kYFXOvLUS4rfjGWxssyHdtZuFf7T
15+
VMmSp5bTS7YAfQ==
16+
-----END PRIVATE KEY-----

0 commit comments

Comments
 (0)
Please sign in to comment.