QGIS API Documentation 3.28.0-Firenze (ed3ad0430f)
qgsauthimportidentitydialog.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsauthimportidentitydialog.cpp
3 ---------------------
4 begin : May 9, 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
18#include "ui_qgsauthimportidentitydialog.h"
19
20#include <QFile>
21#include <QFileDialog>
22#include <QPushButton>
23
24#include "qgssettings.h"
25#include "qgsauthcertutils.h"
26#include "qgsauthconfig.h"
27#include "qgsauthguiutils.h"
28#include "qgsauthmanager.h"
29#include "qgslogger.h"
30#include "qgsapplication.h"
31
32
34 QWidget *parent )
35 : QDialog( parent )
36 , mIdentityType( CertIdentity )
37 , mPkiBundle( QgsPkiBundle() )
38 , mDisabled( false )
39
40{
41 if ( QgsApplication::authManager()->isDisabled() )
42 {
43 mDisabled = true;
44 mAuthNotifyLayout = new QVBoxLayout;
45 this->setLayout( mAuthNotifyLayout );
46 mAuthNotify = new QLabel( QgsApplication::authManager()->disabledMessage(), this );
47 mAuthNotifyLayout->addWidget( mAuthNotify );
48 }
49 else
50 {
51 setupUi( this );
52 connect( lePkiPathsKeyPass, &QLineEdit::textChanged, this, &QgsAuthImportIdentityDialog::lePkiPathsKeyPass_textChanged );
53 connect( chkPkiPathsPassShow, &QCheckBox::stateChanged, this, &QgsAuthImportIdentityDialog::chkPkiPathsPassShow_stateChanged );
54 connect( btnPkiPathsCert, &QToolButton::clicked, this, &QgsAuthImportIdentityDialog::btnPkiPathsCert_clicked );
55 connect( btnPkiPathsKey, &QToolButton::clicked, this, &QgsAuthImportIdentityDialog::btnPkiPathsKey_clicked );
56 connect( lePkiPkcs12KeyPass, &QLineEdit::textChanged, this, &QgsAuthImportIdentityDialog::lePkiPkcs12KeyPass_textChanged );
57 connect( chkPkiPkcs12PassShow, &QCheckBox::stateChanged, this, &QgsAuthImportIdentityDialog::chkPkiPkcs12PassShow_stateChanged );
58 connect( btnPkiPkcs12Bundle, &QToolButton::clicked, this, &QgsAuthImportIdentityDialog::btnPkiPkcs12Bundle_clicked );
59 connect( buttonBox, &QDialogButtonBox::rejected, this, &QWidget::close );
60 connect( buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept );
61
62 mIdentityType = identitytype;
63
64 populateIdentityType();
65 }
66}
67
69{
70 if ( mDisabled )
71 {
73 }
74 return mIdentityType;
75}
76
77const QPair<QSslCertificate, QSslKey> QgsAuthImportIdentityDialog::certBundleToImport()
78{
79 if ( mDisabled )
80 {
81 return qMakePair( QSslCertificate(), QSslKey() );
82 }
83 return mCertBundle;
84}
85
86void QgsAuthImportIdentityDialog::populateIdentityType()
87{
88 if ( mIdentityType == CertIdentity )
89 {
90 stkwBundleType->setVisible( true );
91
92 cmbIdentityTypes->addItem( tr( "PKI PEM/DER Certificate Paths" ),
94 cmbIdentityTypes->addItem( tr( "PKI PKCS#12 Certificate Bundle" ),
96
97 connect( cmbIdentityTypes, static_cast<void ( QComboBox::* )( int )>( &QComboBox::currentIndexChanged ),
98 stkwBundleType, &QStackedWidget::setCurrentIndex );
99 connect( stkwBundleType, &QStackedWidget::currentChanged,
100 cmbIdentityTypes, &QComboBox::setCurrentIndex );
101
102 connect( cmbIdentityTypes, static_cast<void ( QComboBox::* )( int )>( &QComboBox::currentIndexChanged ),
103 this, [ = ] { validateIdentity(); } );
104 connect( stkwBundleType, &QStackedWidget::currentChanged,
105 this, &QgsAuthImportIdentityDialog::validateIdentity );
106
107 cmbIdentityTypes->setCurrentIndex( 0 );
108 stkwBundleType->setCurrentIndex( 0 );
109 }
110 // else switch stacked widget, and populate/connect according to that type and widget
111}
112
113void QgsAuthImportIdentityDialog::validateIdentity()
114{
115 bool ok = false;
116 if ( mIdentityType == CertIdentity )
117 {
118 ok = validateBundle();
119 }
120 okButton()->setEnabled( ok );
121}
122
123bool QgsAuthImportIdentityDialog::validateBundle()
124{
125
126 // clear out any previously set bundle
127 const QSslCertificate emptycert;
128 const QSslKey emptykey;
129 mCertBundle = qMakePair( emptycert, emptykey );
130 mPkiBundle = QgsPkiBundle();
131
132 QWidget *curpage = stkwBundleType->currentWidget();
133 if ( curpage == pagePkiPaths )
134 {
135 return validatePkiPaths();
136 }
137 else if ( curpage == pagePkiPkcs12 )
138 {
139 return validatePkiPkcs12();
140 }
141
142 return false;
143}
144
145void QgsAuthImportIdentityDialog::clearValidation()
146{
147 teValidation->clear();
148 teValidation->setStyleSheet( QString() );
149}
150
151void QgsAuthImportIdentityDialog::writeValidation( const QString &msg,
153 bool append )
154{
155 QString ss;
156 QString txt( msg );
157 switch ( valid )
158 {
159 case Valid:
160 ss = QgsAuthGuiUtils::greenTextStyleSheet( QStringLiteral( "QTextEdit" ) );
161 txt = tr( "Valid: %1" ).arg( msg );
162 break;
163 case Invalid:
164 ss = QgsAuthGuiUtils::redTextStyleSheet( QStringLiteral( "QTextEdit" ) );
165 txt = tr( "Invalid: %1" ).arg( msg );
166 break;
167 case Unknown:
168 break;
169 }
170 teValidation->setStyleSheet( ss );
171 if ( append )
172 {
173 teValidation->append( txt );
174 }
175 else
176 {
177 teValidation->setText( txt );
178 }
179 teValidation->moveCursor( QTextCursor::Start );
180}
181
182void QgsAuthImportIdentityDialog::lePkiPathsKeyPass_textChanged( const QString &pass )
183{
184 Q_UNUSED( pass )
185 validateIdentity();
186}
187
188void QgsAuthImportIdentityDialog::chkPkiPathsPassShow_stateChanged( int state )
189{
190 lePkiPathsKeyPass->setEchoMode( ( state > 0 ) ? QLineEdit::Normal : QLineEdit::Password );
191}
192
193void QgsAuthImportIdentityDialog::btnPkiPathsCert_clicked()
194{
195 const QString &fn = getOpenFileName( tr( "Open Client Certificate File" ), tr( "All files (*.*);;PEM (*.pem);;DER (*.der)" ) );
196 if ( !fn.isEmpty() )
197 {
198 lePkiPathsCert->setText( fn );
199 validateIdentity();
200 }
201}
202
203void QgsAuthImportIdentityDialog::btnPkiPathsKey_clicked()
204{
205 const QString &fn = getOpenFileName( tr( "Open Private Key File" ), tr( "All files (*.*);;PEM (*.pem);;DER (*.der)" ) );
206 if ( !fn.isEmpty() )
207 {
208 lePkiPathsKey->setText( fn );
209 validateIdentity();
210 }
211}
212
213void QgsAuthImportIdentityDialog::lePkiPkcs12KeyPass_textChanged( const QString &pass )
214{
215 Q_UNUSED( pass )
216 validateIdentity();
217}
218
219void QgsAuthImportIdentityDialog::chkPkiPkcs12PassShow_stateChanged( int state )
220{
221 lePkiPkcs12KeyPass->setEchoMode( ( state > 0 ) ? QLineEdit::Normal : QLineEdit::Password );
222}
223
224void QgsAuthImportIdentityDialog::btnPkiPkcs12Bundle_clicked()
225{
226 const QString &fn = getOpenFileName( tr( "Open PKCS#12 Certificate Bundle" ), tr( "PKCS#12 (*.p12 *.pfx)" ) );
227 if ( !fn.isEmpty() )
228 {
229 lePkiPkcs12Bundle->setText( fn );
230 validateIdentity();
231 }
232}
233
234bool QgsAuthImportIdentityDialog::validatePkiPaths()
235{
236 bool isvalid = false;
237
238 // required components
239 const QString certpath( lePkiPathsCert->text() );
240 const QString keypath( lePkiPathsKey->text() );
241
242 const bool certfound = QFile::exists( certpath );
243 const bool keyfound = QFile::exists( keypath );
244
245 fileFound( certpath.isEmpty() || certfound, lePkiPathsCert );
246 fileFound( keypath.isEmpty() || keyfound, lePkiPathsKey );
247
248 if ( !certfound || !keyfound )
249 {
250 writeValidation( tr( "Missing components" ), Invalid );
251 return false;
252 }
253
254 // check for issue date validity
255 QSslCertificate clientcert;
256 QList<QSslCertificate> certs( QgsAuthCertUtils::certsFromFile( certpath ) );
257 QList<QSslCertificate> ca_certs;
258 if ( !certs.isEmpty() )
259 {
260 clientcert = certs.takeFirst();
261 }
262 else
263 {
264 writeValidation( tr( "Failed to read client certificate from file" ), Invalid );
265 return false;
266 }
267
268 if ( clientcert.isNull() )
269 {
270 writeValidation( tr( "Failed to load client certificate from file" ), Invalid );
271 return false;
272 }
273
274 if ( !certs.isEmpty() ) // Multiple certificates in file
275 {
276 teValidation->append( tr( "Extra certificates found with identity" ) );
277 ca_certs = certs;
278 }
279
280 isvalid = QgsAuthCertUtils::certIsViable( clientcert );
281
282 const QDateTime startdate( clientcert.effectiveDate() );
283 const QDateTime enddate( clientcert.expiryDate() );
284
285 writeValidation( tr( "%1 thru %2" ).arg( startdate.toString(), enddate.toString() ),
286 ( QgsAuthCertUtils::certIsCurrent( clientcert ) ? Valid : Invalid ) );
287 //TODO: set enabled on cert info button, relative to cert validity
288
289 // check for valid private key and that any supplied password works
290 const QString keypass( lePkiPathsKeyPass->text() );
291 const QSslKey clientkey( QgsAuthCertUtils::keyFromFile( keypath, keypass ) );
292 if ( clientkey.isNull() )
293 {
294 writeValidation( tr( "Failed to load client private key from file" ), Invalid, true );
295 if ( !keypass.isEmpty() )
296 {
297 writeValidation( tr( "Private key password may not match" ), Invalid, true );
298 }
299 return false;
300 }
301
302 if ( isvalid )
303 {
304 mCertBundle = qMakePair( clientcert, clientkey );
305 mPkiBundle = QgsPkiBundle( clientcert,
306 clientkey,
307 ca_certs );
308 }
309
310 return isvalid;
311}
312
313bool QgsAuthImportIdentityDialog::validatePkiPkcs12()
314{
315 // required components
316 const QString bundlepath( lePkiPkcs12Bundle->text() );
317 const bool bundlefound = QFile::exists( bundlepath );
318 fileFound( bundlepath.isEmpty() || bundlefound, lePkiPkcs12Bundle );
319
320 if ( !bundlefound )
321 {
322 writeValidation( tr( "Missing components" ), Invalid );
323 return false;
324 }
325
326 if ( !QCA::isSupported( "pkcs12" ) )
327 {
328 writeValidation( tr( "QCA library has no PKCS#12 support" ), Invalid );
329 return false;
330 }
331
332 // load the bundle
333 QCA::SecureArray passarray;
334 QString keypass = QString();
335 if ( !lePkiPkcs12KeyPass->text().isEmpty() )
336 {
337 passarray = QCA::SecureArray( lePkiPkcs12KeyPass->text().toUtf8() );
338 keypass = lePkiPkcs12KeyPass->text();
339 }
340
341 QCA::ConvertResult res;
342 const QCA::KeyBundle bundle( QCA::KeyBundle::fromFile( bundlepath, passarray, &res, QStringLiteral( "qca-ossl" ) ) );
343
344 if ( res == QCA::ErrorFile )
345 {
346 writeValidation( tr( "Failed to read bundle file" ), Invalid );
347 return false;
348 }
349 else if ( res == QCA::ErrorPassphrase )
350 {
351 writeValidation( tr( "Incorrect bundle password" ), Invalid );
352 lePkiPkcs12KeyPass->setPlaceholderText( QStringLiteral( "Required passphrase" ) );
353 return false;
354 }
355 else if ( res == QCA::ErrorDecode )
356 {
357 writeValidation( tr( "Failed to decode (try entering password)" ), Invalid );
358 return false;
359 }
360
361 if ( bundle.isNull() )
362 {
363 writeValidation( tr( "Bundle empty or can not be loaded" ), Invalid );
364 return false;
365 }
366
367 // check for primary cert and that it is valid
368 const QCA::Certificate cert( bundle.certificateChain().primary() );
369 if ( cert.isNull() )
370 {
371 writeValidation( tr( "Bundle client cert can not be loaded" ), Invalid );
372 return false;
373 }
374
375 // TODO: add more robust validation, including cert chain resolution
376 const QDateTime startdate( cert.notValidBefore() );
377 const QDateTime enddate( cert.notValidAfter() );
378 const QDateTime now( QDateTime::currentDateTime() );
379 const bool bundlevalid = ( now >= startdate && now <= enddate );
380
381 writeValidation( tr( "%1 thru %2" ).arg( startdate.toString(), enddate.toString() ),
382 ( bundlevalid ? Valid : Invalid ) );
383
384 if ( bundlevalid )
385 {
386 QSslCertificate clientcert;
387 QList<QSslCertificate> certs( QgsAuthCertUtils::certsFromString( cert.toPEM() ) );
388 if ( !certs.isEmpty() )
389 {
390 clientcert = certs.first();
391 }
392 if ( clientcert.isNull() )
393 {
394 writeValidation( tr( "Qt cert could not be created from QCA cert" ), Invalid, true );
395 return false;
396 }
397 QSslKey clientkey;
398 clientkey = QSslKey( bundle.privateKey().toRSA().toPEM().toLatin1(), QSsl::Rsa );
399 if ( clientkey.isNull() )
400 {
401 writeValidation( tr( "Qt private key could not be created from QCA key" ), Invalid, true );
402 return false;
403 }
404
405 const QCA::CertificateChain cert_chain( bundle.certificateChain() );
406 QList<QSslCertificate> ca_certs;
407 if ( cert_chain.size() > 1 )
408 {
409 const auto constCert_chain = cert_chain;
410 for ( const QCA::Certificate &ca_cert : constCert_chain )
411 {
412 if ( ca_cert != cert_chain.primary() )
413 {
414 ca_certs << QSslCertificate( ca_cert.toPEM().toLatin1() );
415 }
416 }
417 }
418
419 mCertBundle = qMakePair( clientcert, clientkey );
420 mPkiBundle = QgsPkiBundle( clientcert, clientkey, ca_certs );
421 }
422
423 return bundlevalid;
424}
425
426void QgsAuthImportIdentityDialog::fileFound( bool found, QWidget *widget )
427{
428 if ( !found )
429 {
430 widget->setStyleSheet( QgsAuthGuiUtils::redTextStyleSheet( QStringLiteral( "QLineEdit" ) ) );
431 widget->setToolTip( tr( "File not found" ) );
432 }
433 else
434 {
435 widget->setStyleSheet( QString() );
436 widget->setToolTip( QString() );
437 }
438}
439
440QString QgsAuthImportIdentityDialog::getOpenFileName( const QString &title, const QString &extfilter )
441{
442 QgsSettings settings;
443 const QString recentdir = settings.value( QStringLiteral( "UI/lastAuthImportBundleOpenFileDir" ), QDir::homePath() ).toString();
444 QString f = QFileDialog::getOpenFileName( this, title, recentdir, extfilter );
445
446 // return dialog focus on Mac
447 this->raise();
448 this->activateWindow();
449
450 if ( !f.isEmpty() )
451 {
452 settings.setValue( QStringLiteral( "UI/lastAuthImportBundleOpenFileDir" ), QFileInfo( f ).absoluteDir().path() );
453 }
454 return f;
455}
456
457QPushButton *QgsAuthImportIdentityDialog::okButton()
458{
459 return buttonBox->button( QDialogButtonBox::Ok );
460}
static QgsAuthManager * authManager()
Returns the application's authentication manager instance.
static bool certIsViable(const QSslCertificate &cert)
certIsViable checks for viability errors of cert and whether it is NULL
static QList< QSslCertificate > certsFromFile(const QString &certspath)
Returns a list of concatenated certs from a PEM or DER formatted file.
static QSslKey keyFromFile(const QString &keypath, const QString &keypass=QString(), QString *algtype=nullptr)
Returns non-encrypted key from a PEM or DER formatted file.
static QList< QSslCertificate > certsFromString(const QString &pemtext)
Returns a list of concatenated certs from a PEM Base64 text block.
static bool certIsCurrent(const QSslCertificate &cert)
certIsCurrent checks if cert is viable for its not before and not after dates
static QString greenTextStyleSheet(const QString &selector="*")
Green text stylesheet representing valid, trusted, etc. certificate.
static QString redTextStyleSheet(const QString &selector="*")
Red text stylesheet representing invalid, untrusted, etc. certificate.
QgsAuthImportIdentityDialog::IdentityType identityType()
Gets identity type.
const QPair< QSslCertificate, QSslKey > certBundleToImport()
Gets certificate/key bundle to be imported.
Validity
Type of certificate/bundle validity output.
IdentityType
Type of identity being imported.
QgsAuthImportIdentityDialog(QgsAuthImportIdentityDialog::IdentityType identitytype, QWidget *parent=nullptr)
Construct a dialog for importing identities.
Storage set for PKI bundle: SSL certificate, key, optional CA cert chain.
This class is a composition of two QSettings instances:
Definition: qgssettings.h:62
QVariant value(const QString &key, const QVariant &defaultValue=QVariant(), Section section=NoSection) const
Returns the value for setting key.
void setValue(const QString &key, const QVariant &value, QgsSettings::Section section=QgsSettings::NoSection)
Sets the value of setting key to value.