diff options
| author | Anton <anton@picapica.im> | 2026-09-21 03:43:27 +0400 |
|---|---|---|
| committer | Anton <anton@picapica.im> | 2026-09-21 03:43:27 +0400 |
| commit | 1f7c350a83d6a515919255dc72266fd4e143cd1d (patch) | |
| tree | a651fc8c7e8e3703e6076ab83e5407fccfe1936f | |
| parent | 3ef95aee732391bf31220f3c7393771f52e55427 (diff) | |
| download | pica-pica-1f7c350a83d6a515919255dc72266fd4e143cd1d.tar.gz pica-pica-1f7c350a83d6a515919255dc72266fd4e143cd1d.zip | |
pica-client: get rid of running the openssl binary, use in-process library calls instead
| -rw-r--r-- | Makefile.am | 5 | ||||
| -rw-r--r-- | configure.ac | 5 | ||||
| -rw-r--r-- | debian/control | 2 | ||||
| -rw-r--r-- | pica-client/accountswindow.cpp | 4 | ||||
| -rw-r--r-- | pica-client/dhparam.cpp | 11 | ||||
| -rw-r--r-- | pica-client/dhparam.h | 6 | ||||
| -rw-r--r-- | pica-client/dialogs/registeraccountdialog.cpp | 8 | ||||
| -rw-r--r-- | pica-client/dialogs/registeraccountdialog.h | 8 | ||||
| -rw-r--r-- | pica-client/dialogs/viewcertdialog.cpp | 4 | ||||
| -rw-r--r-- | pica-client/main.cpp | 2 | ||||
| -rw-r--r-- | pica-client/openssllib.cpp | 846 | ||||
| -rw-r--r-- | pica-client/openssllib.h | 81 | ||||
| -rw-r--r-- | pica-client/openssltool.cpp | 222 | ||||
| -rw-r--r-- | pica-client/openssltool.h | 45 | ||||
| -rw-r--r-- | pica-client/pica-client.pro | 4 | ||||
| -rw-r--r-- | pica-client/skynet.cpp | 4 |
16 files changed, 959 insertions, 298 deletions
diff --git a/Makefile.am b/Makefile.am index ca53df6..e0ed1cd 100644 --- a/Makefile.am +++ b/Makefile.am @@ -78,8 +78,8 @@ pica-client/msguirouter.cpp \ pica-client/msguirouter.h \ pica-client/nodes.cpp \ pica-client/nodes.h \ -pica-client/openssltool.cpp \ -pica-client/openssltool.h \ +pica-client/openssllib.cpp \ +pica-client/openssllib.h \ pica-client/picaactioncenter.cpp \ pica-client/picaactioncenter.h \ pica-client/pica-client.pro \ @@ -152,6 +152,7 @@ moc_contactlistwidget.cpp \ moc_forgedcertdialog.cpp \ moc_mainwindow.cpp \ moc_msguirouter.cpp \ +moc_openssllib.cpp \ moc_picaactioncenter.cpp \ moc_picasystray.cpp \ moc_registeraccountdialog.cpp \ diff --git a/configure.ac b/configure.ac index 2a4ab03..e6639bb 100644 --- a/configure.ac +++ b/configure.ac @@ -167,11 +167,6 @@ AC_CHECK_PROGS([MOC], [$QT_MOC_NAMES], [notfound]) AS_IF([test "x$UIC" = xnotfound],[AC_MSG_ERROR(["uic program not found. Please install Qt development package"])]) AS_IF([test "x$MOC" = xnotfound],[AC_MSG_ERROR(["moc program not found. Please install Qt development package"])]) -#check for openssl command line tool - -AC_CHECK_PROG([OPENSSL_TOOL], [openssl], [yes], [no]) -AS_IF([test "x$OPENSSL_TOOL" != xyes],[AC_MSG_WARN(["openssl command line tool not found. This is a runtime dependency for pica-client!"])]) - #check for FFmpeg libraries PKG_CHECK_MODULES([FFMPEG],["libavcodec libavdevice libavformat libavutil libswresample libswscale"]) AC_SUBST([FFMPEG_CFLAGS]) diff --git a/debian/control b/debian/control index 54fbdbb..e9ad93a 100644 --- a/debian/control +++ b/debian/control @@ -29,7 +29,7 @@ Homepage: http://picapica.im Package: pica-client Architecture: any -Depends: ${shlibs:Depends}, ${misc:Depends}, alsa-utils, libqt5sql5-sqlite, openssl +Depends: ${shlibs:Depends}, ${misc:Depends}, alsa-utils, libqt5sql5-sqlite Recommends: va-driver-all Description: Pica Pica instant messaging client Client for secure distributed instant messaging system diff --git a/pica-client/accountswindow.cpp b/pica-client/accountswindow.cpp index bf8dc76..aeae51f 100644 --- a/pica-client/accountswindow.cpp +++ b/pica-client/accountswindow.cpp @@ -23,7 +23,7 @@ #include "dialogs/showpicaiddialog.h" #include "../PICA_client.h" #include "skynet.h" -#include "openssltool.h" +#include "openssllib.h" #include <QMessageBox> #include <QApplication> #include <cstring> @@ -146,7 +146,7 @@ void AccountsWindow::CreateAccount(QString CertFilename, QString PkeyFilename, b return; } - QString name = OpenSSLTool::NameFromCertFile(CertFilename); + QString name = OpenSSLLib::NameFromCertFile(CertFilename); Accounts::AccountRecord rec; QString certfilename, pkeyfilename; diff --git a/pica-client/dhparam.cpp b/pica-client/dhparam.cpp index 3708334..c2d8b56 100644 --- a/pica-client/dhparam.cpp +++ b/pica-client/dhparam.cpp @@ -76,7 +76,7 @@ bool DHParam::StartNewDHParamGeneration(QString *errormsg) QFile::remove(tempdhfile); return dhparamgenerator -> osslt.GenDHParamSignal(PICA_CLIENT_DHPARAMBITS, - tempdhfile, dhparamgenerator, SLOT(DHParamGenFinished(int, QProcess::ExitStatus))); + tempdhfile, dhparamgenerator, SLOT(DHParamGenFinished(int))); } QString DHParam::GetDHParamFilename() @@ -92,7 +92,7 @@ QString DHParam::GetDHParamFilename() return current_dhparam_file; } -void DHParam::DHParamGenFinished(int retval, QProcess::ExitStatus) +void DHParam::DHParamGenFinished(int retval) { if (retval == 0) { @@ -104,7 +104,12 @@ void DHParam::DHParamGenFinished(int retval, QProcess::ExitStatus) current_dhparam_file = config_dir + QDir::separator() + PICA_CLIENT_DHPARAMFILE; - delete dhparamgenerator; + /* + * deleteLater, not delete: we are inside a slot the generator's own + * OpenSSLLib member is emitting to, and it still has to return + * through it. + */ + dhparamgenerator->deleteLater(); dhparamgenerator = NULL; } diff --git a/pica-client/dhparam.h b/pica-client/dhparam.h index ce49f44..930c277 100644 --- a/pica-client/dhparam.h +++ b/pica-client/dhparam.h @@ -20,7 +20,7 @@ #include <QObject> #include <QString> #include "globals.h" -#include "openssltool.h" +#include "openssllib.h" class DHParam : public QObject { @@ -42,10 +42,10 @@ private: static DHParam *dhparamgenerator; static QString current_dhparam_file; - OpenSSLTool osslt; + OpenSSLLib osslt; private slots: - void DHParamGenFinished(int retval, QProcess::ExitStatus); + void DHParamGenFinished(int retval); }; #endif // DHPARAM_H diff --git a/pica-client/dialogs/registeraccountdialog.cpp b/pica-client/dialogs/registeraccountdialog.cpp index ff77951..cf92a06 100644 --- a/pica-client/dialogs/registeraccountdialog.cpp +++ b/pica-client/dialogs/registeraccountdialog.cpp @@ -178,10 +178,10 @@ void RegisterAccountDialog::Register() btRegister->setEnabled(false); ost.GenRSAKeySignal(config_dir + QDir::separator() + "privkey.pem", cbSetPassword->isChecked(), - password->text(), rand, this, SLOT(stageSignCert(int, QProcess::ExitStatus))); + password->text(), rand, this, SLOT(stageSignCert(int))); } -void RegisterAccountDialog::stageSignCert(int retval, QProcess::ExitStatus) +void RegisterAccountDialog::stageSignCert(int retval) { if (retval != 0) { @@ -196,10 +196,10 @@ void RegisterAccountDialog::stageSignCert(int retval, QProcess::ExitStatus) ost.GenCertSignal(config_dir + QDir::separator() + "cert.pem", config_dir + QDir::separator() + "privkey.pem", password->text(), nickname->text(), - this, SLOT(stageFinished(int, QProcess::ExitStatus))); + this, SLOT(stageFinished(int))); } -void RegisterAccountDialog::stageFinished(int retval, QProcess::ExitStatus) +void RegisterAccountDialog::stageFinished(int retval) { if (retval != 0) { diff --git a/pica-client/dialogs/registeraccountdialog.h b/pica-client/dialogs/registeraccountdialog.h index 447d181..2e4fe22 100644 --- a/pica-client/dialogs/registeraccountdialog.h +++ b/pica-client/dialogs/registeraccountdialog.h @@ -23,7 +23,7 @@ #include <QLabel> #include <QCheckBox> #include <QRegExpValidator> -#include "../openssltool.h" +#include "../openssllib.h" class RegisterAccountDialog : public QDialog { @@ -46,7 +46,7 @@ private: QCheckBox *cbUseDevRandom; QRegExpValidator *vld; - OpenSSLTool ost; + OpenSSLLib ost; QByteArray cert_buf; QString CertFilename_; @@ -59,8 +59,8 @@ private slots: void setPasswordClick(); void useDevRandomClick(); - void stageSignCert(int retval, QProcess::ExitStatus); - void stageFinished(int retval, QProcess::ExitStatus); + void stageSignCert(int retval); + void stageFinished(int retval); }; #endif // REGISTERACCOUNTDIALOG_H diff --git a/pica-client/dialogs/viewcertdialog.cpp b/pica-client/dialogs/viewcertdialog.cpp index f2cc918..6237ac4 100644 --- a/pica-client/dialogs/viewcertdialog.cpp +++ b/pica-client/dialogs/viewcertdialog.cpp @@ -15,7 +15,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ #include "viewcertdialog.h" -#include "../openssltool.h" +#include "../openssllib.h" #include <QVBoxLayout> #include <QMessageBox> #include <QFile> @@ -53,7 +53,7 @@ ViewCertDialog::ViewCertDialog(QWidget *parent) : void ViewCertDialog::SetCert(QString cert_pem) { - cert_text->setPlainText(OpenSSLTool::CertTextFromString(cert_pem)); + cert_text->setPlainText(OpenSSLLib::CertTextFromString(cert_pem)); } void ViewCertDialog::OK() diff --git a/pica-client/main.cpp b/pica-client/main.cpp index 3972ab8..3693276 100644 --- a/pica-client/main.cpp +++ b/pica-client/main.cpp @@ -466,7 +466,7 @@ static bool create_config_dir() /*if (!QFile::exists(config_dir + QDir::separator() + PICA_CLIENT_DHPARAMFILE)) { - OpenSSLTool osslt; + OpenSSLLib osslt; msgBox.setText(QString(QObject::tr("Diffie-Hellman parameters will be generated. Please wait. This process can take several minutes..."))); msgBox.exec(); diff --git a/pica-client/openssllib.cpp b/pica-client/openssllib.cpp new file mode 100644 index 0000000..1dc5ad1 --- /dev/null +++ b/pica-client/openssllib.cpp @@ -0,0 +1,846 @@ +/* + (c) Copyright 2012 - 2018 Anton Sviridenko + https://picapica.im + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, version 3. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ +#include "openssllib.h" +#include "../PICA_security.h" + +#include <openssl/asn1.h> +#include <openssl/bio.h> +#include <openssl/bn.h> +#include <openssl/err.h> +#include <openssl/evp.h> +#include <openssl/pem.h> +#include <openssl/rand.h> +#include <openssl/rsa.h> +#include <openssl/x509.h> +#include <openssl/x509v3.h> +#include <openssl/dh.h> + +#include <QFile> +#include <QRegExp> +#include <QThread> + +#define REGEXOLD "CN = [0-9]+\\#([^\\#\\$&\"\'=\\(\\)\\\\/\\|`!<>\\{\\}\\[\\]\\+\r\n]+)" +#define REGEXNEW "CN = ([^\\#\\$&\"\'=\\(\\)\\\\/\\|`!<>\\{\\}\\[\\]\\+\r\n]+)" + +/* "-days 3680", as it was spelled on the openssl command line. */ +#define PICA_CERT_VALIDITY_DAYS 3680 + +/* + * The serial number width "openssl req -x509" picks when it is not given one + * (SERIAL_RAND_BITS in apps/lib/apps.c). + */ +#define PICA_CERT_SERIAL_BITS 159 + +#if OPENSSL_VERSION_NUMBER < 0x10100000L +#define X509_getm_notBefore(x) X509_get_notBefore(x) +#define X509_getm_notAfter(x) X509_get_notAfter(x) +#endif + +/* + * "oneline,-esc_msb,utf8" and "multiline,-esc_msb,utf8", the two -nameopt + * arguments OpenSSLTool used, worked out the way the command line program's + * option parser works them out: "oneline" and "multiline" clear every other + * bit before setting their own, the two that follow set and clear a single + * bit each. + */ +static const unsigned long ONELINE_NAMEOPT = + (XN_FLAG_ONELINE & ~ASN1_STRFLGS_ESC_MSB) | ASN1_STRFLGS_UTF8_CONVERT; + +static const unsigned long MULTILINE_NAMEOPT = + (XN_FLAG_MULTILINE & ~ASN1_STRFLGS_ESC_MSB) | ASN1_STRFLGS_UTF8_CONVERT; + +static QString BioToString(BIO *bio) +{ + char *data = 0; + long len; + + len = BIO_get_mem_data(bio, &data); + + if (len <= 0 || !data) + return QString(); + + return QString::fromUtf8(data, (int)len); +} + +static QByteArray BioToByteArray(BIO *bio) +{ + char *data = 0; + long len; + + len = BIO_get_mem_data(bio, &data); + + if (len <= 0 || !data) + return QByteArray(); + + return QByteArray(data, (int)len); +} + +static X509 *CertFromPEM(const QByteArray &cert_pem) +{ + BIO *mem; + X509 *cert; + + if (cert_pem.isEmpty()) + return 0; + + mem = BIO_new_mem_buf((void*)cert_pem.constData(), cert_pem.size()); + + if (!mem) + return 0; + + cert = PEM_read_bio_X509(mem, NULL, NULL, NULL); + + BIO_free(mem); + ERR_clear_error(); + + return cert; +} + +/* + * The whole file is read through QFile rather than handed to BIO_new_file(): + * config_dir can hold characters that do not survive the trip through the + * local 8 bit encoding a C file name would have to be, and Qt already knows + * how to open those paths on every platform we build for. + */ +static QByteArray ReadWholeFile(const QString &name) +{ + QFile f(name); + + if (!f.open(QIODevice::ReadOnly)) + return QByteArray(); + + return f.readAll(); +} + +/* + * The name as the "-subject -nameopt oneline,-esc_msb,utf8" output of the + * command line program spelled it, prefix and trailing newline included, so + * that the two regular expressions below see exactly what they used to see. + */ +static QString SubjectLine(X509 *cert) +{ + BIO *mem; + QString line; + + mem = BIO_new(BIO_s_mem()); + + if (!mem) + return QString(); + + BIO_puts(mem, "subject="); + X509_NAME_print_ex(mem, X509_get_subject_name(cert), 0, ONELINE_NAMEOPT); + BIO_puts(mem, "\n"); + + line = BioToString(mem); + + BIO_free(mem); + + return line; +} + +static QString NameFromSubjectLine(QString subject_line) +{ + QRegExp rxOld(REGEXOLD); + QRegExp rx(REGEXNEW); + + QString name = subject_line; + + if (name.contains(rxOld)) + { + name = rxOld.cap(1); + } + else if (name.contains(rx)) + { + name = rx.cap(1); + } + + return name; +} + +/* + * The generating half of the class. RSA key generation takes seconds and DH + * parameter generation at PICA_CLIENT_DHPARAMBITS takes minutes, and the + * thread calling is the one drawing the dialog that shows the progress, so + * the work happens here instead. + * + * No Q_OBJECT: the only signal needed is QThread::finished(), which QThread + * already declares. + */ +class OpenSSLLibWorker : public QThread +{ +public: + enum Job + { + JobGenRSAKey, + JobGenCert, + JobGenDHParam + }; + + OpenSSLLibWorker(Job job, QObject *parent) + : QThread(parent), setpassword_(false), numbits_(0), job_(job), retval_(1) + { + } + + /* Set by the caller before start(), read by it after finished(). */ + QString keyfile_; + QString certfile_; + QString outfile_; + QString password_; + QString rand_; + QString subject_; + bool setpassword_; + quint32 numbits_; + + int retval() const + { + return retval_; + } + + QString errors() const + { + return errors_; + } + +protected: + void run(); + +private: + void RunGenRSAKey(); + void RunGenCert(); + void RunGenDHParam(); + + bool WriteFile(const QString &name, const QByteArray &data); + void Fail(const char *what); + + Job job_; + QString errors_; + int retval_; +}; + +void OpenSSLLibWorker::Fail(const char *what) +{ + BIO *mem = BIO_new(BIO_s_mem()); + + errors_ += QString::fromLatin1(what); + errors_ += QLatin1String("\n"); + + if (mem) + { + ERR_print_errors(mem); + errors_ += BioToString(mem); + BIO_free(mem); + } + + ERR_clear_error(); + + retval_ = 1; +} + +bool OpenSSLLibWorker::WriteFile(const QString &name, const QByteArray &data) +{ + QFile f(name); + + if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate)) + { + errors_ += QString("Failed to open %1 for writing: %2\n").arg(name).arg(f.errorString()); + retval_ = 1; + return false; + } + + if (f.write(data) != data.size() || !f.flush()) + { + errors_ += QString("Failed to write %1: %2\n").arg(name).arg(f.errorString()); + retval_ = 1; + return false; + } + + return true; +} + +void OpenSSLLibWorker::run() +{ + retval_ = 1; + + switch (job_) + { + case JobGenRSAKey: + RunGenRSAKey(); + break; + + case JobGenCert: + RunGenCert(); + break; + + case JobGenDHParam: + RunGenDHParam(); + break; + } +} + +void OpenSSLLibWorker::RunGenRSAKey() +{ + EVP_PKEY_CTX *ctx = 0; + EVP_PKEY *pkey = 0; + BIO *mem = 0; + const EVP_CIPHER *cipher = 0; + QByteArray password; + unsigned char rndchar; + quint32 numbits; + + /* + * Generate random-sized key with min size 4096 and multiply of 8. + * Max size is limited to 4096 + 99*8. + * See https://blog.josefsson.org/2016/11/03/why-i-dont-use-2048-or-4096-rsa-key-sizes/ + * "Why I don’t Use 2048 or 4096 RSA Key Sizes" + */ + numbits = PICA_RSA_MINKEYSIZE; + + if (RAND_bytes(&rndchar, 1) != 1) + { + Fail("Random number generator failure"); + return; + } + + numbits += 8 * (rndchar % 100); + + /* + * What "-rand <file>" did: an extra seeding source, /dev/random when the + * account dialog offers it and the user wants it. RAND_load_file() reads + * a small fixed amount from anything that is not a regular file, so this + * does not sit on the device draining it. + */ + if (!rand_.isEmpty()) + { + if (RAND_load_file(QFile::encodeName(rand_).constData(), -1) <= 0) + { + Fail("Failed to seed the random number generator from the requested source"); + return; + } + } + + if (setpassword_) + { + cipher = EVP_get_cipherbyname(PICA_PRIVKEYENCALGO + 1); /* skip the leading '-' */ + + if (!cipher) + { + Fail("Unknown private key encryption algorithm " PICA_PRIVKEYENCALGO); + return; + } + + /* + * UTF-8 rather than the Latin-1 OpenSSLTool wrote down the pipe: + * AskPassword hands the passphrase to OpenSSL as UTF-8 when the + * account is opened later, so anything outside ASCII produced a key + * that could be created but never unlocked again. + */ + password = password_.toUtf8(); + } + + ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL); + + if (!ctx) + { + Fail("Failed to create an RSA key generation context"); + goto end; + } + + if (EVP_PKEY_keygen_init(ctx) <= 0) + { + Fail("Failed to initialize RSA key generation"); + goto end; + } + + if (EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, (int)numbits) <= 0) + { + Fail("Failed to set the RSA key size"); + goto end; + } + + if (EVP_PKEY_keygen(ctx, &pkey) <= 0) + { + Fail("Failed to generate the RSA keypair"); + goto end; + } + + mem = BIO_new(BIO_s_mem()); + + if (!mem) + { + Fail("Out of memory"); + goto end; + } + + if (!PEM_write_bio_PrivateKey(mem, pkey, cipher, NULL, 0, NULL, + setpassword_ ? (void*)password.constData() : NULL)) + { + Fail("Failed to encode the private key"); + goto end; + } + + if (WriteFile(keyfile_, BioToByteArray(mem))) + retval_ = 0; + +end: + + if (mem) + BIO_free(mem); + + if (pkey) + EVP_PKEY_free(pkey); + + if (ctx) + EVP_PKEY_CTX_free(ctx); +} + +void OpenSSLLibWorker::RunGenCert() +{ + EVP_PKEY *pkey = 0; + X509 *cert = 0; + X509_NAME *name = 0; + BIGNUM *serial = 0; + BIO *keymem = 0; + BIO *mem = 0; + const EVP_MD *digest; + X509V3_CTX extctx; + QByteArray keydata; + QByteArray password; + QByteArray subject; + + /* + * The extensions "openssl req -x509" picked up from the [ v3_ca ] section + * of openssl.cnf. Spelled out here so that a certificate no longer + * depends on which openssl.cnf the machine happens to have - the Windows + * build used to have to ship one of its own next to the executable. + * + * Order matters: "keyid:always" reads the subject key identifier of the + * issuer, which for a self signed certificate is the extension added just + * above it. + */ + static const struct + { + int nid; + const char *value; + } extensions[] = + { + { NID_subject_key_identifier, "hash" }, + { NID_authority_key_identifier, "keyid:always,issuer" }, + { NID_basic_constraints, "critical,CA:true" } + }; + + unsigned int i; + + digest = EVP_get_digestbyname(PICA_CERTDIGESTALGO + 1); /* skip the leading '-' */ + + if (!digest) + { + Fail("Unknown certificate digest algorithm " PICA_CERTDIGESTALGO); + return; + } + + keydata = ReadWholeFile(keyfile_); + + if (keydata.isEmpty()) + { + errors_ += QString("Failed to read the private key file %1\n").arg(keyfile_); + retval_ = 1; + return; + } + + keymem = BIO_new_mem_buf((void*)keydata.constData(), keydata.size()); + + if (!keymem) + { + Fail("Out of memory"); + goto end; + } + + password = password_.toUtf8(); + + pkey = PEM_read_bio_PrivateKey(keymem, NULL, NULL, (void*)password.constData()); + + if (!pkey) + { + Fail("Failed to read the private key"); + goto end; + } + + cert = X509_new(); + + if (!cert) + { + Fail("Out of memory"); + goto end; + } + + if (!X509_set_version(cert, 2)) /* X.509 v3 */ + { + Fail("Failed to set the certificate version"); + goto end; + } + + serial = BN_new(); + + if (!serial + || !BN_rand(serial, PICA_CERT_SERIAL_BITS, BN_RAND_TOP_ANY, BN_RAND_BOTTOM_ANY) + || !BN_to_ASN1_INTEGER(serial, X509_get_serialNumber(cert))) + { + Fail("Failed to generate the certificate serial number"); + goto end; + } + + name = X509_NAME_new(); + + subject = subject_.toUtf8(); + + if (!name + || !X509_NAME_add_entry_by_NID(name, NID_commonName, MBSTRING_UTF8, + (unsigned char*)subject.data(), subject.size(), -1, 0)) + { + Fail("Failed to build the certificate subject name"); + goto end; + } + + if (!X509_set_subject_name(cert, name) || !X509_set_issuer_name(cert, name)) + { + Fail("Failed to set the certificate subject name"); + goto end; + } + + if (!X509_gmtime_adj(X509_getm_notBefore(cert), 0) + || !X509_time_adj_ex(X509_getm_notAfter(cert), PICA_CERT_VALIDITY_DAYS, 0, NULL)) + { + Fail("Failed to set the certificate validity period"); + goto end; + } + + if (!X509_set_pubkey(cert, pkey)) + { + Fail("Failed to set the certificate public key"); + goto end; + } + + X509V3_set_ctx_nodb(&extctx); + X509V3_set_ctx(&extctx, cert, cert, NULL, NULL, 0); + + for (i = 0; i < sizeof(extensions) / sizeof(extensions[0]); i++) + { + X509_EXTENSION *ext; + QByteArray value(extensions[i].value); + + ext = X509V3_EXT_conf_nid(NULL, &extctx, extensions[i].nid, value.data()); + + if (!ext) + { + Fail("Failed to build a certificate extension"); + goto end; + } + + if (!X509_add_ext(cert, ext, -1)) + { + X509_EXTENSION_free(ext); + Fail("Failed to add a certificate extension"); + goto end; + } + + X509_EXTENSION_free(ext); + } + + if (!X509_sign(cert, pkey, digest)) + { + Fail("Failed to sign the certificate"); + goto end; + } + + mem = BIO_new(BIO_s_mem()); + + if (!mem) + { + Fail("Out of memory"); + goto end; + } + + if (!PEM_write_bio_X509(mem, cert)) + { + Fail("Failed to encode the certificate"); + goto end; + } + + if (WriteFile(certfile_, BioToByteArray(mem))) + retval_ = 0; + +end: + + if (mem) + BIO_free(mem); + + if (name) + X509_NAME_free(name); + + if (serial) + BN_free(serial); + + if (cert) + X509_free(cert); + + if (pkey) + EVP_PKEY_free(pkey); + + if (keymem) + BIO_free(keymem); +} + +void OpenSSLLibWorker::RunGenDHParam() +{ + EVP_PKEY_CTX *ctx = 0; + EVP_PKEY *pkey = 0; + BIO *mem = 0; + + ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_DH, NULL); + + if (!ctx) + { + Fail("Failed to create a Diffie-Hellman parameter generation context"); + goto end; + } + + if (EVP_PKEY_paramgen_init(ctx) <= 0) + { + Fail("Failed to initialize Diffie-Hellman parameter generation"); + goto end; + } + + if (EVP_PKEY_CTX_set_dh_paramgen_prime_len(ctx, (int)numbits_) <= 0) + { + Fail("Failed to set the Diffie-Hellman prime length"); + goto end; + } + + /* "-5", the generator OpenSSLTool asked the command line program for. */ + if (EVP_PKEY_CTX_set_dh_paramgen_generator(ctx, 5) <= 0) + { + Fail("Failed to set the Diffie-Hellman generator"); + goto end; + } + + if (EVP_PKEY_paramgen(ctx, &pkey) <= 0) + { + Fail("Failed to generate the Diffie-Hellman parameters"); + goto end; + } + + mem = BIO_new(BIO_s_mem()); + + if (!mem) + { + Fail("Out of memory"); + goto end; + } + + if (!PEM_write_bio_Parameters(mem, pkey)) + { + Fail("Failed to encode the Diffie-Hellman parameters"); + goto end; + } + + if (WriteFile(outfile_, BioToByteArray(mem))) + retval_ = 0; + +end: + + if (mem) + BIO_free(mem); + + if (pkey) + EVP_PKEY_free(pkey); + + if (ctx) + EVP_PKEY_CTX_free(ctx); +} + +OpenSSLLib::OpenSSLLib(QObject *parent) : + QObject(parent), worker_(0) +{ +} + +OpenSSLLib::~OpenSSLLib() +{ + if (worker_) + { + worker_->disconnect(this); + worker_->wait(); + } +} + +static QString NameFromCertBytes(const QByteArray &cert_pem) +{ + X509 *cert; + QString name; + + cert = CertFromPEM(cert_pem); + + if (!cert) + return QString(); + + name = NameFromSubjectLine(SubjectLine(cert)); + + X509_free(cert); + + return name; +} + +QString OpenSSLLib::NameFromCertFile(QString cert_file) +{ + return NameFromCertBytes(ReadWholeFile(cert_file)); +} + +QString OpenSSLLib::NameFromCertString(QString cert_pem) +{ + return NameFromCertBytes(cert_pem.toLatin1()); +} + +QString OpenSSLLib::CertTextFromString(QString cert_pem) +{ + X509 *cert; + BIO *mem; + QString text; + + cert = CertFromPEM(cert_pem.toLatin1()); + + if (!cert) + return QString(); + + mem = BIO_new(BIO_s_mem()); + + if (!mem) + { + X509_free(cert); + return QString(); + } + + if (X509_print_ex(mem, cert, MULTILINE_NAMEOPT, 0)) + text = BioToString(mem); + + BIO_free(mem); + X509_free(cert); + ERR_clear_error(); + + return text; +} + +bool OpenSSLLib::StartWorker(OpenSSLLibWorker *worker, QObject *receiver, const char *finished_slot) +{ + if (worker_) + { + delete worker; + return false; + } + + worker_ = worker; + + errors_.clear(); + output_.clear(); + + disconnect(this, SIGNAL(finished(int)), 0, 0); + connect(this, SIGNAL(finished(int)), receiver, finished_slot); + + /* + * Queued, because QThread::finished() is emitted on the worker thread and + * this object lives on the one that called - so WorkerFinished(), and the + * receiver's slot after it, run where the caller expects them to. + */ + connect(worker_, SIGNAL(finished()), this, SLOT(WorkerFinished())); + + worker_->start(); + + return true; +} + +void OpenSSLLib::WorkerFinished() +{ + OpenSSLLibWorker *worker = worker_; + int retval; + + if (!worker) + return; + + retval = worker->retval(); + errors_ = worker->errors(); + + worker_ = 0; + worker->wait(); /* returns at once - run() is over by the time this is delivered */ + worker->deleteLater(); + + /* + * Last thing done here: the receiver is allowed to delete us from its + * slot, the way DHParam disposes of its generator. + */ + emit finished(retval); +} + +bool OpenSSLLib::GenRSAKeySignal(QString keyfile, bool setpassword, QString password, QString rand, QObject *receiver, const char *finished_slot) +{ + OpenSSLLibWorker *worker = new OpenSSLLibWorker(OpenSSLLibWorker::JobGenRSAKey, this); + + worker->keyfile_ = keyfile; + worker->setpassword_ = setpassword; + worker->password_ = password; + worker->rand_ = rand; + + return StartWorker(worker, receiver, finished_slot); +} + +bool OpenSSLLib::GenCertSignal(QString cert_file, QString keyfile, QString keypassword, + QString subject, QObject *receiver, const char *finished_slot) +{ + OpenSSLLibWorker *worker = new OpenSSLLibWorker(OpenSSLLibWorker::JobGenCert, this); + + worker->certfile_ = cert_file; + worker->keyfile_ = keyfile; + worker->password_ = keypassword; + worker->subject_ = subject; + + return StartWorker(worker, receiver, finished_slot); +} + +bool OpenSSLLib::GenDHParamSignal(quint32 numbits, QString output_file, QObject *receiver, const char *finished_slot) +{ + OpenSSLLibWorker *worker = new OpenSSLLibWorker(OpenSSLLibWorker::JobGenDHParam, this); + + worker->numbits_ = numbits; + worker->outfile_ = output_file; + + return StartWorker(worker, receiver, finished_slot); +} + +QString OpenSSLLib::ReadStdErr() +{ + QString errors = errors_; + + errors_.clear(); + + return errors; +} + +QString OpenSSLLib::ReadStdOut() +{ + QString output = output_; + + output_.clear(); + + return output; +} diff --git a/pica-client/openssllib.h b/pica-client/openssllib.h new file mode 100644 index 0000000..2c58820 --- /dev/null +++ b/pica-client/openssllib.h @@ -0,0 +1,81 @@ +/* + (c) Copyright 2012 - 2018 Anton Sviridenko + https://picapica.im + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, version 3. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ +#ifndef OPENSSLLIB_H +#define OPENSSLLIB_H + +#include <QObject> +#include <QString> + +/* + * Key, certificate and Diffie-Hellman parameter generation, and the reading + * of certificates for display, done through libcrypto. + * + * This is the replacement for OpenSSLTool, which drove the "openssl" command + * line program through QProcess and parsed its output. The results are meant + * to be the same down to the encoding of the files produced - see + * tests/test_openssllib.cpp, which compares them against the command line + * program - but the binary is no longer a runtime dependency, there is no + * openssl.cnf to ship next to the executable, and passphrases no longer + * travel through a pipe. + * + * The three generating methods keep the shape of their OpenSSLTool + * counterparts: they return immediately and call the receiver's slot when the + * work is over. The work itself happens on a worker thread, because it is the + * GUI thread calling - RSA key generation takes seconds and DH parameter + * generation at PICA_CLIENT_DHPARAMBITS takes minutes. The slot signature is + * the one difference from OpenSSLTool: there is no process any more, so it is + * finished(int retval) rather than finished(int, QProcess::ExitStatus). + * retval keeps its old meaning - zero on success. + */ + +class OpenSSLLibWorker; + +class OpenSSLLib : public QObject +{ + Q_OBJECT +public: + explicit OpenSSLLib(QObject *parent = 0); + ~OpenSSLLib(); + + static QString NameFromCertFile(QString cert_file); + static QString NameFromCertString(QString cert_pem); + static QString CertTextFromString(QString cert_pem); + + bool GenRSAKeySignal(QString keyfile, bool setpassword, + QString password, QString rand, QObject *receiver, const char *finished_slot); + bool GenCertSignal(QString cert_file, QString keyfile, QString keypassword, QString subject, QObject *receiver, const char *finished_slot); + + bool GenDHParamSignal(quint32 numbits, QString output_file, QObject *receiver, const char *finished_slot); + + QString ReadStdErr(); + QString ReadStdOut(); + +signals: + void finished(int retval); + +private slots: + void WorkerFinished(); + +private: + bool StartWorker(OpenSSLLibWorker *worker, QObject *receiver, const char *finished_slot); + + OpenSSLLibWorker *worker_; + QString errors_; + QString output_; +}; + +#endif // OPENSSLLIB_H diff --git a/pica-client/openssltool.cpp b/pica-client/openssltool.cpp deleted file mode 100644 index ec7a547..0000000 --- a/pica-client/openssltool.cpp +++ /dev/null @@ -1,222 +0,0 @@ -/* - (c) Copyright 2012 - 2018 Anton Sviridenko - https://picapica.im - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, version 3. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see <https://www.gnu.org/licenses/>. -*/ -#include "openssltool.h" -#include "../PICA_security.h" -#include <openssl/rand.h> -#include <QProcess> -#include <QRegExp> -#include <QMessageBox> - -#define REGEXOLD "CN = [0-9]+\\#([^\\#\\$&\"\'=\\(\\)\\\\/\\|`!<>\\{\\}\\[\\]\\+\r\n]+)" -#define REGEXNEW "CN = ([^\\#\\$&\"\'=\\(\\)\\\\/\\|`!<>\\{\\}\\[\\]\\+\r\n]+)" - -OpenSSLTool::OpenSSLTool() -{ -} - -QString OpenSSLTool::NameFromCertFile(QString cert_file) -{ - // - QProcess openssl; - - openssl.start("openssl", QStringList() << "x509" << "-in" << cert_file << "-subject" << "-nameopt" << "oneline,-esc_msb,utf8" << "-noout"); - if (!openssl.waitForStarted()) - return QString(); - - if (!openssl.waitForFinished()) - return QString(); - - QByteArray result = openssl.readAll(); - QRegExp rxOld(REGEXOLD); - QRegExp rx(REGEXNEW); - - QString name = QString::fromUtf8( result.constData()); - - if (name.contains(rxOld)) - { - name = rxOld.cap(1); - } - else if (name.contains(rx)) - { - name = rx.cap(1); - } - - // - return name; -} - -QString OpenSSLTool::NameFromCertString(QString cert_pem) -{ - QProcess openssl; - - openssl.start("openssl", QStringList() << "x509" << "-subject" << "-nameopt" << "oneline,-esc_msb,utf8" << "-noout"); - if (!openssl.waitForStarted()) - return QString(); - - openssl.write(cert_pem.toLatin1()); - - if (!openssl.waitForFinished()) - return QString(); - - QByteArray result = openssl.readAll(); - QRegExp rxOld(REGEXOLD); - QRegExp rx(REGEXNEW); - - QString name = QString::fromUtf8( result.constData()); - - if (name.contains(rxOld)) - { - name = rxOld.cap(1); - } - else if (name.contains(rx)) - { - name = rx.cap(1); - } - - // - return name; -} - -QString OpenSSLTool::CertTextFromString(QString cert_pem) -{ - QProcess openssl; - qint64 ret; - char *data; - QByteArray ba; - - openssl.start("openssl", QStringList() << "x509" << "-noout" << "-text" << "-nameopt" << "multiline,-esc_msb,utf8"); - - if (!openssl.waitForStarted()) - return QString(); - - ba = cert_pem.toLatin1(); - ret = openssl.write(data = (char*)ba.constData()); - openssl.closeWriteChannel(); - - if (!openssl.waitForFinished()) - return QString(); - - return QString::fromUtf8(openssl.readAllStandardOutput().constData()); -} - -bool OpenSSLTool::GenRSAKeySignal(QString keyfile, bool setpassword, QString password, QString rand, QObject *receiver, const char *finished_slot) -{ - QStringList args; - quint32 numbits; - unsigned char rndchar; - - args << "genrsa" << "-out" << keyfile; - - if (setpassword) - args << "-passout" << "stdin" << PICA_PRIVKEYENCALGO; - - if (!rand.isEmpty()) - args << "-rand" << rand; - - /* - * Generate random-sized key with min size 4096 and multiply of 8. - * Max size is limited to 4096 + 99*8. - * See https://blog.josefsson.org/2016/11/03/why-i-dont-use-2048-or-4096-rsa-key-sizes/ - * "Why I don’t Use 2048 or 4096 RSA Key Sizes" - */ - numbits = PICA_RSA_MINKEYSIZE; - - RAND_pseudo_bytes(&rndchar, 1); - - numbits += 8 * (rndchar % 100); - - args << QString::number(numbits); - - openssl_.start("openssl", args); - - - if (!openssl_.waitForStarted()) - return false; - - if (setpassword) - { - openssl_.write((password + "\n").toLatin1().constData()); - } - - openssl_.disconnect(&openssl_, SIGNAL(finished(int, QProcess::ExitStatus)), 0, 0); - openssl_.connect(&openssl_, SIGNAL(finished(int, QProcess::ExitStatus)), receiver, finished_slot); - - return true; -} - -bool OpenSSLTool::GenCertSignal(QString cert_file, QString keyfile, QString keypassword, - QString subject, QObject *receiver, const char *finished_slot) -{ - openssl_.start("openssl", - QStringList() - << "req" -#ifdef WIN32 - << "-config" << "openssl.cnf" -#endif - << "-out" << cert_file - << "-key" << keyfile - << "-subj" << "/CN=" + subject - << "-new" - << "-x509" - << "-days" << "3680" - << "-utf8" - << "-batch" - << PICA_CERTDIGESTALGO - << "-passin" - << "stdin"); - - if (!openssl_.waitForStarted()) - return false; - - openssl_.write((keypassword + "\n").toLatin1().constData()); - - openssl_.disconnect(&openssl_, SIGNAL(finished(int, QProcess::ExitStatus)), 0, 0); - openssl_.connect(&openssl_, SIGNAL(finished(int, QProcess::ExitStatus)), receiver, finished_slot); - - return true; -} - -QString OpenSSLTool::ReadStdErr() -{ - return QString::fromUtf8(openssl_.readAllStandardError().constData()); -} - -QString OpenSSLTool::ReadStdOut() -{ - return QString::fromUtf8(openssl_.readAllStandardOutput().constData()); -} - -bool OpenSSLTool::GenDHParamSignal(quint32 numbits, QString output_file, QObject *receiver, const char *finished_slot) -{ - //openssl dhparam -5 4096 - openssl_.start("openssl", - QStringList() - << "dhparam" - << "-out" << output_file - << "-5" - << QString::number(numbits)); - - if (!openssl_.waitForStarted()) - return false; - - openssl_.disconnect(&openssl_, SIGNAL(finished(int, QProcess::ExitStatus)), 0, 0); - openssl_.connect(&openssl_, SIGNAL(finished(int, QProcess::ExitStatus)), receiver, finished_slot); - - return true; -} - - diff --git a/pica-client/openssltool.h b/pica-client/openssltool.h deleted file mode 100644 index 8d59860..0000000 --- a/pica-client/openssltool.h +++ /dev/null @@ -1,45 +0,0 @@ -/* - (c) Copyright 2012 - 2018 Anton Sviridenko - https://picapica.im - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, version 3. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see <https://www.gnu.org/licenses/>. -*/ -#ifndef OPENSSLTOOL_H -#define OPENSSLTOOL_H - -#include <QString> -#include <QProcess> - -class OpenSSLTool -{ -public: - OpenSSLTool(); - - static QString NameFromCertFile(QString cert_file); - static QString NameFromCertString(QString cert_pem); - static QString CertTextFromString(QString cert_pem); - - bool GenRSAKeySignal(QString keyfile, bool setpassword, - QString password, QString rand, QObject *receiver, const char *finished_slot); - bool GenCertSignal(QString cert_file, QString keyfile, QString keypassword, QString subject, QObject *receiver, const char *finished_slot); - - bool GenDHParamSignal(quint32 numbits, QString output_file, QObject *receiver, const char *finished_slot); - - QString ReadStdErr(); - QString ReadStdOut(); - -private: - QProcess openssl_; -}; - -#endif // OPENSSLTOOL_H diff --git a/pica-client/pica-client.pro b/pica-client/pica-client.pro index ddbcda8..279879a 100644 --- a/pica-client/pica-client.pro +++ b/pica-client/pica-client.pro @@ -38,7 +38,7 @@ SOURCES += main.cpp\ dialogs/viewcertdialog.cpp \ dialogs/forgedcertdialog.cpp \ dialogs/registeraccountdialog.cpp \ - openssltool.cpp \ + openssllib.cpp \ history.cpp \ ../PICA_id.c \ dhparam.cpp \ @@ -83,7 +83,7 @@ HEADERS += mainwindow.h \ ../PICA_common.h \ ../PICA_proto.h \ dialogs/registeraccountdialog.h \ - openssltool.h \ + openssllib.h \ history.h \ ../PICA_id.h \ dhparam.h \ diff --git a/pica-client/skynet.cpp b/pica-client/skynet.cpp index 7427cd4..e0196f5 100644 --- a/pica-client/skynet.cpp +++ b/pica-client/skynet.cpp @@ -21,7 +21,7 @@ #include <QCoreApplication> //#include "dialogs/viewcertdialog.h" #include "contacts.h" -#include "openssltool.h" +#include "openssllib.h" //#include <QMessageBox> //debug //#include "dialogs/forgedcertdialog.h" #include <QDebug> @@ -324,7 +324,7 @@ void SkyNet::verify_peer_cert(QByteArray peer_id, QString cert_pem, bool *verifi { cnt.SetContactCert(peer_id, cert_pem); - QString name = OpenSSLTool::NameFromCertString(cert_pem); + QString name = OpenSSLLib::NameFromCertString(cert_pem); cnt.SetContactName(peer_id, name); |
