QGIS API Documentation  3.21.0-Master (5b68dc587e)
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 
77 const QPair<QSslCertificate, QSslKey> QgsAuthImportIdentityDialog::certBundleToImport()
78 {
79  if ( mDisabled )
80  {
81  return qMakePair( QSslCertificate(), QSslKey() );
82  }
83  return mCertBundle;
84 }
85 
86 void 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 
113 void QgsAuthImportIdentityDialog::validateIdentity()
114 {
115  bool ok = false;
116  if ( mIdentityType == CertIdentity )
117  {
118  ok = validateBundle();
119  }
120  okButton()->setEnabled( ok );
121 }
122 
123 bool 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 
145 void QgsAuthImportIdentityDialog::clearValidation()
146 {
147  teValidation->clear();
148  teValidation->setStyleSheet( QString() );
149 }
150 
151 void 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 
182 void QgsAuthImportIdentityDialog::lePkiPathsKeyPass_textChanged( const QString &pass )
183 {
184  Q_UNUSED( pass )
185  validateIdentity();
186 }
187 
188 void QgsAuthImportIdentityDialog::chkPkiPathsPassShow_stateChanged( int state )
189 {
190  lePkiPathsKeyPass->setEchoMode( ( state > 0 ) ? QLineEdit::Normal : QLineEdit::Password );
191 }
192 
193 void 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 
203 void 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 
213 void QgsAuthImportIdentityDialog::lePkiPkcs12KeyPass_textChanged( const QString &pass )
214 {
215  Q_UNUSED( pass )
216  validateIdentity();
217 }
218 
219 void QgsAuthImportIdentityDialog::chkPkiPkcs12PassShow_stateChanged( int state )
220 {
221  lePkiPkcs12KeyPass->setEchoMode( ( state > 0 ) ? QLineEdit::Normal : QLineEdit::Password );
222 }
223 
224 void 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 
234 bool 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 
313 bool 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 
426 void 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 
440 QString 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 
457 QPushButton *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.