From 1199d776d2ec95a5cf306d7f730005902ebc026f Mon Sep 17 00:00:00 2001 From: Christian Hohnstaedt Date: Tue, 20 Feb 2024 06:52:39 +0100 Subject: [PATCH] Close #89: ta.key for OpenVPN tls-auth The ta-key will be generated on-demand and assigned to the issuing CA. All issued certificates of this CA will use the same ta-key. It can be exported as single file and will also be part of the exported openvpn configuration file. Extend export tests to validate the output of ta-keys and adapt the OpenVPN conf file export test to also check the ta-key. --- lib/database_model.cpp | 4 ++-- lib/database_schema.cpp | 10 ++++++++++ lib/db_x509.cpp | 7 +++++-- lib/db_x509.h | 2 +- lib/pki_export.cpp | 1 + lib/pki_export.h | 1 + lib/pki_x509.cpp | 39 +++++++++++++++++++++++++++++++++++++++ lib/pki_x509.h | 1 + lib/sql.cpp | 2 +- test/export.cpp | 21 ++++++++++++++------- test/main.cpp | 1 + 11 files changed, 76 insertions(+), 13 deletions(-) diff --git a/lib/database_model.cpp b/lib/database_model.cpp index d9a1c4cf..9fcbd958 100644 --- a/lib/database_model.cpp +++ b/lib/database_model.cpp @@ -72,8 +72,8 @@ const QString &database_model::detect_provider() QSqlError database_model::initSqlDB() { -#define MAX_SCHEMAS 7 -#define SCHEMA_VERSION "7" +#define MAX_SCHEMAS 8 +#define SCHEMA_VERSION "8" QStringList schemas[MAX_SCHEMAS]; diff --git a/lib/database_schema.cpp b/lib/database_schema.cpp index 4b46b8b5..0be8ec2f 100644 --- a/lib/database_schema.cpp +++ b/lib/database_schema.cpp @@ -305,6 +305,16 @@ << "UPDATE settings SET value='7' WHERE key_='schema'" ; + schemas[7] +// OpenVPN TA (tls-auth) keys associated to the CA to be +// the same for all issued certificates +<< "CREATE TABLE takeys (" + "item INTEGER UNIQUE, " // reference to items(id) of the CA + "value " B64_BLOB ", " // The base64 encoded 2048 bit key + "FOREIGN KEY (item) REFERENCES items (id))" +<< "UPDATE settings SET value='8' WHERE key_='schema'" + ; + /* When adding new tables or views, also add them to the list * in XSqlQuery::rewriteQuery(QString) in lib/sql.cpp */ diff --git a/lib/db_x509.cpp b/lib/db_x509.cpp index eedad213..53d2de33 100644 --- a/lib/db_x509.cpp +++ b/lib/db_x509.cpp @@ -566,11 +566,11 @@ int db_x509::exportFlags(const QModelIndex &idx) const return disable_flags; } -void db_x509::writeTaggedPEM(BioByteArray &b, const QString &tag, XFile &file) +void db_x509::writeTaggedPEM(const BioByteArray &b, const QString &tag, XFile &file) { if (b.size() > 0) { file.write(QString("<%1>\n").arg(tag).toLatin1()); - file.write(b); + file.write(b.byteArray()); file.write(QString("\n").arg(tag).toLatin1()); } } @@ -609,6 +609,7 @@ void db_x509::exportItems(const QModelIndexList &list, writeTaggedPEM(extra, "extra-certs", file); writeTaggedPEM(cert, "cert", file); writeTaggedPEM(key, "key", file); + writeTaggedPEM(crt->getTaKey().toLatin1(), "tls-auth", file); } else if (xport->match_all(F_CHAIN)) { for (; crt && crt != oldcrt; oldcrt = crt, crt = crt->getSigner()) crt->writeCert(file, true); @@ -655,6 +656,8 @@ void db_x509::exportItems(const QModelIndexList &list, writeVcalendar(file, vcal); } else if (xport->match_all(F_CONFIG)) { crt->opensslConf(file); + } else if (xport->match_all(F_TAKEY)) { + file.write(crt->getTaKey().toLatin1()); } } diff --git a/lib/db_x509.h b/lib/db_x509.h index eaa61172..ae80a7f9 100644 --- a/lib/db_x509.h +++ b/lib/db_x509.h @@ -23,7 +23,7 @@ class db_x509: public db_x509super { Q_OBJECT - static void writeTaggedPEM(BioByteArray &b, + static void writeTaggedPEM(const BioByteArray &b, const QString &tag, XFile &file); protected: dbheaderList getHeaders(); diff --git a/lib/pki_export.cpp b/lib/pki_export.cpp index 25c284f7..9d1843d6 100644 --- a/lib/pki_export.cpp +++ b/lib/pki_export.cpp @@ -77,6 +77,7 @@ new pki_export(16, x509, "txt", tr("Certificate Index file"), F_INDEX | F_CA, new pki_export(17, x509, "ics", tr("vCalendar"), F_CAL, tr("vCalendar expiry reminder for the selected items")), new pki_export(18, x509, "ics", tr("CA vCalendar"), F_CAL | F_CA, tr("vCalendar expiry reminder containing all issued, valid certificates, the CA itself and the latest CRL")), new pki_export(38, x509, "conf", tr("OpenSSL config"), F_SINGLE | F_CONFIG, tr("OpenSSL configuration file to create a certificate or request with the openssl commandline tool")), +new pki_export(39, x509, "key", tr("OpenVPN tls-auth key"), F_SINGLE | F_TAKEY, tr("The OpenVPN tls-auth key is a secret key shared between endpoints")), new pki_export(19, asym_key, "pem", tr("PEM public"), F_PEM | F_CLIPBOARD, tr("Text format of the public key in one PEM file")), new pki_export(20, asym_key, "pem", tr("PEM private"), F_PEM | F_PRIVATE | F_USUAL | F_CLIPBOARD, tr("Unencrypted private key in text format")), diff --git a/lib/pki_export.h b/lib/pki_export.h index f884ea60..cdc2e5b6 100644 --- a/lib/pki_export.h +++ b/lib/pki_export.h @@ -49,6 +49,7 @@ enum { F_DER = BIT(19),/* DER format */ F_OVPN = BIT(20),/* OpenVPN format */ F_CONFIG = BIT(21),/* OpenSSL config format */ + F_TAKEY = BIT(22),/* OpenVPN TLS-Auth key */ }; class pki_export : public QObject { diff --git a/lib/pki_x509.cpp b/lib/pki_x509.cpp index 13a584d5..44edc6bb 100644 --- a/lib/pki_x509.cpp +++ b/lib/pki_x509.cpp @@ -17,6 +17,8 @@ #include "exception.h" #include "pass_info.h" +#include + pki_x509::pki_x509(X509 *c) :pki_x509super(), cert(c) { @@ -143,6 +145,7 @@ QSqlError pki_x509::deleteSqlData() << "UPDATE crls SET issuer=NULL WHERE issuer=?" << "UPDATE certs SET issuer=NULL WHERE issuer=?" << "DELETE FROM revocations WHERE caId=?" + << "DELETE FROM takeys WHERE item=?" ; foreach(QString task, tasks) { SQL_PREPARE(q, task); @@ -273,6 +276,42 @@ pki_x509 *pki_x509::getBySerial(const a1int &a) const return NULL; } +QString pki_x509::getTaKey() +{ + XSqlQuery q; + QByteArray b; + pki_x509 *issuer = getSigner(); + if (!isCA() && issuer && issuer != this) + return issuer->getTaKey(); + + Transaction; + if (!TransBegin()) + return QString(); + + SQL_PREPARE(q, "SELECT value FROM takeys WHERE item = ?"); + q.bindValue(0, sqlItemId); + q.exec(); + if (q.next()) { + b = QByteArray::fromBase64(q.value(0).toByteArray()); + qDebug() << "Loaded TA key" << this << b.size() << QString::fromLatin1(b.toHex()).left(6); + } else { + b.resize(2048/8); + RAND_bytes((unsigned char*)b.data(), 2048/8); + SQL_PREPARE(q, "INSERT INTO takeys (item, value) VALUES ( ?, ? )"); + q.bindValue(0, sqlItemId); + q.bindValue(1, b.toBase64()); + q.exec(); + qDebug() << "Generated TA key" << this << b.size() << QString::fromLatin1(b.toHex()).left(6); + } + TransCommit(); + QString takey("-----BEGIN OpenVPN Static key V1-----\n"); + QString hex(QString::fromLatin1(b.toHex())); + for (int i=0; i<16; i++) + takey += hex.mid(32*i, 32) + "\n"; + takey += "-----END OpenVPN Static key V1-----\n"; + return takey; +} + a1int pki_x509::hashInfo(const EVP_MD *md) const { unsigned char digest[EVP_MAX_MD_SIZE]; diff --git a/lib/pki_x509.h b/lib/pki_x509.h index cbe89074..3aec5eea 100644 --- a/lib/pki_x509.h +++ b/lib/pki_x509.h @@ -163,6 +163,7 @@ class pki_x509 : public pki_x509super void restoreSql(const QSqlRecord &rec); QStringList icsVEVENT() const; QStringList icsVEVENT_ca() const; + QString getTaKey(); }; Q_DECLARE_METATYPE(pki_x509 *); diff --git a/lib/sql.cpp b/lib/sql.cpp index dea0e958..ccf9be6f 100644 --- a/lib/sql.cpp +++ b/lib/sql.cpp @@ -125,7 +125,7 @@ QString XSqlQuery::rewriteQuery(QString _q) "items" , "crls" , "private_keys" , "public_keys" , "tokens" , "token_mechanism" , "templates" , "certs" , "authority" , "revocations" , "requests" , "x509super" , - "settings" , + "settings" , "takeys", "view_public_keys" , "view_certs" , "view_requests" , "view_crls" , "view_templates" , "view_private", diff --git a/test/export.cpp b/test/export.cpp index 55edde24..0efd0aea 100644 --- a/test/export.cpp +++ b/test/export.cpp @@ -24,14 +24,14 @@ void check_pems(const QString &name, int n, QStringList matches = QStringList()) { int begin = 0, end = 0; - qWarning() << "Expecting" << n << "PEMs in" << name; + qDebug() << "Expecting" << n << "PEMs in" << name; #if 0 // This is an endless loop: open_read() succeeds, // but isOpen returns false. Stop investigating, use POSIX open() XFile F(name); while (!F.isOpen()) { - qWarning() << "OPEN" << name; + qDebug() << "OPEN" << name; F.close(); Q_ASSERT(F.open_read()); } @@ -45,7 +45,7 @@ void check_pems(const QString &name, int n, QStringList matches = QStringList()) QByteArray all(buf, ret); close(fd); #endif - qWarning() << "ALL" << name << all.size(); + qDebug() << "ALL" << name << all.size(); foreach(QByteArray b, all.split('\n')) { if (b.indexOf("-----BEGIN ") == 0) @@ -63,7 +63,7 @@ void check_pems(const QString &name, int n, QStringList matches = QStringList()) QCOMPARE(begin, n); QCOMPARE(end, n); foreach(QString m, matches) { - qWarning() << QString("Pattern %1 not found in %2").arg(m).arg(name); + qDebug() << QString("Pattern %1 not found in %2").arg(m).arg(name); } QCOMPARE(matches.size(), 0); } @@ -76,7 +76,7 @@ void verify_key(const QString &name, QList hashes, bool priv) QCOMPARE(pems->get().size(), hashes.size()); foreach (pki_base *pki, pems->get()) { unsigned hash = pki->hash(); - qWarning() << pki->getIntName() << hash; + qDebug() << pki->getIntName() << hash; QVERIFY2(hashes.contains(hash), qPrintable(QString("%1 not expected in %2") .arg(pki->getIntName()) @@ -220,10 +220,11 @@ void test_main::exportFormat() export_by_id(4, file, list, certs); verify_key(file, QList { ROOT_HASH, INTER_HASH, END_HASH, ENDKEY_HASH }, true); - check_pems(file, 4, QStringList { " RSA PRIVATE KEY-", + check_pems(file, 5, QStringList { " RSA PRIVATE KEY-", " CERTIFICATE-", " CERTIFICATE-"," CERTIFICATE-", "", "", "", "", - "", "", "", "" }); + "", "", "", "", + "", "" }); // Export Endentity as PKCS#7 file = AUTOFILE(CERTP7) export_by_id(8, file, list, certs); @@ -235,9 +236,15 @@ void test_main::exportFormat() verify_file(file, QList { ROOT_HASH, INTER_HASH, END_HASH }); check_pems(file, 0); // Export Endentity as DER certificate + file = AUTOFILE(CERTDER) export_by_id(13, file, list, certs); verify_file(file, QList { END_HASH }); check_pems(file, 0); + // Export Endentity as OpenVPN config file + file = AUTOFILE(OPENVPNTA) + export_by_id(39, file, list, certs); + check_pems(file, 1, QStringList { + "BEGIN OpenVPN Static key V1", "END OpenVPN Static key V1" }); // Export Endentity key list.clear(); diff --git a/test/main.cpp b/test/main.cpp index 2d9a074a..076f5441 100644 --- a/test/main.cpp +++ b/test/main.cpp @@ -68,6 +68,7 @@ void test_main::openDB() new pw_expect("testdbpass", pw_ok), }); mainwin->close_database(); + QFile::remove("testdb.xdb"); Database.open("testdb.xdb"); Settings["pkcs12_keep_legacy"] = true; mainwin->setup_open_database();