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