Replace QCA with a simple OpenSSL wrapper

This commit is contained in:
Albert Vaca Cintora 2023-07-12 18:54:16 +02:00
parent 956c88c964
commit d948d882aa
6 changed files with 182 additions and 86 deletions

View file

@ -10,7 +10,6 @@ project(kdeconnect VERSION ${RELEASE_SERVICE_VERSION})
set(KF_MIN_VERSION "5.101.0")
set(QT_MIN_VERSION "5.15.2")
set(QCA_MIN_VERSION "2.1.0")
find_package(ECM ${KF_MIN_VERSION} REQUIRED NO_MODULE)
set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake)
@ -58,9 +57,6 @@ ecm_set_disabled_deprecation_versions(
add_library(kdeconnectversion INTERFACE)
target_include_directories(kdeconnectversion INTERFACE ${CMAKE_CURRENT_BINARY_DIR})
find_package(Qca-qt${QT_MAJOR_VERSION} ${QCA_MIN_VERSION} REQUIRED)
set(Qca_LIBRARY qca-qt${QT_MAJOR_VERSION})
set(KF5_REQUIRED_COMPONENTS I18n ConfigWidgets DBusAddons IconThemes Notifications KIO KCMUtils Service Solid Kirigami2 People WindowSystem GuiAddons DocTools)
set_package_properties(KF${QT_MAJOR_VERSION}Kirigami2 PROPERTIES

View file

@ -42,6 +42,7 @@ target_sources(kdeconnectcore PRIVATE
compositefiletransferjob.cpp
daemon.cpp
device.cpp
sslhelper.cpp
core_debug.cpp
notificationserverinfo.cpp
openconfig.cpp
@ -55,13 +56,16 @@ ecm_qt_declare_logging_category(kdeconnectcore
target_include_directories(kdeconnectcore PUBLIC ${PROJECT_SOURCE_DIR})
find_package(OpenSSL REQUIRED)
target_link_libraries(kdeconnectcore
PUBLIC
Qt::Network
KF${QT_MAJOR_VERSION}::CoreAddons
${Qca_LIBRARY}
KF${QT_MAJOR_VERSION}::KIOCore
KF${QT_MAJOR_VERSION}::KIOGui
OpenSSL::Crypto
OpenSSL::SSL
PRIVATE
Qt::DBus
KF${QT_MAJOR_VERSION}::I18n

View file

@ -19,23 +19,19 @@
#include <QStandardPaths>
#include <QThread>
#include <QUuid>
#include <QtCrypto>
#include "core_debug.h"
#include "daemon.h"
#include "dbushelper.h"
#include "deviceinfo.h"
#include "pluginloader.h"
#include "sslhelper.h"
const QFile::Permissions strictPermissions = QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser | QFile::WriteUser;
struct KdeConnectConfigPrivate {
// The Initializer object sets things up, and also does cleanup when it goes out of scope
// Note it's not being used anywhere. That's intended
QCA::Initializer m_qcaInitializer;
QCA::PrivateKey m_privateKey;
QSslCertificate m_certificate; // Use QSslCertificate instead of QCA::Certificate due to compatibility with QSslSocket
EVP_PKEY *m_privateKey;
QSslCertificate m_certificate;
QSettings *m_config;
QSettings *m_trustedDevices;
@ -59,15 +55,6 @@ KdeConnectConfig &KdeConnectConfig::instance()
KdeConnectConfig::KdeConnectConfig()
: d(new KdeConnectConfigPrivate)
{
// qCDebug(KDECONNECT_CORE) << "QCA supported capabilities:" << QCA::supportedFeatures().join(",");
if (!QCA::isSupported("rsa")) {
qCritical() << "Could not find support for RSA in your QCA installation";
Daemon::instance()->reportError(i18n("KDE Connect failed to start"),
i18n("Could not find support for RSA in your QCA installation. If your "
"distribution provides separate packets for QCA-ossl and QCA-gnupg, "
"make sure you have them installed and try again."));
}
// Make sure base directory exists
QDir().mkpath(baseConfigDir().path());
@ -75,8 +62,7 @@ KdeConnectConfig::KdeConnectConfig()
d->m_config = new QSettings(baseConfigDir().absoluteFilePath(QStringLiteral("config")), QSettings::IniFormat);
d->m_trustedDevices = new QSettings(baseConfigDir().absoluteFilePath(QStringLiteral("trusted_devices")), QSettings::IniFormat);
loadPrivateKey();
loadCertificate();
loadOrGeneratePrivateKeyAndCertificate(privateKeyPath(), certificatePath());
if (name().isEmpty()) {
setName(getDefaultDeviceName());
@ -260,56 +246,48 @@ QDir KdeConnectConfig::pluginConfigDir(const QString &deviceId, const QString &p
return QDir(pluginConfigDir);
}
void KdeConnectConfig::loadPrivateKey()
bool KdeConnectConfig::loadPrivateKey(const QString &keyPath)
{
QString keyPath = privateKeyPath();
QFile privKey(keyPath);
bool needsToGenerateKey = false;
if (privKey.exists() && privKey.open(QIODevice::ReadOnly)) {
QCA::ConvertResult result;
d->m_privateKey = QCA::PrivateKey::fromPEM(QString::fromLatin1(privKey.readAll()), QCA::SecureArray(), &result);
if (result != QCA::ConvertResult::ConvertGood) {
qCWarning(KDECONNECT_CORE) << "Private key from" << keyPath << "is not valid";
needsToGenerateKey = true;
d->m_privateKey = SslHelper::pemToRsaPrivateKey(privKey.readAll());
if (d->m_privateKey == nullptr) {
qCWarning(KDECONNECT_CORE) << "Private key from" << keyPath << "is not valid!";
}
} else {
needsToGenerateKey = true;
}
return (d->m_privateKey == nullptr);
}
bool KdeConnectConfig::loadCertificate(const QString &certPath)
{
QFile cert(certPath);
if (cert.exists() && cert.open(QIODevice::ReadOnly)) {
auto loadedCerts = QSslCertificate::fromData(cert.readAll());
if (loadedCerts.empty()) {
qCWarning(KDECONNECT_CORE) << "Certificate from" << certPath << "is not valid";
} else {
d->m_certificate = loadedCerts.at(0);
}
}
return d->m_certificate.isNull();
}
void KdeConnectConfig::loadOrGeneratePrivateKeyAndCertificate(const QString &keyPath, const QString &certPath)
{
bool needsToGenerateKey = loadPrivateKey(keyPath);
bool needsToGenerateCert = needsToGenerateKey || loadCertificate(certPath);
if (needsToGenerateKey) {
generatePrivateKey(keyPath);
}
if (needsToGenerateCert) {
generateCertificate(certPath);
}
// Extra security check
if (QFile::permissions(keyPath) != strictPermissions) {
qCWarning(KDECONNECT_CORE) << "Warning: KDE Connect private key file has too open permissions " << keyPath;
}
}
void KdeConnectConfig::loadCertificate()
{
QString certPath = certificatePath();
QFile cert(certPath);
bool needsToGenerateCert = false;
if (cert.exists() && cert.open(QIODevice::ReadOnly)) {
auto loadedCerts = QSslCertificate::fromPath(certPath);
if (loadedCerts.empty()) {
qCWarning(KDECONNECT_CORE) << "Certificate from" << certPath << "is not valid";
needsToGenerateCert = true;
} else {
d->m_certificate = loadedCerts.at(0);
}
} else {
needsToGenerateCert = true;
}
if (needsToGenerateCert) {
generateCertificate(certPath);
}
// Extra security check
if (QFile::permissions(certPath) != strictPermissions) {
qCWarning(KDECONNECT_CORE) << "Warning: KDE Connect certificate file has too open permissions " << certPath;
}
@ -319,16 +297,16 @@ void KdeConnectConfig::generatePrivateKey(const QString &keyPath)
{
qCDebug(KDECONNECT_CORE) << "Generating private key";
bool error = false;
d->m_privateKey = QCA::KeyGenerator().createRSA(2048);
d->m_privateKey = SslHelper::generateRsaPrivateKey();
QByteArray keyPem = SslHelper::privateKeyToPEM(d->m_privateKey);
QFile privKey(keyPath);
bool error = false;
if (!privKey.open(QIODevice::ReadWrite | QIODevice::Truncate)) {
error = true;
} else {
privKey.setPermissions(strictPermissions);
int written = privKey.write(d->m_privateKey.toPEM().toLatin1());
int written = privKey.write(keyPem);
if (written <= 0) {
error = true;
}
@ -343,37 +321,22 @@ void KdeConnectConfig::generateCertificate(const QString &certPath)
{
qCDebug(KDECONNECT_CORE) << "Generating certificate";
bool error = false;
QString uuid = QUuid::createUuid().toString();
DBusHelper::filterNonExportableCharacters(uuid);
qCDebug(KDECONNECT_CORE) << "My id:" << uuid;
// FIXME: We only use QCA here to generate the cert and key, would be nice to get rid of it completely.
// The same thing we are doing with QCA could be done invoking openssl (although it's potentially less portable):
// openssl req -new -x509 -sha256 -newkey rsa:2048 -nodes -keyout privateKey.pem -days 3650 -out certificate.pem -subj "/O=KDE/OU=KDE
// Connect/CN=_e6e29ad4_2b31_4b6d_8f7a_9872dbaa9095_"
QCA::CertificateOptions certificateOptions = QCA::CertificateOptions();
QDateTime startTime = QDateTime::currentDateTime().addYears(-1);
QDateTime endTime = startTime.addYears(10);
QCA::CertificateInfo certificateInfo;
certificateInfo.insert(QCA::CommonName, uuid);
certificateInfo.insert(QCA::Organization, QStringLiteral("KDE"));
certificateInfo.insert(QCA::OrganizationalUnit, QStringLiteral("Kde connect"));
certificateOptions.setInfo(certificateInfo);
certificateOptions.setFormat(QCA::PKCS10);
certificateOptions.setSerialNumber(QCA::BigInteger(10));
certificateOptions.setValidityPeriod(startTime, endTime);
d->m_certificate = QSslCertificate(QCA::Certificate(certificateOptions, d->m_privateKey).toPEM().toLatin1());
X509 *certificate = SslHelper::generateSelfSignedCertificate(d->m_privateKey, uuid);
QByteArray pemCertificate = SslHelper::certificateToPEM(certificate);
X509_free(certificate);
d->m_certificate = QSslCertificate(pemCertificate);
QFile cert(certPath);
bool error = false;
if (!cert.open(QIODevice::ReadWrite | QIODevice::Truncate)) {
error = true;
} else {
cert.setPermissions(strictPermissions);
int written = cert.write(d->m_certificate.toPem());
int written = cert.write(pemCertificate);
if (written <= 0) {
error = true;
}

View file

@ -68,9 +68,10 @@ public:
private:
KdeConnectConfig();
void loadPrivateKey();
void loadOrGeneratePrivateKeyAndCertificate(const QString &keyPath, const QString &certPath);
bool loadPrivateKey(const QString &path);
bool loadCertificate(const QString &path);
void generatePrivateKey(const QString &path);
void loadCertificate();
void generateCertificate(const QString &path);
struct KdeConnectConfigPrivate *d;

100
core/sslhelper.cpp Normal file
View file

@ -0,0 +1,100 @@
/**
* SPDX-FileCopyrightText: 2023 Albert Vaca <albertvaka@gmail.com>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
#include "sslhelper.h"
#include <openssl/bn.h>
#include <openssl/pem.h>
#include <openssl/rsa.h>
#include <openssl/x509v3.h>
namespace SslHelper
{
X509 *generateSelfSignedCertificate(EVP_PKEY *privateKey, const QString &commonName)
{
X509 *x509 = X509_new();
X509_set_version(x509, 2);
// Generate a random serial number for the certificate
BIGNUM *serialNumber = BN_new();
BN_rand(serialNumber, 160, -1, 0);
ASN1_INTEGER *serial = X509_get_serialNumber(x509);
BN_to_ASN1_INTEGER(serialNumber, serial);
BN_free(serialNumber);
// Set the certificate subject and issuer (self-signed)
X509_NAME *name = X509_NAME_new();
X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, reinterpret_cast<const unsigned char *>(commonName.toLatin1().data()), -1, -1, 0); // Common Name
X509_NAME_add_entry_by_txt(name, "O", MBSTRING_ASC, reinterpret_cast<const unsigned char *>("KDE"), -1, -1, 0); // Organization
X509_NAME_add_entry_by_txt(name, "OU", MBSTRING_ASC, reinterpret_cast<const unsigned char *>("KDE Connect"), -1, -1, 0); // Organizational Unit
X509_set_subject_name(x509, name);
X509_set_issuer_name(x509, name);
X509_NAME_free(name);
// Set the certificate validity period
time_t now = time(nullptr);
int a_year_in_seconds = 356 * 24 * 60 * 60;
ASN1_TIME_set(X509_get_notBefore(x509), now - a_year_in_seconds);
ASN1_TIME_set(X509_get_notAfter(x509), now + 10 * a_year_in_seconds);
// Set the public key for the certificate
X509_set_pubkey(x509, privateKey);
// Sign the certificate with the private key
X509_sign(x509, privateKey, EVP_sha256());
return x509;
}
EVP_PKEY *generateRsaPrivateKey()
{
EVP_PKEY *privateKey = EVP_PKEY_new();
RSA *rsa = RSA_new();
BIGNUM *exponent = BN_new();
BN_set_word(exponent, RSA_F4);
RSA_generate_key_ex(rsa, 2048, exponent, nullptr);
EVP_PKEY_assign_RSA(privateKey, rsa);
BN_free(exponent);
return privateKey;
}
QByteArray certificateToPEM(X509 *certificate)
{
BIO *bio = BIO_new(BIO_s_mem());
PEM_write_bio_X509(bio, certificate);
BUF_MEM *mem = nullptr;
BIO_get_mem_ptr(bio, &mem);
QByteArray pemData(mem->data, mem->length);
BIO_free_all(bio);
return pemData;
}
QByteArray privateKeyToPEM(EVP_PKEY *privateKey)
{
BIO *bio = BIO_new(BIO_s_mem());
PEM_write_bio_PrivateKey(bio, privateKey, nullptr, nullptr, 0, nullptr, nullptr);
BUF_MEM *mem = nullptr;
BIO_get_mem_ptr(bio, &mem);
QByteArray pemData(mem->data, mem->length);
BIO_free_all(bio);
return pemData;
}
EVP_PKEY *pemToRsaPrivateKey(const QByteArray &privateKeyPem)
{
const char *pemData = privateKeyPem.constData();
BIO *bio = BIO_new_mem_buf(pemData, -1);
EVP_PKEY *privateKey = PEM_read_bio_PrivateKey(bio, NULL, NULL, NULL);
BIO_free(bio);
return privateKey;
}
} // namespace SslHelper

32
core/sslhelper.h Normal file
View file

@ -0,0 +1,32 @@
/**
* SPDX-FileCopyrightText: 2023 Albert Vaca <albertvaka@gmail.com>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
#ifndef SSLHELPER_H
#define SSLHELPER_H
#include <openssl/evp.h>
#include <openssl/x509.h>
#include <QByteArray>
#include <QString>
namespace SslHelper
{
// Delete with X509_free(certificate);
X509 *generateSelfSignedCertificate(EVP_PKEY *privateKey, const QString &commonName);
// Delete with EVP_PKEY_free(privateKey);
EVP_PKEY *generateRsaPrivateKey();
QByteArray certificateToPEM(X509 *certificate);
QByteArray privateKeyToPEM(EVP_PKEY *privateKey);
EVP_PKEY *pemToRsaPrivateKey(const QByteArray &privateKeyPem);
}
#endif