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.
This commit is contained in:
Christian Hohnstaedt 2024-02-20 06:52:39 +01:00
parent 0dd587a3f2
commit 1199d776d2
11 changed files with 76 additions and 13 deletions

View File

@ -72,8 +72,8 @@ const QString &database_model::detect_provider()
QSqlError database_model::initSqlDB() QSqlError database_model::initSqlDB()
{ {
#define MAX_SCHEMAS 7 #define MAX_SCHEMAS 8
#define SCHEMA_VERSION "7" #define SCHEMA_VERSION "8"
QStringList schemas[MAX_SCHEMAS]; QStringList schemas[MAX_SCHEMAS];

View File

@ -305,6 +305,16 @@
<< "UPDATE settings SET value='7' WHERE key_='schema'" << "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 /* When adding new tables or views, also add them to the list
* in XSqlQuery::rewriteQuery(QString) in lib/sql.cpp * in XSqlQuery::rewriteQuery(QString) in lib/sql.cpp
*/ */

View File

@ -566,11 +566,11 @@ int db_x509::exportFlags(const QModelIndex &idx) const
return disable_flags; 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) { if (b.size() > 0) {
file.write(QString("<%1>\n").arg(tag).toLatin1()); file.write(QString("<%1>\n").arg(tag).toLatin1());
file.write(b); file.write(b.byteArray());
file.write(QString("</%1>\n").arg(tag).toLatin1()); file.write(QString("</%1>\n").arg(tag).toLatin1());
} }
} }
@ -609,6 +609,7 @@ void db_x509::exportItems(const QModelIndexList &list,
writeTaggedPEM(extra, "extra-certs", file); writeTaggedPEM(extra, "extra-certs", file);
writeTaggedPEM(cert, "cert", file); writeTaggedPEM(cert, "cert", file);
writeTaggedPEM(key, "key", file); writeTaggedPEM(key, "key", file);
writeTaggedPEM(crt->getTaKey().toLatin1(), "tls-auth", file);
} else if (xport->match_all(F_CHAIN)) { } else if (xport->match_all(F_CHAIN)) {
for (; crt && crt != oldcrt; oldcrt = crt, crt = crt->getSigner()) for (; crt && crt != oldcrt; oldcrt = crt, crt = crt->getSigner())
crt->writeCert(file, true); crt->writeCert(file, true);
@ -655,6 +656,8 @@ void db_x509::exportItems(const QModelIndexList &list,
writeVcalendar(file, vcal); writeVcalendar(file, vcal);
} else if (xport->match_all(F_CONFIG)) { } else if (xport->match_all(F_CONFIG)) {
crt->opensslConf(file); crt->opensslConf(file);
} else if (xport->match_all(F_TAKEY)) {
file.write(crt->getTaKey().toLatin1());
} }
} }

View File

@ -23,7 +23,7 @@ class db_x509: public db_x509super
{ {
Q_OBJECT Q_OBJECT
static void writeTaggedPEM(BioByteArray &b, static void writeTaggedPEM(const BioByteArray &b,
const QString &tag, XFile &file); const QString &tag, XFile &file);
protected: protected:
dbheaderList getHeaders(); dbheaderList getHeaders();

View File

@ -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(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(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(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(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")), 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")),

View File

@ -49,6 +49,7 @@ enum {
F_DER = BIT(19),/* DER format */ F_DER = BIT(19),/* DER format */
F_OVPN = BIT(20),/* OpenVPN format */ F_OVPN = BIT(20),/* OpenVPN format */
F_CONFIG = BIT(21),/* OpenSSL config format */ F_CONFIG = BIT(21),/* OpenSSL config format */
F_TAKEY = BIT(22),/* OpenVPN TLS-Auth key */
}; };
class pki_export : public QObject { class pki_export : public QObject {

View File

@ -17,6 +17,8 @@
#include "exception.h" #include "exception.h"
#include "pass_info.h" #include "pass_info.h"
#include <openssl/rand.h>
pki_x509::pki_x509(X509 *c) pki_x509::pki_x509(X509 *c)
:pki_x509super(), cert(c) :pki_x509super(), cert(c)
{ {
@ -143,6 +145,7 @@ QSqlError pki_x509::deleteSqlData()
<< "UPDATE crls SET issuer=NULL WHERE issuer=?" << "UPDATE crls SET issuer=NULL WHERE issuer=?"
<< "UPDATE certs SET issuer=NULL WHERE issuer=?" << "UPDATE certs SET issuer=NULL WHERE issuer=?"
<< "DELETE FROM revocations WHERE caId=?" << "DELETE FROM revocations WHERE caId=?"
<< "DELETE FROM takeys WHERE item=?"
; ;
foreach(QString task, tasks) { foreach(QString task, tasks) {
SQL_PREPARE(q, task); SQL_PREPARE(q, task);
@ -273,6 +276,42 @@ pki_x509 *pki_x509::getBySerial(const a1int &a) const
return NULL; 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 a1int pki_x509::hashInfo(const EVP_MD *md) const
{ {
unsigned char digest[EVP_MAX_MD_SIZE]; unsigned char digest[EVP_MAX_MD_SIZE];

View File

@ -163,6 +163,7 @@ class pki_x509 : public pki_x509super
void restoreSql(const QSqlRecord &rec); void restoreSql(const QSqlRecord &rec);
QStringList icsVEVENT() const; QStringList icsVEVENT() const;
QStringList icsVEVENT_ca() const; QStringList icsVEVENT_ca() const;
QString getTaKey();
}; };
Q_DECLARE_METATYPE(pki_x509 *); Q_DECLARE_METATYPE(pki_x509 *);

View File

@ -125,7 +125,7 @@ QString XSqlQuery::rewriteQuery(QString _q)
"items" , "crls" , "private_keys" , "public_keys" , "items" , "crls" , "private_keys" , "public_keys" ,
"tokens" , "token_mechanism" , "templates" , "certs" , "tokens" , "token_mechanism" , "templates" , "certs" ,
"authority" , "revocations" , "requests" , "x509super" , "authority" , "revocations" , "requests" , "x509super" ,
"settings" , "settings" , "takeys",
"view_public_keys" , "view_certs" , "view_requests" , "view_public_keys" , "view_certs" , "view_requests" ,
"view_crls" , "view_templates" , "view_private", "view_crls" , "view_templates" , "view_private",

View File

@ -24,14 +24,14 @@
void check_pems(const QString &name, int n, QStringList matches = QStringList()) void check_pems(const QString &name, int n, QStringList matches = QStringList())
{ {
int begin = 0, end = 0; int begin = 0, end = 0;
qWarning() << "Expecting" << n << "PEMs in" << name; qDebug() << "Expecting" << n << "PEMs in" << name;
#if 0 #if 0
// This is an endless loop: open_read() succeeds, // This is an endless loop: open_read() succeeds,
// but isOpen returns false. Stop investigating, use POSIX open() // but isOpen returns false. Stop investigating, use POSIX open()
XFile F(name); XFile F(name);
while (!F.isOpen()) { while (!F.isOpen()) {
qWarning() << "OPEN" << name; qDebug() << "OPEN" << name;
F.close(); F.close();
Q_ASSERT(F.open_read()); Q_ASSERT(F.open_read());
} }
@ -45,7 +45,7 @@ void check_pems(const QString &name, int n, QStringList matches = QStringList())
QByteArray all(buf, ret); QByteArray all(buf, ret);
close(fd); close(fd);
#endif #endif
qWarning() << "ALL" << name << all.size(); qDebug() << "ALL" << name << all.size();
foreach(QByteArray b, all.split('\n')) { foreach(QByteArray b, all.split('\n')) {
if (b.indexOf("-----BEGIN ") == 0) if (b.indexOf("-----BEGIN ") == 0)
@ -63,7 +63,7 @@ void check_pems(const QString &name, int n, QStringList matches = QStringList())
QCOMPARE(begin, n); QCOMPARE(begin, n);
QCOMPARE(end, n); QCOMPARE(end, n);
foreach(QString m, matches) { 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); QCOMPARE(matches.size(), 0);
} }
@ -76,7 +76,7 @@ void verify_key(const QString &name, QList<unsigned> hashes, bool priv)
QCOMPARE(pems->get().size(), hashes.size()); QCOMPARE(pems->get().size(), hashes.size());
foreach (pki_base *pki, pems->get()) { foreach (pki_base *pki, pems->get()) {
unsigned hash = pki->hash(); unsigned hash = pki->hash();
qWarning() << pki->getIntName() << hash; qDebug() << pki->getIntName() << hash;
QVERIFY2(hashes.contains(hash), QVERIFY2(hashes.contains(hash),
qPrintable(QString("%1 not expected in %2") qPrintable(QString("%1 not expected in %2")
.arg(pki->getIntName()) .arg(pki->getIntName())
@ -220,10 +220,11 @@ void test_main::exportFormat()
export_by_id(4, file, list, certs); export_by_id(4, file, list, certs);
verify_key(file, QList<unsigned> { verify_key(file, QList<unsigned> {
ROOT_HASH, INTER_HASH, END_HASH, ENDKEY_HASH }, true); 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-", " CERTIFICATE-", " CERTIFICATE-"," CERTIFICATE-",
"<ca>", "</ca>", "<extra-certs>", "</extra-certs>", "<ca>", "</ca>", "<extra-certs>", "</extra-certs>",
"<cert>", "</cert>", "<key>", "</key>" }); "<cert>", "</cert>", "<key>", "</key>",
"<tls-auth>", "</tls-auth>" });
// Export Endentity as PKCS#7 // Export Endentity as PKCS#7
file = AUTOFILE(CERTP7) file = AUTOFILE(CERTP7)
export_by_id(8, file, list, certs); export_by_id(8, file, list, certs);
@ -235,9 +236,15 @@ void test_main::exportFormat()
verify_file(file, QList<unsigned> { ROOT_HASH, INTER_HASH, END_HASH }); verify_file(file, QList<unsigned> { ROOT_HASH, INTER_HASH, END_HASH });
check_pems(file, 0); check_pems(file, 0);
// Export Endentity as DER certificate // Export Endentity as DER certificate
file = AUTOFILE(CERTDER)
export_by_id(13, file, list, certs); export_by_id(13, file, list, certs);
verify_file(file, QList<unsigned> { END_HASH }); verify_file(file, QList<unsigned> { END_HASH });
check_pems(file, 0); 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 // Export Endentity key
list.clear(); list.clear();

View File

@ -68,6 +68,7 @@ void test_main::openDB()
new pw_expect("testdbpass", pw_ok), new pw_expect("testdbpass", pw_ok),
}); });
mainwin->close_database(); mainwin->close_database();
QFile::remove("testdb.xdb");
Database.open("testdb.xdb"); Database.open("testdb.xdb");
Settings["pkcs12_keep_legacy"] = true; Settings["pkcs12_keep_legacy"] = true;
mainwin->setup_open_database(); mainwin->setup_open_database();