QGIS API Documentation  2.12.0-Lyon
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( 0 )
59  , mAuthNotify( 0 )
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  QgsDebugMsg( "entered" );
143 
144  // clear out any previously set bundle
145  QSslCertificate emptycert;
146  QSslKey emptykey;
147  mCertBundle = qMakePair( emptycert, emptykey );
148  mPkiBundle = QgsPkiBundle();
149 
150  QWidget *curpage = stkwBundleType->currentWidget();
151  if ( curpage == pagePkiPaths )
152  {
153  return validatePkiPaths();
154  }
155  else if ( curpage == pagePkiPkcs12 )
156  {
157  return validatePkiPkcs12();
158  }
159 
160  return false;
161 }
162 
163 void QgsAuthImportIdentityDialog::clearValidation()
164 {
165  teValidation->clear();
166  teValidation->setStyleSheet( "" );
167 }
168 
169 void QgsAuthImportIdentityDialog::writeValidation( const QString &msg,
171  bool append )
172 {
173  QString ss;
174  QString txt( msg );
175  switch ( valid )
176  {
177  case Valid:
178  ss = QgsAuthGuiUtils::greenTextStyleSheet( "QTextEdit" );
179  txt = tr( "Valid: %1" ).arg( msg );
180  break;
181  case Invalid:
182  ss = QgsAuthGuiUtils::redTextStyleSheet( "QTextEdit" );
183  txt = tr( "Invalid: %1" ).arg( msg );
184  break;
185  case Unknown:
186  default:
187  ss = "";
188  break;
189  }
190  teValidation->setStyleSheet( ss );
191  if ( append )
192  {
193  teValidation->append( txt );
194  }
195  else
196  {
197  teValidation->setText( txt );
198  }
199  teValidation->moveCursor( QTextCursor::Start );
200 }
201 
202 void QgsAuthImportIdentityDialog::on_lePkiPathsKeyPass_textChanged( const QString &pass )
203 {
204  Q_UNUSED( pass );
205  validateIdentity();
206 }
207 
208 void QgsAuthImportIdentityDialog::on_chkPkiPathsPassShow_stateChanged( int state )
209 {
210  lePkiPathsKeyPass->setEchoMode(( state > 0 ) ? QLineEdit::Normal : QLineEdit::Password );
211 }
212 
213 void QgsAuthImportIdentityDialog::on_btnPkiPathsCert_clicked()
214 {
215  const QString& fn = getOpenFileName( tr( "Open Client Certificate File" ), tr( "PEM (*.pem);;DER (*.der)" ) );
216  if ( !fn.isEmpty() )
217  {
218  lePkiPathsCert->setText( fn );
219  validateIdentity();
220  }
221 }
222 
223 void QgsAuthImportIdentityDialog::on_btnPkiPathsKey_clicked()
224 {
225  const QString& fn = getOpenFileName( tr( "Open Private Key File" ), tr( "PEM (*.pem);;DER (*.der)" ) );
226  if ( !fn.isEmpty() )
227  {
228  lePkiPathsKey->setText( fn );
229  validateIdentity();
230  }
231 }
232 
233 void QgsAuthImportIdentityDialog::on_lePkiPkcs12KeyPass_textChanged( const QString &pass )
234 {
235  Q_UNUSED( pass );
236  validateIdentity();
237 }
238 
239 void QgsAuthImportIdentityDialog::on_chkPkiPkcs12PassShow_stateChanged( int state )
240 {
241  lePkiPkcs12KeyPass->setEchoMode(( state > 0 ) ? QLineEdit::Normal : QLineEdit::Password );
242 }
243 
244 void QgsAuthImportIdentityDialog::on_btnPkiPkcs12Bundle_clicked()
245 {
246  const QString& fn = getOpenFileName( tr( "Open PKCS#12 Certificate Bundle" ), tr( "PKCS#12 (*.p12 *.pfx)" ) );
247  if ( !fn.isEmpty() )
248  {
249  lePkiPkcs12Bundle->setText( fn );
250  validateIdentity();
251  }
252 }
253 
254 bool QgsAuthImportIdentityDialog::validatePkiPaths()
255 {
256  bool isvalid = false;
257 
258  // required components
259  QString certpath( lePkiPathsCert->text() );
260  QString keypath( lePkiPathsKey->text() );
261 
262  bool certfound = QFile::exists( certpath );
263  bool keyfound = QFile::exists( keypath );
264 
265  fileFound( certpath.isEmpty() || certfound, lePkiPathsCert );
266  fileFound( keypath.isEmpty() || keyfound, lePkiPathsKey );
267 
268  if ( !certfound || !keyfound )
269  {
270  writeValidation( tr( "Missing components" ), Invalid );
271  return false;
272  }
273 
274  // check for issue date validity
275  QSslCertificate clientcert;
277  QList<QSslCertificate> ca_certs;
278  if ( !certs.isEmpty() )
279  {
280  clientcert = certs.takeFirst();
281  }
282  else
283  {
284  writeValidation( tr( "Failed to read client certificate from file" ), Invalid );
285  return false;
286  }
287 
288  if ( clientcert.isNull() )
289  {
290  writeValidation( tr( "Failed to load client certificate from file" ), Invalid );
291  return false;
292  }
293 
294  if ( !certs.isEmpty() ) // Multiple certificates in file
295  {
296  teValidation->append( tr( "Extra certificates found with identity" ) );
297  ca_certs = certs;
298  }
299 
300  isvalid = clientcert.isValid();
301  QDateTime startdate( clientcert.effectiveDate() );
302  QDateTime enddate( clientcert.expiryDate() );
303 
304  writeValidation( tr( "%1 thru %2" ).arg( startdate.toString(), enddate.toString() ),
305  ( isvalid ? Valid : Invalid ) );
306  //TODO: set enabled on cert info button, relative to cert validity
307 
308  // check for valid private key and that any supplied password works
309  bool keypem = keypath.endsWith( ".pem", Qt::CaseInsensitive );
310  QByteArray keydata( fileData_( keypath, keypem ) );
311 
312  QSslKey clientkey;
313  QString keypass = lePkiPathsKeyPass->text();
314  clientkey = QSslKey( keydata,
315  QSsl::Rsa,
316  keypem ? QSsl::Pem : QSsl::Der,
317  QSsl::PrivateKey,
318  !keypass.isEmpty() ? keypass.toUtf8() : QByteArray() );
319  if ( clientkey.isNull() )
320  {
321  // try DSA algorithm, since Qt can't seem to determine it otherwise
322  clientkey = QSslKey( keydata,
323  QSsl::Dsa,
324  keypem ? QSsl::Pem : QSsl::Der,
325  QSsl::PrivateKey,
326  !keypass.isEmpty() ? keypass.toUtf8() : QByteArray() );
327  }
328 
329  if ( clientkey.isNull() )
330  {
331  writeValidation( tr( "Failed to load client private key from file" ), Invalid, true );
332  if ( !keypass.isEmpty() )
333  {
334  writeValidation( tr( "Private key password may not match" ), Invalid, true );
335  }
336  return false;
337  }
338  else
339  {
340  isvalid = isvalid && true;
341  }
342 
343  if ( isvalid )
344  {
345  mCertBundle = qMakePair( clientcert, clientkey );
346  mPkiBundle = QgsPkiBundle( clientcert,
347  clientkey,
348  ca_certs );
349  }
350 
351  return isvalid;
352 }
353 
354 bool QgsAuthImportIdentityDialog::validatePkiPkcs12()
355 {
356  // required components
357  QString bundlepath( lePkiPkcs12Bundle->text() );
358  bool bundlefound = QFile::exists( bundlepath );
359  fileFound( bundlepath.isEmpty() || bundlefound, lePkiPkcs12Bundle );
360 
361  if ( !bundlefound )
362  {
363  writeValidation( tr( "Missing components" ), Invalid );
364  return false;
365  }
366 
367  if ( !QCA::isSupported( "pkcs12" ) )
368  {
369  writeValidation( tr( "QCA library has no PKCS#12 support" ), Invalid );
370  return false;
371  }
372 
373  // load the bundle
374  QCA::SecureArray passarray;
375  QString keypass = QString::null;
376  if ( !lePkiPkcs12KeyPass->text().isEmpty() )
377  {
378  passarray = QCA::SecureArray( lePkiPkcs12KeyPass->text().toUtf8() );
379  keypass = lePkiPkcs12KeyPass->text();
380  }
381 
382  QCA::ConvertResult res;
383  QCA::KeyBundle bundle( QCA::KeyBundle::fromFile( bundlepath, passarray, &res, QString( "qca-ossl" ) ) );
384 
385  if ( res == QCA::ErrorFile )
386  {
387  writeValidation( tr( "Failed to read bundle file" ), Invalid );
388  return false;
389  }
390  else if ( res == QCA::ErrorPassphrase )
391  {
392  writeValidation( tr( "Incorrect bundle password" ), Invalid );
393  lePkiPkcs12KeyPass->setPlaceholderText( QString( "Required passphrase" ) );
394  return false;
395  }
396  else if ( res == QCA::ErrorDecode )
397  {
398  writeValidation( tr( "Failed to decode (try entering password)" ), Invalid );
399  return false;
400  }
401 
402  if ( bundle.isNull() )
403  {
404  writeValidation( tr( "Bundle empty or can not be loaded" ), Invalid );
405  return false;
406  }
407 
408  // check for primary cert and that it is valid
409  QCA::Certificate cert( bundle.certificateChain().primary() );
410  if ( cert.isNull() )
411  {
412  writeValidation( tr( "Bundle client cert can not be loaded" ), Invalid );
413  return false;
414  }
415 
416  // TODO: add more robust validation, including cert chain resolution
417  QDateTime startdate( cert.notValidBefore() );
418  QDateTime enddate( cert.notValidAfter() );
420  bool bundlevalid = ( now >= startdate && now <= enddate );
421 
422  writeValidation( tr( "%1 thru %2" ).arg( startdate.toString(), enddate.toString() ),
423  ( bundlevalid ? Valid : Invalid ) );
424 
425  if ( bundlevalid )
426  {
427  QSslCertificate clientcert;
429  if ( !certs.isEmpty() )
430  {
431  clientcert = certs.first();
432  }
433  if ( clientcert.isNull() )
434  {
435  writeValidation( tr( "Qt cert could not be created from QCA cert" ), Invalid, true );
436  return false;
437  }
438  QSslKey clientkey;
439  clientkey = QSslKey( bundle.privateKey().toRSA().toPEM().toAscii(), QSsl::Rsa );
440  if ( clientkey.isNull() )
441  {
442  writeValidation( tr( "Qt private key could not be created from QCA key" ), Invalid, true );
443  return false;
444  }
445 
446  QCA::CertificateChain cert_chain( bundle.certificateChain() );
447  QList<QSslCertificate> ca_certs;
448  if ( cert_chain.size() > 1 )
449  {
450  Q_FOREACH ( const QCA::Certificate& ca_cert, cert_chain )
451  {
452  if ( ca_cert != cert_chain.primary() )
453  {
454  ca_certs << QSslCertificate( ca_cert.toPEM().toAscii() );
455  }
456  }
457  }
458 
459  mCertBundle = qMakePair( clientcert, clientkey );
460  mPkiBundle = QgsPkiBundle( clientcert, clientkey, ca_certs );
461  }
462 
463  return bundlevalid;
464 }
465 
466 void QgsAuthImportIdentityDialog::fileFound( bool found, QWidget *widget )
467 {
468  if ( !found )
469  {
470  widget->setStyleSheet( QgsAuthGuiUtils::redTextStyleSheet( "QLineEdit" ) );
471  widget->setToolTip( tr( "File not found" ) );
472  }
473  else
474  {
475  widget->setStyleSheet( "" );
476  widget->setToolTip( "" );
477  }
478 }
479 
480 QString QgsAuthImportIdentityDialog::getOpenFileName( const QString &title, const QString &extfilter )
481 {
482  QSettings settings;
483  QString recentdir = settings.value( "UI/lastAuthImportBundleOpenFileDir", QDir::homePath() ).toString();
484  QString f = QFileDialog::getOpenFileName( this, title, recentdir, extfilter );
485 
486  // return dialog focus on Mac
487  this->raise();
488  this->activateWindow();
489 
490  if ( !f.isEmpty() )
491  {
492  settings.setValue( "UI/lastAuthImportBundleOpenFileDir", QFileInfo( f ).absoluteDir().path() );
493  }
494  return f;
495 }
496 
497 QPushButton *QgsAuthImportIdentityDialog::okButton()
498 {
499  return buttonBox->button( QDialogButtonBox::Ok );
500 }
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)
static QgsAuthManager * instance()
Enforce singleton pattern.
bool isNull() const
bool isValid() const
#define QgsDebugMsg(str)
Definition: qgslogger.h:33
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.
IdentityType
Type of identity being imported.
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
QByteArray readAll()
QgsAuthImportIdentityDialog(QgsAuthImportIdentityDialog::IdentityType identitytype, QWidget *parent=0)
Construct a dialog for importing identities.
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()
Validity
Type of certificate/bundle validity output.
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)
QString arg(qlonglong a, int fieldWidth, int base, const QChar &fillChar) const
QString toString() const
QByteArray toUtf8() const