mumble-voip_mumble/src/mumble/Cert.cpp

572 lines
15 KiB
C++

// Copyright 2009-2023 The Mumble Developers. All rights reserved.
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
// Ignore old-style casts for the whole file.
// We can't use push/pop. They were implemented in GCC 4.6,
// but we still build with GCC 4.2 for the legacy OS X Universal
// build.
#if defined(__GNUC__)
# pragma GCC diagnostic ignored "-Wold-style-cast"
#endif
#include "Cert.h"
#include "Accessibility.h"
#include "SelfSignedCertificate.h"
#include "Utils.h"
#include "Global.h"
#include <QTimer>
#include <QtCore/QUrl>
#include <QtGui/QDesktopServices>
#include <QtWidgets/QFileDialog>
#include <QtWidgets/QToolTip>
#include <openssl/evp.h>
#include <openssl/pkcs12.h>
#include <openssl/x509.h>
#define SSL_STRING(x) QString::fromLatin1(x).toUtf8().data()
CertView::CertView(QWidget *p) : AccessibleQGroupBox(p) {
QGridLayout *grid = new QGridLayout(this);
QLabel *l;
l = new QLabel(tr("Name"));
grid->addWidget(l, 0, 0, 1, 1, Qt::AlignLeft);
qlSubjectName = new QLabel();
qlSubjectName->setTextFormat(Qt::PlainText);
qlSubjectName->setWordWrap(true);
grid->addWidget(qlSubjectName, 0, 1, 1, 1);
l = new QLabel(tr("Email"));
grid->addWidget(l, 1, 0, 1, 1, Qt::AlignLeft);
qlSubjectEmail = new QLabel();
qlSubjectEmail->setTextFormat(Qt::PlainText);
qlSubjectEmail->setWordWrap(true);
grid->addWidget(qlSubjectEmail, 1, 1, 1, 1);
l = new QLabel(tr("Issuer"));
grid->addWidget(l, 2, 0, 1, 1, Qt::AlignLeft);
qlIssuerName = new QLabel();
qlIssuerName->setTextFormat(Qt::PlainText);
qlIssuerName->setWordWrap(true);
grid->addWidget(qlIssuerName, 2, 1, 1, 1);
l = new QLabel(tr("Expiry Date"));
grid->addWidget(l, 3, 0, 1, 1, Qt::AlignLeft);
qlExpiry = new QLabel();
qlExpiry->setWordWrap(true);
grid->addWidget(qlExpiry, 3, 1, 1, 1);
grid->setColumnStretch(1, 1);
updateAccessibleText();
}
void CertView::setCert(const QList< QSslCertificate > &cert) {
qlCert = cert;
if (qlCert.isEmpty()) {
qlSubjectName->setText(QString());
qlSubjectEmail->setText(QString());
qlIssuerName->setText(QString());
qlExpiry->setText(QString());
} else {
QSslCertificate qscCert = qlCert.at(0);
const QStringList &names = qscCert.subjectInfo(QSslCertificate::CommonName);
QString name;
if (names.count() > 0) {
name = names.at(0);
}
QStringList emails = qscCert.subjectAlternativeNames().values(QSsl::EmailEntry);
QString tmpName = name;
tmpName = tmpName.replace(QLatin1String("\\x"), QLatin1String("%"));
tmpName = QUrl::fromPercentEncoding(tmpName.toLatin1());
qlSubjectName->setText(tmpName);
if (emails.count() > 0)
qlSubjectEmail->setText(emails.join(QLatin1String("\n")));
else
qlSubjectEmail->setText(tr("(none)"));
const auto expiryDateStr = QLocale::system().toString(qscCert.expiryDate(), QLocale::ShortFormat);
if (qscCert.expiryDate() <= QDateTime::currentDateTime())
qlExpiry->setText(
QString::fromLatin1("<font color=\"red\"><b>%1</b></font>").arg(expiryDateStr.toHtmlEscaped()));
else
qlExpiry->setText(expiryDateStr);
if (qlCert.count() > 1)
qscCert = qlCert.last();
const QStringList &issuerNames = qscCert.issuerInfo(QSslCertificate::CommonName);
QString issuerName;
if (issuerNames.count() > 0) {
issuerName = issuerNames.at(0);
}
qlIssuerName->setText((issuerName == name) ? tr("Self-signed") : issuerName);
}
updateAccessibleText();
}
CertWizard::CertWizard(QWidget *p) : QWizard(p) {
setupUi(this);
Mumble::Accessibility::fixWizardButtonLabels(this);
setOption(QWizard::NoCancelButton, false);
qwpExport->setCommitPage(true);
qwpExport->setComplete(false);
qlPasswordNotice->setVisible(false);
m_overrideFilter = new OverrideTabOrderFilter(this, this);
installEventFilter(m_overrideFilter);
connect(this, &CertWizard::currentIdChanged, this, &CertWizard::showPage);
QTimer::singleShot(0, [this] { this->showPage(0); });
}
int CertWizard::nextId() const {
switch (currentId()) {
case 0: { // Welcome
if (qrbQuick->isChecked())
return 5;
else if (qrbCreate->isChecked())
return 1;
else if (qrbImport->isChecked())
return 2;
else if (qrbExport->isChecked())
return 3;
return -1;
}
case 2: // Import
if (validateCert(kpCurrent))
return 4;
else
return 5;
case 4: // Replace
if (qrbCreate->isChecked())
return 3;
if (qrbImport->isChecked())
return 5;
return -1;
case 3: // Export
if (qrbCreate->isChecked())
return 5;
else
return -1;
case 1: // New
if (validateCert(kpCurrent))
return 4;
else
return 3;
}
return -1;
}
void CertWizard::showPage(int pageid) {
if (pageid == -1) {
return;
}
setFocus(Qt::ActiveWindowFocusReason);
QWidget *selectedWidget = Mumble::Accessibility::getFirstFocusableChild(currentPage());
if (selectedWidget) {
m_overrideFilter->focusTarget = selectedWidget;
} else {
m_overrideFilter->focusTarget = button(QWizard::NextButton);
}
}
void CertWizard::initializePage(int id) {
if (id == 0) {
kpCurrent = kpNew = Global::get().s.kpCertificate;
if (validateCert(kpCurrent)) {
qrbQuick->setEnabled(false);
qrbExport->setEnabled(true);
cvWelcome->setCert(kpCurrent.first);
cvWelcome->setVisible(true);
} else {
qrbQuick->setEnabled(true);
qrbExport->setEnabled(false);
cvWelcome->setVisible(false);
qrbQuick->setChecked(true);
}
}
if (id == 3) {
cvExport->setCert(kpNew.first);
}
if (id == 4) {
cvNew->setCert(kpNew.first);
cvCurrent->setCert(kpCurrent.first);
}
if (id == 2) {
on_qleImportFile_textChanged(qleImportFile->text());
}
QWizard::initializePage(id);
}
bool CertWizard::validateCurrentPage() {
if (currentPage() == qwpNew) {
QRegExp ereg(QLatin1String("(^$)|((.+)@(.+))"), Qt::CaseInsensitive, QRegExp::RegExp2);
if (!ereg.exactMatch(qleEmail->text())) {
qlError->setText(tr("Unable to validate email.<br />Enter a valid (or blank) email to continue."));
qwpNew->setComplete(false);
return false;
} else {
kpNew = generateNewCert(qleName->text(), qleEmail->text());
if (!validateCert(kpNew)) {
qlError->setText(tr("There was an error generating your certificate.<br />Please try again."));
return false;
}
}
}
if (currentPage() == qwpExport) {
QByteArray qba = exportCert(kpNew);
if (qba.isEmpty()) {
QToolTip::showText(qleExportFile->mapToGlobal(QPoint(0, 0)),
tr("Your certificate and key could not be exported to PKCS#12 format. There might be an "
"error in your certificate."),
qleExportFile);
return false;
}
QFile f(qleExportFile->text());
if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Unbuffered)) {
QToolTip::showText(qleExportFile->mapToGlobal(QPoint(0, 0)),
tr("The file could not be opened for writing. Please use another file."), qleExportFile);
return false;
}
if (!f.setPermissions(QFile::ReadOwner | QFile::WriteOwner)) {
QToolTip::showText(qleExportFile->mapToGlobal(QPoint(0, 0)),
tr("The file's permissions could not be set. No certificate and key has been written. "
"Please use another file."),
qleExportFile);
return false;
}
qint64 written = f.write(qba);
f.close();
if (written != qba.length()) {
QToolTip::showText(qleExportFile->mapToGlobal(QPoint(0, 0)),
tr("The file could not be written successfully. Please use another file."),
qleExportFile);
return false;
}
}
if (currentPage() == qwpImport) {
QFile f(qleImportFile->text());
if (!f.open(QIODevice::ReadOnly | QIODevice::Unbuffered)) {
QToolTip::showText(qleImportFile->mapToGlobal(QPoint(0, 0)),
tr("The file could not be opened for reading. Please use another file."), qleImportFile);
return false;
}
QByteArray qba = f.readAll();
f.close();
if (qba.isEmpty()) {
QToolTip::showText(qleImportFile->mapToGlobal(QPoint(0, 0)),
tr("The file is empty or could not be read. Please use another file."), qleImportFile);
return false;
}
QPair< QList< QSslCertificate >, QSslKey > imp = importCert(qba, qlePassword->text());
if (!validateCert(imp)) {
QToolTip::showText(qleImportFile->mapToGlobal(QPoint(0, 0)),
tr("The file did not contain a valid certificate and key. Please use another file."),
qleImportFile);
return false;
}
kpNew = imp;
}
if (currentPage() == qwpFinish) {
Global::get().s.kpCertificate = kpNew;
}
return QWizard::validateCurrentPage();
}
void CertWizard::on_qleEmail_textChanged(const QString &) {
qwpNew->setComplete(true);
}
void CertWizard::on_qpbExportFile_clicked() {
QString fname =
QFileDialog::getSaveFileName(this, tr("Select file to export certificate to"), qleExportFile->text(),
QLatin1String("PKCS12 (*.p12 *.pfx *.pkcs12);;All (*)"));
if (!fname.isNull()) {
QFileInfo fi(fname);
if (fi.suffix().isEmpty())
fname += QLatin1String(".p12");
qleExportFile->setText(QDir::toNativeSeparators(fname));
}
}
void CertWizard::on_qleExportFile_textChanged(const QString &text) {
if (text.isEmpty()) {
qwpExport->setComplete(false);
return;
}
QString fname = QDir::fromNativeSeparators(text);
QFile f(fname);
QFileInfo fi(f);
if (fi.exists()) {
if (fi.isWritable()) {
qwpExport->setComplete(f.open(QIODevice::WriteOnly | QIODevice::Append));
return;
}
} else {
if (f.open(QIODevice::WriteOnly)) {
if (f.remove()) {
qwpExport->setComplete(true);
return;
}
}
}
qwpExport->setComplete(false);
}
void CertWizard::on_qpbImportFile_clicked() {
QString fname =
QFileDialog::getOpenFileName(this, tr("Select file to import certificate from"), qleImportFile->text(),
QLatin1String("PKCS12 (*.p12 *.pfx *.pkcs12);;All (*)"));
if (!fname.isNull()) {
qleImportFile->setText(QDir::toNativeSeparators(fname));
}
}
void CertWizard::on_qleImportFile_textChanged(const QString &text) {
if (text.isEmpty()) {
qlePassword->clear();
qlePassword->setEnabled(false);
qlPassword->setEnabled(false);
qlPasswordNotice->clear();
qlPasswordNotice->setVisible(false);
qwpImport->setComplete(false);
return;
}
QString fname = QDir::fromNativeSeparators(text);
QFile f(fname);
if (f.open(QIODevice::ReadOnly)) {
QByteArray qba = f.readAll();
QPair< QList< QSslCertificate >, QSslKey > imp = importCert(qba, qlePassword->text());
if (validateCert(imp)) {
qlePassword->setEnabled(false);
qlPassword->setEnabled(false);
qlPasswordNotice->clear();
qlPasswordNotice->setVisible(false);
cvImport->setCert(imp.first);
qwpImport->setComplete(true);
return;
} else {
qlePassword->setEnabled(true);
qlPassword->setEnabled(true);
qlPasswordNotice->setText(tr("Unable to import. Missing password or incompatible file type."));
qlPasswordNotice->setVisible(true);
}
} else {
qlePassword->clear();
qlePassword->setEnabled(false);
qlPassword->setEnabled(false);
qlPasswordNotice->clear();
qlPasswordNotice->setVisible(false);
}
cvImport->setCert(QList< QSslCertificate >());
qwpImport->setComplete(false);
}
void CertWizard::on_qlePassword_textChanged(const QString &) {
on_qleImportFile_textChanged(qleImportFile->text());
}
void CertWizard::on_qlIntroText_linkActivated(const QString &url) {
QDesktopServices::openUrl(QUrl(url));
}
bool CertWizard::validateCert(const Settings::KeyPair &kp) {
bool valid = !kp.second.isNull() && !kp.first.isEmpty();
foreach (const QSslCertificate &cert, kp.first)
valid = valid && !cert.isNull();
return valid;
}
Settings::KeyPair CertWizard::generateNewCert(QString qsname, const QString &qsemail) {
QSslCertificate qscCert;
QSslKey qskKey;
// Ignore return value.
// The method sets qscCert and qskKey to null values if it fails.
SelfSignedCertificate::generateMumbleCertificate(qsname, qsemail, qscCert, qskKey);
QList< QSslCertificate > qlCert;
qlCert << qscCert;
return Settings::KeyPair(qlCert, qskKey);
}
Settings::KeyPair CertWizard::importCert(QByteArray data, const QString &pw) {
X509 *x509 = nullptr;
EVP_PKEY *pkey = nullptr;
PKCS12 *pkcs = nullptr;
BIO *mem = nullptr;
STACK_OF(X509) *certs = nullptr;
Settings::KeyPair kp;
int ret = 0;
mem = BIO_new_mem_buf(data.data(), data.size());
Q_UNUSED(BIO_set_close(mem, BIO_NOCLOSE));
pkcs = d2i_PKCS12_bio(mem, nullptr);
if (pkcs) {
ret = PKCS12_parse(pkcs, nullptr, &pkey, &x509, &certs);
if (pkcs && !pkey && !x509 && !pw.isEmpty()) {
if (certs) {
if (ret)
sk_X509_free(certs);
certs = nullptr;
}
ret = PKCS12_parse(pkcs, pw.toUtf8().constData(), &pkey, &x509, &certs);
}
if (pkey && x509 && X509_check_private_key(x509, pkey)) {
unsigned char *dptr;
QByteArray key, crt;
key.resize(i2d_PrivateKey(pkey, nullptr));
dptr = reinterpret_cast< unsigned char * >(key.data());
i2d_PrivateKey(pkey, &dptr);
crt.resize(i2d_X509(x509, nullptr));
dptr = reinterpret_cast< unsigned char * >(crt.data());
i2d_X509(x509, &dptr);
QSslCertificate qscCert = QSslCertificate(crt, QSsl::Der);
QSslKey qskKey = QSslKey(key, QSsl::Rsa, QSsl::Der);
QList< QSslCertificate > qlCerts;
qlCerts << qscCert;
if (certs) {
for (int i = 0; i < sk_X509_num(certs); ++i) {
X509 *c = sk_X509_value(certs, i);
crt.resize(i2d_X509(c, nullptr));
dptr = reinterpret_cast< unsigned char * >(crt.data());
i2d_X509(c, &dptr);
QSslCertificate cert = QSslCertificate(crt, QSsl::Der);
qlCerts << cert;
}
}
bool valid = !qskKey.isNull();
foreach (const QSslCertificate &cert, qlCerts)
valid = valid && !cert.isNull();
if (valid)
kp = Settings::KeyPair(qlCerts, qskKey);
}
}
if (ret) {
if (pkey)
EVP_PKEY_free(pkey);
if (x509)
X509_free(x509);
if (certs)
sk_X509_free(certs);
}
if (pkcs)
PKCS12_free(pkcs);
if (mem)
BIO_free(mem);
return kp;
}
QByteArray CertWizard::exportCert(const Settings::KeyPair &kp) {
X509 *x509 = nullptr;
EVP_PKEY *pkey = nullptr;
PKCS12 *pkcs = nullptr;
BIO *mem = nullptr;
STACK_OF(X509) *certs = sk_X509_new_null();
const unsigned char *p;
char *data = nullptr;
if (kp.first.isEmpty())
return QByteArray();
QByteArray crt = kp.first.at(0).toDer();
QByteArray key = kp.second.toDer();
QByteArray qba;
p = reinterpret_cast< const unsigned char * >(key.constData());
pkey = d2i_AutoPrivateKey(nullptr, &p, key.length());
if (pkey) {
p = reinterpret_cast< const unsigned char * >(crt.constData());
x509 = d2i_X509(nullptr, &p, crt.length());
if (x509 && X509_check_private_key(x509, pkey)) {
X509_keyid_set1(x509, nullptr, 0);
X509_alias_set1(x509, nullptr, 0);
QList< QSslCertificate > qlCerts = kp.first;
qlCerts.removeFirst();
foreach (const QSslCertificate &cert, qlCerts) {
X509 *c = nullptr;
crt = cert.toDer();
p = reinterpret_cast< const unsigned char * >(crt.constData());
c = d2i_X509(nullptr, &p, crt.length());
if (c)
sk_X509_push(certs, c);
}
pkcs = PKCS12_create(SSL_STRING(""), SSL_STRING("Mumble Identity"), pkey, x509, certs, -1, -1, 0, 0, 0);
if (pkcs) {
long size;
mem = BIO_new(BIO_s_mem());
i2d_PKCS12_bio(mem, pkcs);
Q_UNUSED(BIO_flush(mem));
size = BIO_get_mem_data(mem, &data);
qba = QByteArray(data, static_cast< int >(size));
}
}
}
if (pkey)
EVP_PKEY_free(pkey);
if (x509)
X509_free(x509);
if (pkcs)
PKCS12_free(pkcs);
if (mem)
BIO_free(mem);
if (certs)
sk_X509_free(certs);
return qba;
}
#undef SSL_STRING