QGIS API Documentation  2.18.21-Las Palmas (9fba24a)
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 #include <QSettings>
24 
25 #include "qgsauthcertutils.h"
26 #include "qgsauthconfig.h"
27 #include "qgsauthguiutils.h"
28 #include "qgsauthmanager.h"
29 #include "qgslogger.h"
30 
31 
32 static QByteArray fileData_( const QString& path, bool astext = false )
33 {
34  QByteArray data;
35  QFile file( path );
36  if ( file.exists() )
37  {
38  QFile::OpenMode openflags( QIODevice::ReadOnly );
39  if ( astext )
40  openflags |= QIODevice::Text;
41  bool ret = file.open( openflags );
42  if ( ret )
43  {
44  data = file.readAll();
45  }
46  file.close();
47  }
48  return data;
49 }
50 
51 
53  QWidget *parent )
54  : QDialog( parent )
55  , mIdentityType( CertIdentity )
56  , mPkiBundle( QgsPkiBundle() )
57  , mDisabled( false )
58  , mAuthNotifyLayout( nullptr )
59  , mAuthNotify( nullptr )
60 {
61  if ( QgsAuthManager::instance()->isDisabled() )
62  {
63  mDisabled = true;
64  mAuthNotifyLayout = new QVBoxLayout;
65  this->setLayout( mAuthNotifyLayout );
66  mAuthNotify = new QLabel( QgsAuthManager::instance()->disabledMessage(), this );
67  mAuthNotifyLayout->addWidget( mAuthNotify );
68  }
69  else
70  {
71  setupUi( this );
72  connect( buttonBox, SIGNAL( rejected() ), this, SLOT( close() ) );
73  connect( buttonBox, SIGNAL( accepted() ), this, SLOT( accept() ) );
74 
75  mIdentityType = identitytype;
76 
77  populateIdentityType();
78  }
79 }
80 
82 {
83 }
84 
86 {
87  if ( mDisabled )
88  {
90  }
91  return mIdentityType;
92 }
93 
95 {
96  if ( mDisabled )
97  {
98  return qMakePair( QSslCertificate(), QSslKey() );
99  }
100  return mCertBundle;
101 }
102 
103 void QgsAuthImportIdentityDialog::populateIdentityType()
104 {
105  if ( mIdentityType == CertIdentity )
106  {
107  stkwBundleType->setVisible( true );
108 
109  cmbIdentityTypes->addItem( tr( "PKI PEM/DER Certificate Paths" ),
111  cmbIdentityTypes->addItem( tr( "PKI PKCS#12 Certificate Bundle" ),
113 
114  connect( cmbIdentityTypes, SIGNAL( currentIndexChanged( int ) ),
115  stkwBundleType, SLOT( setCurrentIndex( int ) ) );
116  connect( stkwBundleType, SIGNAL( currentChanged( int ) ),
117  cmbIdentityTypes, SLOT( setCurrentIndex( int ) ) );
118 
119  connect( cmbIdentityTypes, SIGNAL( currentIndexChanged( int ) ),
120  this, SLOT( validateIdentity() ) );
121  connect( stkwBundleType, SIGNAL( currentChanged( int ) ),
122  this, SLOT( validateIdentity() ) );
123 
124  cmbIdentityTypes->setCurrentIndex( 0 );
125  stkwBundleType->setCurrentIndex( 0 );
126  }
127  // else switch stacked widget, and populate/connect according to that type and widget
128 }
129 
130 void QgsAuthImportIdentityDialog::validateIdentity()
131 {
132  bool ok = false;
133  if ( mIdentityType == CertIdentity )
134  {
135  ok = validateBundle();
136  }
137  okButton()->setEnabled( ok );
138 }
139 
140 bool QgsAuthImportIdentityDialog::validateBundle()
141 {
142 
143  // clear out any previously set bundle
144  QSslCertificate emptycert;
145  QSslKey emptykey;
146  mCertBundle = qMakePair( emptycert, emptykey );
147  mPkiBundle = QgsPkiBundle();
148 
149  QWidget *curpage = stkwBundleType->currentWidget();
150  if ( curpage == pagePkiPaths )
151  {
152  return validatePkiPaths();
153  }
154  else if ( curpage == pagePkiPkcs12 )
155  {
156  return validatePkiPkcs12();
157  }
158 
159  return false;
160 }
161 
162 void QgsAuthImportIdentityDialog::clearValidation()
163 {
164  teValidation->clear();
165  teValidation->setStyleSheet( "" );
166 }
167 
168 void QgsAuthImportIdentityDialog::writeValidation( const QString &msg,
170  bool append )
171 {
172  QString ss;
173  QString txt( msg );
174  switch ( valid )
175  {
176  case Valid:
177  ss = QgsAuthGuiUtils::greenTextStyleSheet( "QTextEdit" );
178  txt = tr( "Valid: %1" ).arg( msg );
179  break;
180  case Invalid:
181  ss = QgsAuthGuiUtils::redTextStyleSheet( "QTextEdit" );
182  txt = tr( "Invalid: %1" ).arg( msg );
183  break;
184  case Unknown:
185  default:
186  ss = "";
187  break;
188  }
189  teValidation->setStyleSheet( ss );
190  if ( append )
191  {
192  teValidation->append( txt );
193  }
194  else
195  {
196  teValidation->setText( txt );
197  }
198  teValidation->moveCursor( QTextCursor::Start );
199 }
200 
201 void QgsAuthImportIdentityDialog::on_lePkiPathsKeyPass_textChanged( const QString &pass )
202 {
203  Q_UNUSED( pass );
204  validateIdentity();
205 }
206 
207 void QgsAuthImportIdentityDialog::on_chkPkiPathsPassShow_stateChanged( int state )
208 {
209  lePkiPathsKeyPass->setEchoMode(( state > 0 ) ? QLineEdit::Normal : QLineEdit::Password );
210 }
211 
212 void QgsAuthImportIdentityDialog::on_btnPkiPathsCert_clicked()
213 {
214  const QString& fn = getOpenFileName( tr( "Open Client Certificate File" ), tr( "PEM (*.pem);;DER (*.der)" ) );
215  if ( !fn.isEmpty() )
216  {
217  lePkiPathsCert->setText( fn );
218  validateIdentity();
219  }
220 }
221 
222 void QgsAuthImportIdentityDialog::on_btnPkiPathsKey_clicked()
223 {
224  const QString& fn = getOpenFileName( tr( "Open Private Key File" ), tr( "PEM (*.pem);;DER (*.der)" ) );
225  if ( !fn.isEmpty() )
226  {
227  lePkiPathsKey->setText( fn );
228  validateIdentity();
229  }
230 }
231 
232 void QgsAuthImportIdentityDialog::on_lePkiPkcs12KeyPass_textChanged( const QString &pass )
233 {
234  Q_UNUSED( pass );
235  validateIdentity();
236 }
237 
238 void QgsAuthImportIdentityDialog::on_chkPkiPkcs12PassShow_stateChanged( int state )
239 {
240  lePkiPkcs12KeyPass->setEchoMode(( state > 0 ) ? QLineEdit::Normal : QLineEdit::Password );
241 }
242 
243 void QgsAuthImportIdentityDialog::on_btnPkiPkcs12Bundle_clicked()
244 {
245  const QString& fn = getOpenFileName( tr( "Open PKCS#12 Certificate Bundle" ), tr( "PKCS#12 (*.p12 *.pfx)" ) );
246  if ( !fn.isEmpty() )
247  {
248  lePkiPkcs12Bundle->setText( fn );
249  validateIdentity();
250  }
251 }
252 
253 bool QgsAuthImportIdentityDialog::validatePkiPaths()
254 {
255  bool isvalid = false;
256 
257  // required components
258  QString certpath( lePkiPathsCert->text() );
259  QString keypath( lePkiPathsKey->text() );
260 
261  bool certfound = QFile::exists( certpath );
262  bool keyfound = QFile::exists( keypath );
263 
264  fileFound( certpath.isEmpty() || certfound, lePkiPathsCert );
265  fileFound( keypath.isEmpty() || keyfound, lePkiPathsKey );
266 
267  if ( !certfound || !keyfound )
268  {
269  writeValidation( tr( "Missing components" ), Invalid );
270  return false;
271  }
272 
273  // check for issue date validity
274  QSslCertificate clientcert;
276  QList<QSslCertificate> ca_certs;
277  if ( !certs.isEmpty() )
278  {
279  clientcert = certs.takeFirst();
280  }
281  else
282  {
283  writeValidation( tr( "Failed to read client certificate from file" ), Invalid );
284  return false;
285  }
286 
287  if ( clientcert.isNull() )
288  {
289  writeValidation( tr( "Failed to load client certificate from file" ), Invalid );
290  return false;
291  }
292 
293  if ( !certs.isEmpty() ) // Multiple certificates in file
294  {
295  teValidation->append( tr( "Extra certificates found with identity" ) );
296  ca_certs = certs;
297  }
298 
299  isvalid = clientcert.isValid();
300  QDateTime startdate( clientcert.effectiveDate() );
301  QDateTime enddate( clientcert.expiryDate() );
302 
303  writeValidation( tr( "%1 thru %2" ).arg( startdate.toString(), enddate.toString() ),
304  ( isvalid ? Valid : Invalid ) );
305  //TODO: set enabled on cert info button, relative to cert validity
306 
307  // check for valid private key and that any supplied password works
308  bool keypem = keypath.endsWith( ".pem", Qt::CaseInsensitive );
309  QByteArray keydata( fileData_( keypath, keypem ) );
310 
311  QSslKey clientkey;
312  QString keypass = lePkiPathsKeyPass->text();
313  clientkey = QSslKey( keydata,
314  QSsl::Rsa,
315  keypem ? QSsl::Pem : QSsl::Der,
316  QSsl::PrivateKey,
317  !keypass.isEmpty() ? keypass.toUtf8() : QByteArray() );
318  if ( clientkey.isNull() )
319  {
320  // try DSA algorithm, since Qt can't seem to determine it otherwise
321  clientkey = QSslKey( keydata,
322  QSsl::Dsa,
323  keypem ? QSsl::Pem : QSsl::Der,
324  QSsl::PrivateKey,
325  !keypass.isEmpty() ? keypass.toUtf8() : QByteArray() );
326  }
327 
328  if ( clientkey.isNull() )
329  {
330  writeValidation( tr( "Failed to load client private key from file" ), Invalid, true );
331  if ( !keypass.isEmpty() )
332  {
333  writeValidation( tr( "Private key password may not match" ), Invalid, true );
334  }
335  return false;
336  }
337  else
338  {
339  isvalid = isvalid && true;
340  }
341 
342  if ( isvalid )
343  {
344  mCertBundle = qMakePair( clientcert, clientkey );
345  mPkiBundle = QgsPkiBundle( clientcert,
346  clientkey,
347  ca_certs );
348  }
349 
350  return isvalid;
351 }
352 
353 bool QgsAuthImportIdentityDialog::validatePkiPkcs12()
354 {
355  // required components
356  QString bundlepath( lePkiPkcs12Bundle->text() );
357  bool bundlefound = QFile::exists( bundlepath );
358  fileFound( bundlepath.isEmpty() || bundlefound, lePkiPkcs12Bundle );
359 
360  if ( !bundlefound )
361  {
362  writeValidation( tr( "Missing components" ), Invalid );
363  return false;
364  }
365 
366  if ( !QCA::isSupported( "pkcs12" ) )
367  {
368  writeValidation( tr( "QCA library has no PKCS#12 support" ), Invalid );
369  return false;
370  }
371 
372  // load the bundle
373  QCA::SecureArray passarray;
374  QString keypass = QString::null;
375  if ( !lePkiPkcs12KeyPass->text().isEmpty() )
376  {
377  passarray = QCA::SecureArray( lePkiPkcs12KeyPass->text().toUtf8() );
378  keypass = lePkiPkcs12KeyPass->text();
379  }
380 
381  QCA::ConvertResult res;
382  QCA::KeyBundle bundle( QCA::KeyBundle::fromFile( bundlepath, passarray, &res, QString( "qca-ossl" ) ) );
383 
384  if ( res == QCA::ErrorFile )
385  {
386  writeValidation( tr( "Failed to read bundle file" ), Invalid );
387  return false;
388  }
389  else if ( res == QCA::ErrorPassphrase )
390  {
391  writeValidation( tr( "Incorrect bundle password" ), Invalid );
392  lePkiPkcs12KeyPass->setPlaceholderText( QString( "Required passphrase" ) );
393  return false;
394  }
395  else if ( res == QCA::ErrorDecode )
396  {
397  writeValidation( tr( "Failed to decode (try entering password)" ), Invalid );
398  return false;
399  }
400 
401  if ( bundle.isNull() )
402  {
403  writeValidation( tr( "Bundle empty or can not be loaded" ), Invalid );
404  return false;
405  }
406 
407  // check for primary cert and that it is valid
408  QCA::Certificate cert( bundle.certificateChain().primary() );
409  if ( cert.isNull() )
410  {
411  writeValidation( tr( "Bundle client cert can not be loaded" ), Invalid );
412  return false;
413  }
414 
415  // TODO: add more robust validation, including cert chain resolution
416  QDateTime startdate( cert.notValidBefore() );
417  QDateTime enddate( cert.notValidAfter() );
419  bool bundlevalid = ( now >= startdate && now <= enddate );
420 
421  writeValidation( tr( "%1 thru %2" ).arg( startdate.toString(), enddate.toString() ),
422  ( bundlevalid ? Valid : Invalid ) );
423 
424  if ( bundlevalid )
425  {
426  QSslCertificate clientcert;
428  if ( !certs.isEmpty() )
429  {
430  clientcert = certs.first();
431  }
432  if ( clientcert.isNull() )
433  {
434  writeValidation( tr( "Qt cert could not be created from QCA cert" ), Invalid, true );
435  return false;
436  }
437  QSslKey clientkey;
438  clientkey = QSslKey( bundle.privateKey().toRSA().toPEM().toAscii(), QSsl::Rsa );
439  if ( clientkey.isNull() )
440  {
441  writeValidation( tr( "Qt private key could not be created from QCA key" ), Invalid, true );
442  return false;
443  }
444 
445  QCA::CertificateChain cert_chain( bundle.certificateChain() );
446  QList<QSslCertificate> ca_certs;
447  if ( cert_chain.size() > 1 )
448  {
449  Q_FOREACH ( const QCA::Certificate& ca_cert, cert_chain )
450  {
451  if ( ca_cert != cert_chain.primary() )
452  {
453  ca_certs << QSslCertificate( ca_cert.toPEM().toAscii() );
454  }
455  }
456  }
457 
458  mCertBundle = qMakePair( clientcert, clientkey );
459  mPkiBundle = QgsPkiBundle( clientcert, clientkey, ca_certs );
460  }
461 
462  return bundlevalid;
463 }
464 
465 void QgsAuthImportIdentityDialog::fileFound( bool found, QWidget *widget )
466 {
467  if ( !found )
468  {
469  widget->setStyleSheet( QgsAuthGuiUtils::redTextStyleSheet( "QLineEdit" ) );
470  widget->setToolTip( tr( "File not found" ) );
471  }
472  else
473  {
474  widget->setStyleSheet( "" );
475  widget->setToolTip( "" );
476  }
477 }
478 
479 QString QgsAuthImportIdentityDialog::getOpenFileName( const QString &title, const QString &extfilter )
480 {
481  QSettings settings;
482  QString recentdir = settings.value( "UI/lastAuthImportBundleOpenFileDir", QDir::homePath() ).toString();
483  QString f = QFileDialog::getOpenFileName( this, title, recentdir, extfilter );
484 
485  // return dialog focus on Mac
486  this->raise();
487  this->activateWindow();
488 
489  if ( !f.isEmpty() )
490  {
491  settings.setValue( "UI/lastAuthImportBundleOpenFileDir", QFileInfo( f ).absoluteDir().path() );
492  }
493  return f;
494 }
495 
496 QPushButton *QgsAuthImportIdentityDialog::okButton()
497 {
498  return buttonBox->button( QDialogButtonBox::Ok );
499 }
typedef OpenMode
QDateTime effectiveDate() const
void setStyleSheet(const QString &styleSheet)
bool close()
QString & append(QChar ch)
void setupUi(QWidget *widget)
static QByteArray fileData_(const QString &path, bool astext=false)
IdentityType
Type of identity being imported.
static QgsAuthManager * instance()
Enforce singleton pattern.
bool isNull() const
bool isValid() const
void rejected()
static QList< QSslCertificate > certsFromFile(const QString &certspath)
Return list of concatenated certs from a PEM or DER formatted file.
void accepted()
QgsAuthImportIdentityDialog::IdentityType identityType()
Get identity type.
bool exists() const
QString homePath()
QString tr(const char *sourceText, const char *disambiguation, int n)
Storage set for PKI bundle: SSL certificate, key, optional CA cert chain.
void setValue(const QString &key, const QVariant &value)
void setEnabled(bool)
void addWidget(QWidget *widget, int stretch, QFlags< Qt::AlignmentFlag > alignment)
void setLayout(QLayout *layout)
static QString greenTextStyleSheet(const QString &selector="*")
Green text stylesheet representing valid, trusted, etc.
bool isNull() const
bool isEmpty() const
bool isEmpty() const
QByteArray readAll()
QgsAuthImportIdentityDialog(QgsAuthImportIdentityDialog::IdentityType identitytype, QWidget *parent=nullptr)
Construct a dialog for importing identities.
T & first()
virtual bool open(QFlags< QIODevice::OpenModeFlag > mode)
virtual void accept()
static QList< QSslCertificate > certsFromString(const QString &pemtext)
Return list of concatenated certs from a PEM Base64 text block.
virtual void close()
static QString redTextStyleSheet(const QString &selector="*")
Red text stylesheet representing invalid, untrusted, etc.
QVariant value(const QString &key, const QVariant &defaultValue) const
QDateTime currentDateTime()
QDateTime expiryDate() const
void activateWindow()
T takeFirst()
void setToolTip(const QString &)
QString getOpenFileName(QWidget *parent, const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, QFlags< QFileDialog::Option > options)
const QPair< QSslCertificate, QSslKey > certBundleToImport()
Get certificate/key bundle to be imported.
bool connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
Validity
Type of certificate/bundle validity output.
QString arg(qlonglong a, int fieldWidth, int base, const QChar &fillChar) const
QString toString() const
QByteArray toUtf8() const