1 /***************************************************************************
2 qgsauthpkipathsmethod.cpp
3 ---------------------
4 begin : September 1, 2015
5 copyright : (C) 2015 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 "qgsauthpkipathsmethod.h"
18
19 #include "qgsauthcertutils.h"
20 #include "qgsauthmanager.h"
21 #include "qgslogger.h"
22 #include "qgsapplication.h"
23 #ifdef HAVE_GUI
24 #include "qgsauthpkipathsedit.h"
25 #endif
26
27 #include <QDir>
28 #include <QFile>
29 #include <QRegularExpression>
30 #include <QUuid>
31 #ifndef QT_NO_SSL
32 #include <QtCrypto>
33 #include <QSslConfiguration>
34 #include <QSslError>
35 #endif
36 #include <QMutexLocker>
37
38 const QString QgsAuthPkiPathsMethod::AUTH_METHOD_KEY = QStringLiteral( "PKI-Paths" );
39 const QString QgsAuthPkiPathsMethod::AUTH_METHOD_DESCRIPTION = QStringLiteral( "PKI paths authentication" );
40 const QString QgsAuthPkiPathsMethod::AUTH_METHOD_DISPLAY_DESCRIPTION = tr( "PKI paths authentication" );
41
42 QMap<QString, QgsPkiConfigBundle *> QgsAuthPkiPathsMethod::sPkiConfigBundleCache = QMap<QString, QgsPkiConfigBundle *>();
43
44
QgsAuthPkiPathsMethod()45 QgsAuthPkiPathsMethod::QgsAuthPkiPathsMethod()
46 {
47 setVersion( 2 );
48 setExpansions( QgsAuthMethod::NetworkRequest | QgsAuthMethod::DataSourceUri );
49 setDataProviders( QStringList()
50 << QStringLiteral( "ows" )
51 << QStringLiteral( "wfs" ) // convert to lowercase
52 << QStringLiteral( "wcs" )
53 << QStringLiteral( "wms" )
54 << QStringLiteral( "postgres" ) );
55 }
56
~QgsAuthPkiPathsMethod()57 QgsAuthPkiPathsMethod::~QgsAuthPkiPathsMethod()
58 {
59 const QMutexLocker locker( &mMutex );
60 qDeleteAll( sPkiConfigBundleCache );
61 sPkiConfigBundleCache.clear();
62 }
63
key() const64 QString QgsAuthPkiPathsMethod::key() const
65 {
66 return AUTH_METHOD_KEY;
67 }
68
description() const69 QString QgsAuthPkiPathsMethod::description() const
70 {
71 return AUTH_METHOD_DESCRIPTION;
72 }
73
displayDescription() const74 QString QgsAuthPkiPathsMethod::displayDescription() const
75 {
76 return AUTH_METHOD_DISPLAY_DESCRIPTION;
77 }
78
79
updateNetworkRequest(QNetworkRequest & request,const QString & authcfg,const QString & dataprovider)80 bool QgsAuthPkiPathsMethod::updateNetworkRequest( QNetworkRequest &request, const QString &authcfg,
81 const QString &dataprovider )
82 {
83 Q_UNUSED( dataprovider )
84 const QMutexLocker locker( &mMutex );
85
86 // TODO: is this too restrictive, to intercept only HTTPS connections?
87 if ( request.url().scheme().toLower() != QLatin1String( "https" ) )
88 {
89 QgsDebugMsg( QStringLiteral( "Update request SSL config SKIPPED for authcfg %1: not HTTPS" ).arg( authcfg ) );
90 return true;
91 }
92
93 QgsDebugMsg( QStringLiteral( "Update request SSL config: HTTPS connection for authcfg: %1" ).arg( authcfg ) );
94
95 QgsPkiConfigBundle *pkibundle = getPkiConfigBundle( authcfg );
96 if ( !pkibundle || !pkibundle->isValid() )
97 {
98 QgsDebugMsg( QStringLiteral( "Update request SSL config FAILED for authcfg: %1: PKI bundle invalid" ).arg( authcfg ) );
99 return false;
100 }
101
102 QgsDebugMsg( QStringLiteral( "Update request SSL config: PKI bundle valid for authcfg: %1" ).arg( authcfg ) );
103
104 QSslConfiguration sslConfig = request.sslConfiguration();
105 //QSslConfiguration sslConfig( QSslConfiguration::defaultConfiguration() );
106
107 sslConfig.setPrivateKey( pkibundle->clientCertKey() );
108 sslConfig.setLocalCertificate( pkibundle->clientCert() );
109
110 // add extra CAs from the bundle
111 if ( pkibundle->config().config( QStringLiteral( "addcas" ), QStringLiteral( "false" ) ) == QStringLiteral( "true" ) )
112 {
113 if ( pkibundle->config().config( QStringLiteral( "addrootca" ), QStringLiteral( "false" ) ) == QStringLiteral( "true" ) )
114 {
115 sslConfig.setCaCertificates( pkibundle->caChain() );
116 }
117 else
118 {
119 sslConfig.setCaCertificates( QgsAuthCertUtils::casRemoveSelfSigned( pkibundle->caChain() ) );
120 }
121 }
122 request.setSslConfiguration( sslConfig );
123
124 return true;
125 }
126
127
updateDataSourceUriItems(QStringList & connectionItems,const QString & authcfg,const QString & dataprovider)128 bool QgsAuthPkiPathsMethod::updateDataSourceUriItems( QStringList &connectionItems, const QString &authcfg,
129 const QString &dataprovider )
130 {
131 Q_UNUSED( dataprovider )
132 const QMutexLocker locker( &mMutex );
133
134 QgsDebugMsg( QStringLiteral( "Update URI items for authcfg: %1" ).arg( authcfg ) );
135
136 QgsPkiConfigBundle *pkibundle = getPkiConfigBundle( authcfg );
137 if ( !pkibundle || !pkibundle->isValid() )
138 {
139 QgsDebugMsg( QStringLiteral( "Update URI items FAILED: PKI bundle invalid" ) );
140 return false;
141 }
142 QgsDebugMsg( QStringLiteral( "Update URI items: PKI bundle valid" ) );
143
144 const QString pkiTempFileBase = QStringLiteral( "tmppki_%1.pem" );
145
146 // save client cert to temp file
147 const QString certFilePath = QgsAuthCertUtils::pemTextToTempFile(
148 pkiTempFileBase.arg( QUuid::createUuid().toString() ),
149 pkibundle->clientCert().toPem() );
150 if ( certFilePath.isEmpty() )
151 {
152 return false;
153 }
154
155 // save client cert key to temp file
156 const QString keyFilePath = QgsAuthCertUtils::pemTextToTempFile(
157 pkiTempFileBase.arg( QUuid::createUuid().toString() ),
158 pkibundle->clientCertKey().toPem() );
159 if ( keyFilePath.isEmpty() )
160 {
161 return false;
162 }
163
164 // add extra CAs from the bundle
165 QList<QSslCertificate> cas;
166 if ( pkibundle->config().config( QStringLiteral( "addcas" ), QStringLiteral( "false" ) ) == QStringLiteral( "true" ) )
167 {
168 if ( pkibundle->config().config( QStringLiteral( "addrootca" ), QStringLiteral( "false" ) ) == QStringLiteral( "true" ) )
169 {
170 cas = QgsAuthCertUtils::casMerge( QgsApplication::authManager()->trustedCaCerts(), pkibundle->caChain() );
171 }
172 else
173 {
174 cas = QgsAuthCertUtils::casMerge( QgsApplication::authManager()->trustedCaCerts(),
175 QgsAuthCertUtils::casRemoveSelfSigned( pkibundle->caChain() ) );
176 }
177 }
178 else
179 {
180 cas = QgsApplication::authManager()->trustedCaCerts();
181 }
182
183 // save CAs to temp file
184 const QString caFilePath = QgsAuthCertUtils::pemTextToTempFile(
185 pkiTempFileBase.arg( QUuid::createUuid().toString() ),
186 QgsAuthCertUtils::certsToPemText( cas ) );
187 if ( caFilePath.isEmpty() )
188 {
189 return false;
190 }
191
192 // get common name of the client certificate
193 const QString commonName = QgsAuthCertUtils::resolvedCertName( pkibundle->clientCert(), false );
194
195 // add uri parameters
196 const QString userparam = "user='" + commonName + "'";
197 const thread_local QRegularExpression userRegExp( "^user='.*" );
198 const int userindx = connectionItems.indexOf( userRegExp );
199 if ( userindx != -1 )
200 {
201 connectionItems.replace( userindx, userparam );
202 }
203 else
204 {
205 connectionItems.append( userparam );
206 }
207
208 // add uri parameters
209 const QString certparam = "sslcert='" + certFilePath + "'";
210 const thread_local QRegularExpression sslcertRegExp( "^sslcert='.*" );
211 const int sslcertindx = connectionItems.indexOf( sslcertRegExp );
212 if ( sslcertindx != -1 )
213 {
214 connectionItems.replace( sslcertindx, certparam );
215 }
216 else
217 {
218 connectionItems.append( certparam );
219 }
220
221 const QString keyparam = "sslkey='" + keyFilePath + "'";
222 const thread_local QRegularExpression sslkeyRegExp( "^sslkey='.*" );
223 const int sslkeyindx = connectionItems.indexOf( sslkeyRegExp );
224 if ( sslkeyindx != -1 )
225 {
226 connectionItems.replace( sslkeyindx, keyparam );
227 }
228 else
229 {
230 connectionItems.append( keyparam );
231 }
232
233 const QString caparam = "sslrootcert='" + caFilePath + "'";
234 const thread_local QRegularExpression sslcaRegExp( "^sslrootcert='.*" );
235 const int sslcaindx = connectionItems.indexOf( sslcaRegExp );
236 if ( sslcaindx != -1 )
237 {
238 connectionItems.replace( sslcaindx, caparam );
239 }
240 else
241 {
242 connectionItems.append( caparam );
243 }
244
245 return true;
246 }
247
clearCachedConfig(const QString & authcfg)248 void QgsAuthPkiPathsMethod::clearCachedConfig( const QString &authcfg )
249 {
250 const QMutexLocker locker( &mMutex );
251 removePkiConfigBundle( authcfg );
252 }
253
updateMethodConfig(QgsAuthMethodConfig & mconfig)254 void QgsAuthPkiPathsMethod::updateMethodConfig( QgsAuthMethodConfig &mconfig )
255 {
256 const QMutexLocker locker( &mMutex );
257 if ( mconfig.hasConfig( QStringLiteral( "oldconfigstyle" ) ) )
258 {
259 QgsDebugMsg( QStringLiteral( "Updating old style auth method config" ) );
260
261 const QStringList conflist = mconfig.config( QStringLiteral( "oldconfigstyle" ) ).split( QStringLiteral( "|||" ) );
262 mconfig.setConfig( QStringLiteral( "certpath" ), conflist.at( 0 ) );
263 mconfig.setConfig( QStringLiteral( "keypath" ), conflist.at( 1 ) );
264 mconfig.setConfig( QStringLiteral( "keypass" ), conflist.at( 2 ) );
265 mconfig.removeConfig( QStringLiteral( "oldconfigstyle" ) );
266 }
267
268 // TODO: add updates as method version() increases due to config storage changes
269 }
270
getPkiConfigBundle(const QString & authcfg)271 QgsPkiConfigBundle *QgsAuthPkiPathsMethod::getPkiConfigBundle( const QString &authcfg )
272 {
273 const QMutexLocker locker( &mMutex );
274 QgsPkiConfigBundle *bundle = nullptr;
275
276 // check if it is cached
277 if ( sPkiConfigBundleCache.contains( authcfg ) )
278 {
279 bundle = sPkiConfigBundleCache.value( authcfg );
280 if ( bundle )
281 {
282 QgsDebugMsg( QStringLiteral( "Retrieved PKI bundle for authcfg %1" ).arg( authcfg ) );
283 return bundle;
284 }
285 }
286
287 // else build PKI bundle
288 QgsAuthMethodConfig mconfig;
289
290 if ( !QgsApplication::authManager()->loadAuthenticationConfig( authcfg, mconfig, true ) )
291 {
292 QgsDebugMsg( QStringLiteral( "PKI bundle for authcfg %1: FAILED to retrieve config" ).arg( authcfg ) );
293 return bundle;
294 }
295
296 // init client cert
297 // Note: if this is not valid, no sense continuing
298 const QSslCertificate clientcert( QgsAuthCertUtils::certFromFile( mconfig.config( QStringLiteral( "certpath" ) ) ) );
299 if ( !QgsAuthCertUtils::certIsViable( clientcert ) )
300 {
301 QgsDebugMsg( QStringLiteral( "PKI bundle for authcfg %1: insert FAILED, client cert is not viable" ).arg( authcfg ) );
302 return bundle;
303 }
304
305 // init key
306 const QSslKey clientkey = QgsAuthCertUtils::keyFromFile( mconfig.config( QStringLiteral( "keypath" ) ), mconfig.config( QStringLiteral( "keypass" ) ) );
307
308 if ( clientkey.isNull() )
309 {
310 QgsDebugMsg( QStringLiteral( "PKI bundle for authcfg %1: insert FAILED, cert key is null" ).arg( authcfg ) );
311 return bundle;
312 }
313
314 bundle = new QgsPkiConfigBundle( mconfig, clientcert, clientkey, QgsAuthCertUtils::casFromFile( mconfig.config( QStringLiteral( "certpath" ) ) ) );
315
316 // cache bundle
317 putPkiConfigBundle( authcfg, bundle );
318
319 return bundle;
320 }
321
putPkiConfigBundle(const QString & authcfg,QgsPkiConfigBundle * pkibundle)322 void QgsAuthPkiPathsMethod::putPkiConfigBundle( const QString &authcfg, QgsPkiConfigBundle *pkibundle )
323 {
324 const QMutexLocker locker( &mMutex );
325 QgsDebugMsg( QStringLiteral( "Putting PKI bundle for authcfg %1" ).arg( authcfg ) );
326 sPkiConfigBundleCache.insert( authcfg, pkibundle );
327 }
328
removePkiConfigBundle(const QString & authcfg)329 void QgsAuthPkiPathsMethod::removePkiConfigBundle( const QString &authcfg )
330 {
331 const QMutexLocker locker( &mMutex );
332 if ( sPkiConfigBundleCache.contains( authcfg ) )
333 {
334 QgsPkiConfigBundle *pkibundle = sPkiConfigBundleCache.take( authcfg );
335 delete pkibundle;
336 pkibundle = nullptr;
337 QgsDebugMsg( QStringLiteral( "Removed PKI bundle for authcfg: %1" ).arg( authcfg ) );
338 }
339 }
340
341 #ifdef HAVE_GUI
editWidget(QWidget * parent) const342 QWidget *QgsAuthPkiPathsMethod::editWidget( QWidget *parent ) const
343 {
344 return new QgsAuthPkiPathsEdit( parent );
345 }
346 #endif
347
348 //////////////////////////////////////////////
349 // Plugin externals
350 //////////////////////////////////////////////
351
352
353 #ifndef HAVE_STATIC_PROVIDERS
authMethodMetadataFactory()354 QGISEXTERN QgsAuthMethodMetadata *authMethodMetadataFactory()
355 {
356 return new QgsAuthPkiPathsMethodMetadata();
357 }
358 #endif
359