QGIS API Documentation 3.41.0-Master (af5edcb665c)
Loading...
Searching...
No Matches
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 "moc_qgsauthimportidentitydialog.cpp"
19#include "ui_qgsauthimportidentitydialog.h"
20
21#include <QFile>
22#include <QFileDialog>
23#include <QPushButton>
24
25#include "qgssettings.h"
26#include "qgsauthcertutils.h"
27#include "qgsauthconfig.h"
28#include "qgsauthguiutils.h"
29#include "qgsauthmanager.h"
30#include "qgslogger.h"
31#include "qgsapplication.h"
32
33
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
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" ), QVariant( QgsAuthImportIdentityDialog::PkiPaths ) );
93 cmbIdentityTypes->addItem( tr( "PKI PKCS#12 Certificate Bundle" ), QVariant( QgsAuthImportIdentityDialog::PkiPkcs12 ) );
94
95 connect( cmbIdentityTypes, static_cast<void ( QComboBox::* )( int )>( &QComboBox::currentIndexChanged ), stkwBundleType, &QStackedWidget::setCurrentIndex );
96 connect( stkwBundleType, &QStackedWidget::currentChanged, cmbIdentityTypes, &QComboBox::setCurrentIndex );
97
98 connect( cmbIdentityTypes, static_cast<void ( QComboBox::* )( int )>( &QComboBox::currentIndexChanged ), this, [=] { validateIdentity(); } );
99 connect( stkwBundleType, &QStackedWidget::currentChanged, this, &QgsAuthImportIdentityDialog::validateIdentity );
100
101 cmbIdentityTypes->setCurrentIndex( 0 );
102 stkwBundleType->setCurrentIndex( 0 );
103 }
104 // else switch stacked widget, and populate/connect according to that type and widget
105}
106
107void QgsAuthImportIdentityDialog::validateIdentity()
108{
109 bool ok = false;
110 if ( mIdentityType == CertIdentity )
111 {
112 ok = validateBundle();
113 }
114 okButton()->setEnabled( ok );
115}
116
117bool QgsAuthImportIdentityDialog::validateBundle()
118{
119 // clear out any previously set bundle
120 const QSslCertificate emptycert;
121 const QSslKey emptykey;
122 mCertBundle = qMakePair( emptycert, emptykey );
123 mPkiBundle = QgsPkiBundle();
124
125 QWidget *curpage = stkwBundleType->currentWidget();
126 if ( curpage == pagePkiPaths )
127 {
128 return validatePkiPaths();
129 }
130 else if ( curpage == pagePkiPkcs12 )
131 {
132 return validatePkiPkcs12();
133 }
134
135 return false;
136}
137
138void QgsAuthImportIdentityDialog::clearValidation()
139{
140 teValidation->clear();
141 teValidation->setStyleSheet( QString() );
142}
143
144void QgsAuthImportIdentityDialog::writeValidation( const QString &msg, QgsAuthImportIdentityDialog::Validity valid, bool append )
145{
146 QString ss;
147 QString txt( msg );
148 switch ( valid )
149 {
150 case Valid:
151 ss = QgsAuthGuiUtils::greenTextStyleSheet( QStringLiteral( "QTextEdit" ) );
152 txt = tr( "Valid: %1" ).arg( msg );
153 break;
154 case Invalid:
155 ss = QgsAuthGuiUtils::redTextStyleSheet( QStringLiteral( "QTextEdit" ) );
156 txt = tr( "Invalid: %1" ).arg( msg );
157 break;
158 case Unknown:
159 break;
160 }
161 teValidation->setStyleSheet( ss );
162 if ( append )
163 {
164 teValidation->append( txt );
165 }
166 else
167 {
168 teValidation->setText( txt );
169 }
170 teValidation->moveCursor( QTextCursor::Start );
171}
172
173void QgsAuthImportIdentityDialog::lePkiPathsKeyPass_textChanged( const QString &pass )
174{
175 Q_UNUSED( pass )
176 validateIdentity();
177}
178
179void QgsAuthImportIdentityDialog::chkPkiPathsPassShow_stateChanged( int state )
180{
181 lePkiPathsKeyPass->setEchoMode( ( state > 0 ) ? QLineEdit::Normal : QLineEdit::Password );
182}
183
184void QgsAuthImportIdentityDialog::btnPkiPathsCert_clicked()
185{
186 const QString &fn = getOpenFileName( tr( "Open Client Certificate File" ), tr( "All files (*.*);;PEM (*.pem);;DER (*.der)" ) );
187 if ( !fn.isEmpty() )
188 {
189 lePkiPathsCert->setText( fn );
190 validateIdentity();
191 }
192}
193
194void QgsAuthImportIdentityDialog::btnPkiPathsKey_clicked()
195{
196 const QString &fn = getOpenFileName( tr( "Open Private Key File" ), tr( "All files (*.*);;PEM (*.pem);;DER (*.der)" ) );
197 if ( !fn.isEmpty() )
198 {
199 lePkiPathsKey->setText( fn );
200 validateIdentity();
201 }
202}
203
204void QgsAuthImportIdentityDialog::lePkiPkcs12KeyPass_textChanged( const QString &pass )
205{
206 Q_UNUSED( pass )
207 validateIdentity();
208}
209
210void QgsAuthImportIdentityDialog::chkPkiPkcs12PassShow_stateChanged( int state )
211{
212 lePkiPkcs12KeyPass->setEchoMode( ( state > 0 ) ? QLineEdit::Normal : QLineEdit::Password );
213}
214
215void QgsAuthImportIdentityDialog::btnPkiPkcs12Bundle_clicked()
216{
217 const QString &fn = getOpenFileName( tr( "Open PKCS#12 Certificate Bundle" ), tr( "PKCS#12 (*.p12 *.pfx)" ) );
218 if ( !fn.isEmpty() )
219 {
220 lePkiPkcs12Bundle->setText( fn );
221 validateIdentity();
222 }
223}
224
225bool QgsAuthImportIdentityDialog::validatePkiPaths()
226{
227 bool isvalid = false;
228
229 // required components
230 const QString certpath( lePkiPathsCert->text() );
231 const QString keypath( lePkiPathsKey->text() );
232
233 const bool certfound = QFile::exists( certpath );
234 const bool keyfound = QFile::exists( keypath );
235
236 fileFound( certpath.isEmpty() || certfound, lePkiPathsCert );
237 fileFound( keypath.isEmpty() || keyfound, lePkiPathsKey );
238
239 if ( !certfound || !keyfound )
240 {
241 writeValidation( tr( "Missing components" ), Invalid );
242 return false;
243 }
244
245 // check for issue date validity
246 QSslCertificate clientcert;
247 QList<QSslCertificate> certs( QgsAuthCertUtils::certsFromFile( certpath ) );
248 QList<QSslCertificate> ca_certs;
249 if ( !certs.isEmpty() )
250 {
251 clientcert = certs.takeFirst();
252 }
253 else
254 {
255 writeValidation( tr( "Failed to read client certificate from file" ), Invalid );
256 return false;
257 }
258
259 if ( clientcert.isNull() )
260 {
261 writeValidation( tr( "Failed to load client certificate from file" ), Invalid );
262 return false;
263 }
264
265 if ( !certs.isEmpty() ) // Multiple certificates in file
266 {
267 teValidation->append( tr( "Extra certificates found with identity" ) );
268 ca_certs = certs;
269 }
270
271 isvalid = QgsAuthCertUtils::certIsViable( clientcert );
272
273 const QDateTime startdate( clientcert.effectiveDate() );
274 const QDateTime enddate( clientcert.expiryDate() );
275
276 writeValidation( tr( "%1 thru %2" ).arg( startdate.toString(), enddate.toString() ), ( QgsAuthCertUtils::certIsCurrent( clientcert ) ? Valid : Invalid ) );
277 //TODO: set enabled on cert info button, relative to cert validity
278
279 // check for valid private key and that any supplied password works
280 const QString keypass( lePkiPathsKeyPass->text() );
281 const QSslKey clientkey( QgsAuthCertUtils::keyFromFile( keypath, keypass ) );
282 if ( clientkey.isNull() )
283 {
284 writeValidation( tr( "Failed to load client private key from file" ), Invalid, true );
285 if ( !keypass.isEmpty() )
286 {
287 writeValidation( tr( "Private key password may not match" ), Invalid, true );
288 }
289 return false;
290 }
291
292 if ( isvalid )
293 {
294 mCertBundle = qMakePair( clientcert, clientkey );
295 mPkiBundle = QgsPkiBundle( clientcert, clientkey, ca_certs );
296 }
297
298 return isvalid;
299}
300
301bool QgsAuthImportIdentityDialog::validatePkiPkcs12()
302{
303 // required components
304 const QString bundlepath( lePkiPkcs12Bundle->text() );
305 const bool bundlefound = QFile::exists( bundlepath );
306 fileFound( bundlepath.isEmpty() || bundlefound, lePkiPkcs12Bundle );
307
308 if ( !bundlefound )
309 {
310 writeValidation( tr( "Missing components" ), Invalid );
311 return false;
312 }
313
314 if ( !QCA::isSupported( "pkcs12" ) )
315 {
316 writeValidation( tr( "QCA library has no PKCS#12 support" ), Invalid );
317 return false;
318 }
319
320 // load the bundle
321 QCA::SecureArray passarray;
322 QString keypass = QString();
323 if ( !lePkiPkcs12KeyPass->text().isEmpty() )
324 {
325 passarray = QCA::SecureArray( lePkiPkcs12KeyPass->text().toUtf8() );
326 keypass = lePkiPkcs12KeyPass->text();
327 }
328
329 QCA::ConvertResult res;
330 const QCA::KeyBundle bundle( QCA::KeyBundle::fromFile( bundlepath, passarray, &res, QStringLiteral( "qca-ossl" ) ) );
331
332 if ( res == QCA::ErrorFile )
333 {
334 writeValidation( tr( "Failed to read bundle file" ), Invalid );
335 return false;
336 }
337 else if ( res == QCA::ErrorPassphrase )
338 {
339 writeValidation( tr( "Incorrect bundle password" ), Invalid );
340 lePkiPkcs12KeyPass->setPlaceholderText( QStringLiteral( "Required passphrase" ) );
341 return false;
342 }
343 else if ( res == QCA::ErrorDecode )
344 {
345 writeValidation( tr( "Failed to decode (try entering password)" ), Invalid );
346 return false;
347 }
348
349 if ( bundle.isNull() )
350 {
351 writeValidation( tr( "Bundle empty or can not be loaded" ), Invalid );
352 return false;
353 }
354
355 // check for primary cert and that it is valid
356 const QCA::Certificate cert( bundle.certificateChain().primary() );
357 if ( cert.isNull() )
358 {
359 writeValidation( tr( "Bundle client cert can not be loaded" ), Invalid );
360 return false;
361 }
362
363 // TODO: add more robust validation, including cert chain resolution
364 const QDateTime startdate( cert.notValidBefore() );
365 const QDateTime enddate( cert.notValidAfter() );
366 const QDateTime now( QDateTime::currentDateTime() );
367 const bool bundlevalid = ( now >= startdate && now <= enddate );
368
369 writeValidation( tr( "%1 thru %2" ).arg( startdate.toString(), enddate.toString() ), ( bundlevalid ? Valid : Invalid ) );
370
371 if ( bundlevalid )
372 {
373 QSslCertificate clientcert;
374 QList<QSslCertificate> certs( QgsAuthCertUtils::certsFromString( cert.toPEM() ) );
375 if ( !certs.isEmpty() )
376 {
377 clientcert = certs.first();
378 }
379 if ( clientcert.isNull() )
380 {
381 writeValidation( tr( "Qt cert could not be created from QCA cert" ), Invalid, true );
382 return false;
383 }
384 QSslKey clientkey;
385 clientkey = QSslKey( bundle.privateKey().toRSA().toPEM().toLatin1(), QSsl::Rsa );
386 if ( clientkey.isNull() )
387 {
388 writeValidation( tr( "Qt private key could not be created from QCA key" ), Invalid, true );
389 return false;
390 }
391
392 const QCA::CertificateChain cert_chain( bundle.certificateChain() );
393 QList<QSslCertificate> ca_certs;
394 if ( cert_chain.size() > 1 )
395 {
396 const auto constCert_chain = cert_chain;
397 for ( const QCA::Certificate &ca_cert : constCert_chain )
398 {
399 if ( ca_cert != cert_chain.primary() )
400 {
401 ca_certs << QSslCertificate( ca_cert.toPEM().toLatin1() );
402 }
403 }
404 }
405
406 mCertBundle = qMakePair( clientcert, clientkey );
407 mPkiBundle = QgsPkiBundle( clientcert, clientkey, ca_certs );
408 }
409
410 return bundlevalid;
411}
412
413void QgsAuthImportIdentityDialog::fileFound( bool found, QWidget *widget )
414{
415 if ( !found )
416 {
417 widget->setStyleSheet( QgsAuthGuiUtils::redTextStyleSheet( QStringLiteral( "QLineEdit" ) ) );
418 widget->setToolTip( tr( "File not found" ) );
419 }
420 else
421 {
422 widget->setStyleSheet( QString() );
423 widget->setToolTip( QString() );
424 }
425}
426
427QString QgsAuthImportIdentityDialog::getOpenFileName( const QString &title, const QString &extfilter )
428{
429 QgsSettings settings;
430 const QString recentdir = settings.value( QStringLiteral( "UI/lastAuthImportBundleOpenFileDir" ), QDir::homePath() ).toString();
431 QString f = QFileDialog::getOpenFileName( this, title, recentdir, extfilter );
432
433 // return dialog focus on Mac
434 this->raise();
435 this->activateWindow();
436
437 if ( !f.isEmpty() )
438 {
439 settings.setValue( QStringLiteral( "UI/lastAuthImportBundleOpenFileDir" ), QFileInfo( f ).absoluteDir().path() );
440 }
441 return f;
442}
443
444QPushButton *QgsAuthImportIdentityDialog::okButton()
445{
446 return buttonBox->button( QDialogButtonBox::Ok );
447}
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:64
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.