Pull to refresh

Восполняя пробелы Qt — Генерация ключа SSL

Reading time7 min
Views7.5K
К сожалению библиотека Qt, имея все необходимые компоненты для работы с openSSL не включает в себя кода для генерации ключей. Посему попытаемся исправить ситуацию.
Для начала рассмотрим заголовочный файл:
#ifndef SSLKEYGEN_H
#define SSLKEYGEN_H

#include <QtGui/QDialog>
#include <QSslKey>
#include <QMap>
#include <QSslCertificate>
#include <QWizard>
#include <QWizardPage>
#include <QProgressBar>
#include <QDir>
#include <QFileDialog>
#include <QFileInfo>
#include <QFile>
#include <QLabel>

bool isPrivateKeyCorrespondsToCertificate(QSslCertificate cert, QSslKey key);

class CertificateGeneratorWizard : public QWizard
{
	Q_OBJECT
	
public:
	CertificateGeneratorWizard(QWidget * parent=0);
	QSslKey getPrivateKey(){return pkey;}
	QSslCertificate getCertificate(){return cert;}
	void accept();
private:
	QSslKey pkey;
	QSslCertificate cert;
};

class RandSeeder : public QObject
{
	Q_OBJECT
	
public:
	void startSeeding(QList<QWidget *> w, int lim);
	void stop();
	bool isCompleted() const{return (counter>=maximum);}
protected:
	QMap<QWidget *, bool> wdgs;
	int counter;
	int maximum;
	bool eventFilter(QObject *obj, QEvent *event);
signals:
	void step();
	void stopped();
};

class getCertDataPage : public QWizardPage
{
	Q_OBJECT
	
public:
	getCertDataPage(QWidget *parent=0);
};

class randomizePage : public QWizardPage
{
	Q_OBJECT
	
public:
	randomizePage(QWidget *parent=0);
	virtual void cleanupPage();
	virtual void initializePage();
	virtual bool isComplete() const;
private:
	QProgressBar * progressBar;
	QLabel * label;
	RandSeeder seeder;
	private slots:
	void seedStep();
	void seedStopped();
};

#endif // SSLKEYGEN_H

Первая объявленная функцииis PrivateKeyCorrespondsToCertificate предназначена, как вы понимаете, для проверки соответствия сертификата ключу — тоже явное упущение библиотеки Qt. Вот ее код из файла sslkeygen.cpp:
bool isPrivateKeyCorrespondsToCertificate( QSslCertificate cert, QSslKey key )
{
	X509 *x;
	EVP_PKEY *k;
	
	x=(X509 *)cert.handle();
	k=EVP_PKEY_new();
	if(key.algorithm() == QSsl::Rsa)
		EVP_PKEY_assign_RSA(k, (RSA *)key.handle());
	else
		EVP_PKEY_assign_DSA(k, (DSA *)key.handle());
	if(X509_check_private_key(x,k)==1)
		return true;
	return false;
}

Все достаточно просто, если вы знаете библиотеку OpenSSL (документирована она, прямо скажем, неважно — вроде все функции описаны, но как-то скупо и без пояснений)
Следующие объявления определяют визард генерации ключа и его страницы. Несколько особняком стоит класс RandSeeder, но о немпозже.
Чтобы не загромождать статью лишними знаками, сразу переходим к коду. Ниже приведена вспомогательная функция, которую, признаюсь честно, я стырил прямо из исходников Qt (да простит меня Trolltech-Nokia):
QByteArray QByteArray_from_X509(X509 *x509)
{
	if (!x509)
		return QByteArray();
	
	// Use i2d_X509 to convert the X509 to an array.
	int length = i2d_X509(x509, 0);
	QByteArray array;
	array.resize(length);
	char *data = array.data();
	char **dataP = &data;
	unsigned char **dataPu = (unsigned char **)dataP;
	if (i2d_X509(x509, dataPu) < 0)
		return QByteArray();
	
	// Convert to Base64 - wrap at 64 characters.
	array = array.toBase64();
	QByteArray tmp;
	for (int i = 0; i < array.size() - 64; i += 64) {
		tmp += QByteArray::fromRawData(array.data() + i, 64);
		tmp += "\n";
	}
	if (int remainder = array.size() % 64) {
		tmp += QByteArray::fromRawData(array.data() + array.size() - remainder, remainder);
		tmp += "\n";
	}
	
	return "-----BEGIN CERTIFICATE-----\n" + tmp + "-----END CERTIFICATE-----\n";
}

Функция конвертирует хранилище сертификата X509 из внутреннего представления OpenSSLв QByteArray, откуда мы уже легко можем получить QSslCertificate.
Теперь, собственно, то, что касается визарда генерации ключа. Первой у нас будет отображаться страница запрашивающая данные для заполнения сертификата. Весь код класса этой страницы состоит только из конструктора:
getCertDataPage::getCertDataPage( QWidget *parent )
	: QWizardPage(parent)
{
	setTitle("Данные сертификата");
	
	QVBoxLayout * verticalLayout = new QVBoxLayout();
	QLabel * label = new QLabel(tr("Заполните необходимые данные сертификата. Все поля обязательны."));
	label->setWordWrap(true);
	verticalLayout->addWidget(label);
	
	QHBoxLayout * horizontalLayout = new QHBoxLayout();
	label = new QLabel(tr("&Пользователь:"));
	label->setMinimumSize(QSize(80, 0));
	horizontalLayout->addWidget(label);
	QLineEdit * lineEdit = new QLineEdit();
	horizontalLayout->addWidget(lineEdit);
	label->setBuddy(lineEdit);
	verticalLayout->addLayout(horizontalLayout);
	registerField("certSubject*",lineEdit);
	
	horizontalLayout = new QHBoxLayout();
	label = new QLabel(tr("&Организация:"));
	label->setMinimumSize(QSize(80, 0));
	horizontalLayout->addWidget(label);
	lineEdit = new QLineEdit();
	horizontalLayout->addWidget(lineEdit);
	label->setBuddy(lineEdit);
	verticalLayout->addLayout(horizontalLayout);
	registerField("certOrganization*",lineEdit);
	
	horizontalLayout = new QHBoxLayout();
	label = new QLabel(tr("&E-Mail:"));
	label->setMinimumSize(QSize(80, 0));
	horizontalLayout->addWidget(label);
	lineEdit = new QLineEdit();
	horizontalLayout->addWidget(lineEdit);
	label->setBuddy(lineEdit);
	verticalLayout->addLayout(horizontalLayout);
	registerField("certEMail*",lineEdit);
	
	setLayout(verticalLayout);
}

Это простой конструктор страницы, создающий три поля для ввода данных сертификата. Мы используем только три, но в вашей реализации могут быть и другие поля
Далее наш визард должен каким-то образом сгенерировать хорошую энтропию. Не буду объяснять что это значит здесь, но от того насколько она случайна (насколько хороши статистические показатели случайности) зависит надежность ключа. Мы используем для этого перемещения мыши, попросив пользователя поводить мышью из стороны в сторону. Вот здесь мы и будем использовать класс RandSeeder. Вот его реализация:
bool RandSeeder::eventFilter( QObject *obj, QEvent *event )
{
	if(event->type() == QEvent::MouseMove)
	{
		uint addition=QDateTime::currentDateTime().toTime_t();
		QMouseEvent *mEvent = static_cast<QMouseEvent *>(event);
		addition*=mEvent->globalX();
		addition*=mEvent->globalY();
		RAND_seed(&addition,sizeof(uint));
		counter++;
		emit step();
		if(counter>maximum)
			stop();
		return true;
	}
	else
		return QObject::eventFilter(obj, event);
}

void RandSeeder::startSeeding( QList<QWidget *> w, int lim )
{
	counter=0;
	if(lim<512)
		lim=512;
	maximum=lim;
	int i;
	for(i=0;i<w.count();i++)
	{
		wdgs.insert(w.at(i),w.at(i)->hasMouseTracking());
		w.at(i)->installEventFilter(this);
		w.at(i)->setMouseTracking(true);
	}
}

void RandSeeder::stop()
{
	QList<QWidget *> w=wdgs.keys();
	int i;
	bool mt;
	for(i=0;i<w.count();i++)
	{
		mt=wdgs.value(w.at(i));
		wdgs.remove(w.at(i));
		w.at(i)->setMouseTracking(mt);
		w.at(i)->removeEventFilter(this);
	}
	emit stopped();
}

Теперь посмотрим как используется класс RandSeeder на второй странице нашего визарда:
randomizePage::randomizePage( QWidget *parent )
	: QWizardPage(parent)
{
	setTitle("Генерация энтропии");
	
	setFinalPage(true);
	
	QVBoxLayout * verticalLayout = new QVBoxLayout();
	label = new QLabel(tr("Подвигайте мышью в разных случайных направлениях для генерации энтропии."));
	label->setWordWrap(true);
	verticalLayout->addWidget(label);
	progressBar = new QProgressBar();
	progressBar->setMaximum(1024);
	progressBar->setValue(0);
	verticalLayout->addWidget(progressBar);
	
	setLayout(verticalLayout);
	
	connect(&seeder, SIGNAL(step()), this, SLOT(seedStep()));
	connect(&seeder, SIGNAL(stopped()), this, SLOT(seedStopped()));
}

void randomizePage::seedStep()
{
	progressBar->setValue(progressBar->value()+1);
}

void randomizePage::cleanupPage()
{
	seeder.stop();
	progressBar->setValue(0);
}

void randomizePage::initializePage()
{
	progressBar->setValue(0);
	progressBar->setMaximum(1024);
	QList<QWidget *> wlst, chwdg;
	wlst << this << wizard() << progressBar << label;
	int i;
	chwdg=wizard()->findChildren<QWidget *>();
	for(i=0;i<chwdg.count();i++)
	{
		if(!wlst.contains(chwdg.at(i)))
			wlst.append(chwdg.at(i));
	}
	seeder.startSeeding(wlst ,1024);
}

void randomizePage::seedStopped()
{
	if(seeder.isCompleted())
		emit completeChanged();
}

bool randomizePage::isComplete() const
{
	if(seeder.isCompleted())
		return true;
	return false;
}

Наш класс RandSeeder генерирует события на начало, конец и каждый шаг подстройки энтропии. Эти события мы отлавливаем на странице визарда и показываем пользователю прогресс. Кнопка перехода к следующей странице становится доступной только по окончании подстройки, а запуск подстройки выполняется в функции initializePage.
И вот, собственно, мы готовы выполнить генерацию ключа и сертификата. Генерация ключа выполняется в функции accept визарда. Но прежде чем мы посмотрим ее, взглянем на конструктор визарда:
CertificateGeneratorWizard::CertificateGeneratorWizard(QWidget * parent)
	: QWizard(parent)
{
	setOption(QWizard::NoCancelButton, false);
	setOption(QWizard::NoDefaultButton, false);
	setOption(QWizard::CancelButtonOnLeft, true);
	addPage(new getCertDataPage);
	addPage(new randomizePage);
	
	setWindowTitle(tr("Генератор сертификата и ключа"));
}

Он предельно прост — устанавливаем некоторые опции (по вкусу, который может быть другим у вас), добавляем наши страницы и устанавливаем заголовок.
И, наконец, наш долгожданный генератор:
void CertificateGeneratorWizard::accept()
{
	EVP_PKEY *pk;
	RSA *rsa;
	X509 *x;
	X509_NAME *name=NULL;
	bool ok;
	//parameters
	int bits=4096;
	long serial=57;
	long days=1895;
	
	setCursor(Qt::WaitCursor);
	//create private key
	pk=EVP_PKEY_new();
	rsa=RSA_generate_key(bits,RSA_F4,NULL,NULL);
	EVP_PKEY_assign_RSA(pk,rsa);
	{
		//save it to QSslKey
		BIO *bio = BIO_new(BIO_s_mem());
		PEM_write_bio_RSAPrivateKey(bio, rsa, (const EVP_CIPHER *)0, NULL, 0, 0, 0);
		QByteArray pem;
		char *data;
		long size = BIO_get_mem_data(bio, &data);
		pem = QByteArray(data, size);
		BIO_free(bio);
		pkey=QSslKey(pem,QSsl::Rsa);
		ok=!pkey.isNull();
	}
	x=X509_new();
	X509_set_version(x,2);
	ASN1_INTEGER_set(X509_get_serialNumber(x),serial);
	X509_gmtime_adj(X509_get_notBefore(x),(long)60*60*24*(-2));
	X509_gmtime_adj(X509_get_notAfter(x),(long)60*60*24*days);
	X509_set_pubkey(x,pk);
	
	name=X509_get_subject_name(x);
	
	QVariant fldVal=field("certSubject");
	X509_NAME_add_entry_by_txt(name,"CN", MBSTRING_ASC,
							   fldVal.toString().toLatin1().constData(), -1, -1, 0);
	fldVal=field("certOrganization");
	if(!fldVal.isNull())
		X509_NAME_add_entry_by_txt(name,"O", MBSTRING_ASC,
								   fldVal.toString().toLatin1().constData(), -1, -1, 0);
	
	fldVal=field("certEMail");
	if(!fldVal.isNull())
	{
		X509_NAME_add_entry_by_txt(name,"emailAddress", MBSTRING_ASC,
								   fldVal.toString().toLatin1().constData(), -1, -1, 0);
	}
	
	X509_set_issuer_name(x,name);
	
	X509_sign(x,pk,EVP_md5());
	{
		QByteArray crt=QByteArray_from_X509(x);
		cert=QSslCertificate(crt);
		ok=cert.isValid();
	}
	
	ok=isPrivateKeyCorrespondsToCertificate(cert,pkey);
	
	QWizard::accept();
}

Сначала мы генерируем приватный ключ, а затем сертификат. Обратите внимание на имена полей сертификата. Если вы захотите генерировать сертификаты с большим количеством полей, то здесь вам нужно будет их заполнять. Смотрите документацию OpenSSL, там где-то есть список возможных полей.
Ну вот и всё.
Tags:
Hubs:
+3
Comments0

Articles