Implementierung eines SSL Servers und Clients und Authentizitätsprüfung mit Zertifikaten
Der Artikel Sinnige/richtige Implementierung eines Multi-Threaded TCP-Servers mit Qt erklärt ja bereits die Grundlagen, wie man einen TCP-Server mit Qt programmiert. Ausgehend von diesem Artikel soll nun erklärt werden, wie man verschlüsselte Kommunikation mit SSL-Sockets hinzufügt und welche Zertifikate man braucht und wie man die erstellt.
Hier erstmal kurz das angestrebte Szenario:
- wir brauchen einen Server, der auf einer bestimmten Adresse lauscht (ein DNS-Eintrag, wie z.B. server.meine-domain.de) und verschlüsselt mit Clients kommuniziert
- dann brauchen wir noch einen Client, der sich mit dem Server verbindet und dann eine verschlüsselte Verbindung aufbaut
- zusätzlich soll der Client noch prüfen können, ob der Server wirklich derjenige ist, der an der bestimmten Adresse erwartet wird
- außerdem soll der Server prüfen können, ob der Client auch wirklich der richtige Client ist und nicht irgendeine andere Software
Ein komplettes Beispiel gibt es als Quelltext-Archiv: SSLServerClient-Example.7z
Grundlegendes zu den Begrifflichkeiten
Zertifikate, Wurzelzertifikate, Zertifikatautoritäten und private Schlüssel… was ist das alles:
| Deutscher Begriff | Englischer Begriff | Erläuterung |
|---|---|---|
| Zertifikat | Certificate | Eine digitale Datei, die die Identität einer Person, eines Servers oder einer Organisation bestätigt. Sie enthält den öffentlichen Schlüssel des Inhabers sowie Angaben zum Aussteller und ist digital signiert. |
| Zertifikatautorität | Certificate Authority (CA) | Eine vertrauenswürdige Organisation, die Zertifikate ausstellt und deren Echtheit mit ihrer Signatur bestätigt. Man unterscheidet öffentliche CAs (z. B. Let’s Encrypt) und private CAs für den internen Einsatz. |
| Wurzelzertifikat | Root Certificate | Das oberste Zertifikat in einer Zertifikatskette, das von einer Wurzel-Zertifikatautorität selbst signiert ist. Betriebssysteme und Browser liefern eine Liste vertrauenswürdiger Wurzelzertifikate mit. |
| Zwischenzertifikat | Intermediate Certificate | Ein Zertifikat, das zwischen dem Wurzelzertifikat und dem eigentlichen Server-Zertifikat in der Kette steht. Es wird von der CA ausgestellt, um das Wurzelzertifikat nicht direkt einzusetzen und so das Risiko zu verringern. |
| Privater Schlüssel | Private Key | Der geheime Teil eines asymmetrischen Schlüsselpaares. Er wird zum Entschlüsseln eingehender Nachrichten und zum Erstellen digitaler Signaturen verwendet und darf niemals weitergegeben werden. |
| Öffentlicher Schlüssel | Public Key | Der frei verteilbare Teil eines asymmetrischen Schlüsselpaares. Er ist im Zertifikat enthalten und wird von Gegenstellen genutzt, um Nachrichten zu verschlüsseln oder Signaturen zu prüfen. |
| Zertifikatsanforderung | Certificate Signing Request (CSR) | Eine Anforderungsdatei, die ein Antragsteller erstellt und an eine CA schickt. Sie enthält den öffentlichen Schlüssel und Angaben zum Inhaber; die CA prüft diese und stellt daraufhin das signierte Zertifikat aus. |
| Zertifikatskette | Certificate Chain | Die geordnete Abfolge von Zertifikaten vom Server-Zertifikat über eventuelle Zwischenzertifikate bis hin zum Wurzelzertifikat. Eine Gegenstelle prüft die gesamte Kette, um die Vertrauenswürdigkeit sicherzustellen. |
Kurze Erläuterung an einem Beispiel:
- Nehmen wir mal an, wir haben eine Zertifikatautorität, z.B. mich selbst. Ich hab dann ein Wurzelzertifikat. Ich kann dann weitere Zertifikate als echt signieren. Jedes von mir signierte Zertifikat kann dann weitere Zertifikate als echt signieren. So entsteht eine Zertifikatskette.
- Wenn ich dem Inhaber und dessen Zertifikat vertraue, dann vertraue ich damit indirekt auch allen nachfolgenden Zertifikaten in der Kette.
- Da jedes Zertifikat seinen Unterzeichner/Beglaubiger kennt, kann ich mich ausgehend von einem beliebigen Zertifikat so lange nach oben “hangeln”, bis ich ein Zertifikat finde, welchem ich vertraue. In der Regel lande ich beim Wurzelzertifikat und prüfe dann in einer Liste von vertrauenswürdigen Wurzelzertifikaten nach, ob das darin enthalten ist. Eine solche Liste von vertrauenswürdigen Wurzelzertifikaten wird mit Betriebssystemen mitgeliefert.
Öffentliche und private Schlüssel
Der öffentliche und private Schlüssel gehören zu einer asymmetrischen Verschlüsselung. Mit dem öffentlichen Schlüssel kann der Sender den Nachrichteninhalt verschlüsseln. Dieser lässt sich aber nur unter Zuhilfenahme des privaten Schlüssels zügig entschlüsseln - selbst auf Supercomputern dauert das sonst ewig, alle Varianten auszuprobieren. Je länger der Schlüssel ist, umso schwieriger ist es, diesen durch Ausprobieren (brute force) zu knacken.
Ohne den privaten Schlüssel ist die Nachricht also sicher, weswegen der private Schlüssel auch sehr gut geschützt werden muss. Bei einer Client-Server-Kommunikation darf der ausschließlich auf dem (zugriffsbeschränkten) Server liegen, und nicht im Client-Programm verteilt werden (dies ist einer der häufigsten Fehler, welche die Verschlüsselung kompromittieren).
Was passiert beim Aufbau einer SSL-Verbindung
Wenn ein Client sich mit einem Server verbindet, läuft grob folgendes ab:
- Client öffnet Verbindung zum Server.
- Server schickt sein Zertifikat. Dieses enthält unter anderem den öffentlichen Schlüssel des Servers.
- Client prüft das Zertifikat. Er hangelt sich die Zertifikatskette nach oben und prüft, ob er einem der Zertifikate in der Kette vertraut (in der Regel dem Wurzelzertifikat in seiner vertrauenswürdigen Liste).
- Client erzeugt ein Geheimnis. Der Client erstellt einen zufälligen Wert (das sogenannte Pre-Master-Secret), verschlüsselt diesen mit dem öffentlichen Schlüssel des Servers und schickt ihn an den Server.
- Server entschlüsselt das Geheimnis. Nur der Server kann das Pre-Master-Secret entschlüsseln, weil nur er den passenden privaten Schlüssel besitzt. Damit kennen jetzt beide Seiten dasselbe Geheimnis – ohne dass es je im Klartext übertragen wurde.
- Beide leiten einen symmetrischen Sitzungsschlüssel ab. Aus dem gemeinsamen Geheimnis berechnen Client und Server unabhängig voneinander denselben symmetrischen Schlüssel. Die gesamte weitere Kommunikation wird mit diesem Schlüssel verschlüsselt.
Der Grund für dieses zweistufige Vorgehen:
Asymmetrische Verschlüsselung (öffentlicher/privater Schlüssel) ist rechenintensiv und daher langsam. Sie wird deshalb nur beim Verbindungsaufbau eingesetzt, um sicher einen gemeinsamen symmetrischen Schlüssel auszuhandeln. Die eigentliche Datenkommunikation läuft dann mit der wesentlich schnelleren symmetrischen Verschlüsselung.
Beispielimplementierung des Servers
Die main.cpp ist nahezu identisch zu dem oben verlinkten TCP-Server-Beispiel. Auch die Server-Klasse sieht nahezu gleich aus. Die Funktion QTcpServer::listen() ist überladen, um die SSL-Konfiguration für alle zukünftigen SSL-Verbindungen zu erstellen.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*! SslServer implements QTcpServer interface and adds QSslConfiguration to be used
when sockets are being created.
*/
class SslServer : public QTcpServer {
Q_OBJECT
public:
explicit SslServer(QObject *parent = nullptr);
/*! Overloaded to configure SSL parameters. */
bool listen(quint16 port);
protected:
void incomingConnection(qintptr socketDescriptor) override;
private slots:
void onThreadFinished();
private:
QSslConfiguration m_sslConfig;
QSet<SslSocketThread *> m_threads;
};
In der Funktion listen() werden nun das Server-Zertifikat und der dazugehörige private Schlüssel aus der Ressourcendatei gelesen und die Klasse QSslConfiguration damit parametriert:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
bool SslServer::listen(quint16 port) {
// read server certificate and private key files from resources
QFile certFile(QStringLiteral(":/Testserver_auf_Localhost_und_127.0.0.1.crt"));
QFile keyFile(QStringLiteral(":/Serverschlüssel.pem"));
if (!certFile.open(QIODevice::ReadOnly)) {
qCritical() << "Cannot open certificate file.";
return false;
}
if (!keyFile.open(QIODevice::ReadOnly)) {
qCritical() << "Cannot open key file.";
return false;
}
QSslCertificate certificate(&certFile, QSsl::Pem);
QSslKey privateKey(&keyFile, QSsl::Rsa, QSsl::Pem); // RSA type private key
certFile.close();
keyFile.close();
if (certificate.isNull() || privateKey.isNull()) {
qCritical() << "Failed to load certificate or private key.";
return false;
}
// configure QSslConfiguration to be used for each QSslSocket to be created
m_sslConfig = QSslConfiguration::defaultConfiguration();
m_sslConfig.setLocalCertificate(certificate);
m_sslConfig.setPrivateKey(privateKey);
// For now we do not check the client
m_sslConfig.setPeerVerifyMode(QSslSocket::VerifyNone);
return QTcpServer::listen(QHostAddress::Any, port);
}
Wie man Zertifikate und private Schlüssel erstellt, ist im Abschnitt weiter unten zur Erstellung von Zertifikaten beschrieben.
Bevor der Server mit dem abschließenden Aufruf von QTcpServer::listen() tatsächlich auf eingehende Verbindungen lauscht, wird noch das für jeden SSL-Socket benötigte QSslConfiguration-Objekt vorbereitet.
Ausgehend von der Standardkonfiguration QSslConfiguration::defaultConfiguration() werden das Zertifikat und der private Schlüssel gesetzt.
In der ersten Fassung soll der Server den Client noch nicht verifizieren. Daher wird QSslSocket::VerifyNone für die serverseitigen Sockets gesetzt.
Erstellung von SSL-Sockets
Sobald eine eingehende Verbindung erkannt wird, ruft der QTcpServer die Funktion incomingConnection() auf:
1
2
3
4
5
6
7
void SslServer::incomingConnection(qintptr socketDescriptor) {
// create new socket thread and pass on SSL configuration
SslSocketThread *thread = new SslSocketThread(socketDescriptor, m_sslConfig, this);
connect(thread, &SslSocketThread::finished, this, &SslServer::onThreadFinished);
m_threads.insert(thread);
thread->start();
}
Diese Funktion ist nahezu identisch zur Variante beim Multi-Threaded TCP-Server (siehe Link oben), nur wird diesmal das vorbereitete QSslConfiguration-Objekt im Konstruktor mitgegeben.
Das Aufräumen nach Ende des Threads ist unverändert:
1
2
3
4
5
void SslServer::onThreadFinished() {
SslSocketThread *thread = qobject_cast<SslSocketThread *>(sender());
m_threads.remove(thread);
thread->deleteLater();
}
SslSocketThread und der Aufbau der verschlüsselten Verbindung
Die Klasse SslSocketThread ist fast identisch mit der Klasse SocketThread aus dem Multi-Threaded TCP-Server Beispiel, verwendet jetzt jedoch ein QSslSocket:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class SslSocketThread : public QThread {
Q_OBJECT
public:
explicit SslSocketThread(qintptr socketDescriptor,
const QSslConfiguration &sslConfig,
QObject *parent = nullptr);
~SslSocketThread() override; // memory cleanup
protected:
void run() override;
private slots:
void onEncrypted();
void onReadyRead();
void onDisconnected();
void onSslErrors(const QList<QSslError> &errors);
private:
qintptr m_socketDescriptor;
QSslConfiguration m_sslConfig;
QSslSocket *m_socket = nullptr;
};
Konstruktor und Destruktor sind minimalistisch:
1
2
3
4
5
6
7
8
9
10
11
12
13
SslSocketThread::SslSocketThread(qintptr socketDescriptor,
const QSslConfiguration &sslConfig,
QObject *parent)
: QThread(parent)
, m_socketDescriptor(socketDescriptor)
, m_sslConfig(sslConfig)
{}
SslSocketThread::~SslSocketThread() {
// free allocated memory
delete m_socket;
}
Interessanter ist die run()-Funktion des Threads:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void SslSocketThread::run() {
m_socket = new QSslSocket();
m_socket->setSslConfiguration(m_sslConfig); // set QSslConfiguration
if (!m_socket->setSocketDescriptor(m_socketDescriptor)) {
qCritical() << "Couldn't set socket descriptor";
return;
}
// Qt::DirectConnection is required here: signal-slot connections inside a
// QThread subclass default to QueuedConnection, which would dispatch the
// slots back to the main thread instead of this worker thread.
connect(m_socket, &QSslSocket::readyRead, this, &SslSocketThread::onReadyRead, Qt::DirectConnection);
connect(m_socket, &QSslSocket::disconnected, this, &SslSocketThread::onDisconnected, Qt::DirectConnection);
// two more signal connections for SSL connections
connect(m_socket, &QSslSocket::encrypted, this, &SslSocketThread::onEncrypted, Qt::DirectConnection);
connect(m_socket, &QSslSocket::sslErrors, this, &SslSocketThread::onSslErrors, Qt::DirectConnection);
m_socket->startServerEncryption();
exec();
}
Wiederum ist die Funktion nahezu identisch mit der QTcpSocket-Variante, mit folgenden Unterschieden:
setSslConfiguration()gibt dem Socket die vorbereitete SSL-Konfiguration, d.h. Serverzertifikat und -schlüssel- zwei weitere Signale können verknüpft werden, um Statusinformationen und Fehler im Zusammenhang mit der Verschlüsselung abzugreifen
- der Aufruf
startServerEncryption()initialisiert die verschlüsselte Verbindung
All die Details sind sehr bequem in der Klasse QSslSocket gekapselt.
Die Slots sind selbsterklärend:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void SslSocketThread::onEncrypted() {
qInfo() << "Encrypted connection established with" << m_socket->peerAddress().toString();
}
void SslSocketThread::onReadyRead() {
const QByteArray data = m_socket->readAll();
qInfo() << "Received:" << data;
m_socket->write(data);
}
void SslSocketThread::onDisconnected() {
qInfo() << "Client disconnected:" << m_socket->peerAddress().toString();
if (m_socket->error() != QAbstractSocket::RemoteHostClosedError)
qWarning() << m_socket->error() << ":" << m_socket->errorString();
// leave event loop -> sends finished() signal
exit(0);
}
void SslSocketThread::onSslErrors(const QList<QSslError> &errors) {
for (const QSslError &e : errors)
qWarning() << "SSL error on" << m_socket->peerAddress().toString() << ":" << e.errorString();
}
Im Slot onDisconnected() habe ich noch eine Fehlermeldung eingebaut. Normalerweise wird die Verbindung vom Client gekappt, welches dann den Fehlerstatus QAbstractSocket::RemoteHostClosedError setzt (eigentlich kein Fehler, sondern meist erwartetes Verhalten).
Falls was anderes schiefgegangen ist, wird eine Warnung mit der Fehlermeldung ausgegeben.
Das war’s auch schon für den Server. Diesen kann man nun ausführen.
Testen mit telnet und openssl
Wenn man einen SSL-Server versucht, mit einer Telnetverbindung zu erreichen, also z.B. mit
1
telnet localhost 1234
liefert der Server folgende Ausgabe:
1
2
3
SSL server listening on port 1245
Client disconnected: "::ffff:127.0.0.1"
QAbstractSocket::SocketError(13) "Error during SSL handshake: error:0A0000C6:SSL routines::packet length too long, error:0A000139:SSL routines::record layer failure"
Ist ja auch klar, denn der Server erwartet den Beginn des Handshake-Prozesses.
Man kann die SSL-Verbindung stattdessen mit openssl testen und erhält dann sehr detaillierte Informationen über den Handshake-Prozess und die beteiligten Zertifikate:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
$ openssl s_client -connect localhost:1234
Connecting to 127.0.0.1
CONNECTED(00000003)
Can't use SSL_get_servername
depth=0 C=de, O=Schneggenport, CN=localhost
verify error:num=20:unable to get local issuer certificate
verify return:1
depth=0 C=de, O=Schneggenport, CN=localhost
verify error:num=21:unable to verify the first certificate
verify return:1
depth=0 C=de, O=Schneggenport, CN=localhost
verify return:1
---
Certificate chain
0 s:C=de, O=Schneggenport, CN=localhost
i:O=Schneggenport, CN=schneggenport-root-ca
a:PKEY: RSA, 4096 (bit); sigalg: sha256WithRSAEncryption
v:NotBefore: May 19 17:22:00 2026 GMT; NotAfter: May 19 17:22:00 2027 GMT
---
Server certificate
-----BEGIN CERTIFICATE-----
MIIFBjCCAu6gAwIBAgIIC0mzm6OB5qswDQYJKoZIhvcNAQELBQAwODEWMBQGA1UE
ChMNU2NobmVnZ2VucG9ydDEeMBwGA1UEAxMVc2NobmVnZ2VucG9ydC1yb290LWNh
MB4XDTI2MDUxOTE3MjIwMFoXDTI3MDUxOTE3MjIwMFowOTELMAkGA1UEBhMCZGUx
FjAUBgNVBAoTDVNjaG5lZ2dlbnBvcnQxEjAQBgNVBAMTCWxvY2FsaG9zdDCCAiIw
DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAN0wxdr7JFa8YtCrvr38TyhqNFFU
Wlt6UR1zN++x0lZOtgcMxWGXoxexd29fhuuug8aPY5b85x4Fjmduf9npk+DVm7l0
VksGZC3yfPcCpsEpomg2vjZVJZOF9No4RchMNitsB+4aIQi5cRJLef84SuOcA26+
YcchIIYOMELdEP5xRK9SlbkCNsb7U0LYWn3JoselFSZghl5OsJB5K1d1ffybmUCu
/XjiSTH4h5ngd+ddeXlBxTKzb+/wObPN+URcygH1ZTIoBYhkUzWdRqLtWnoeh3eR
SqwIG6WQ4KhV4zFdiATDDKYg09bOqOnkJynjSsiZGB1GAU5uXwM7XCdc0Z3Xr8Up
zAlL6OhaIsK67BPH6iOYgW4bhtkeynkLn/br/cO3qZW2AfVQY7Oa7pjBzUkDMn+5
bUw4gqnHSSYYnHMGgaZHsR4JxIWyNXfE3B9oAnkQ04GK02hYr2dkDMMDRt5KJ1Kk
MGEol0JkCaUvBvC3IIAO3XXvTOcW6VX9v3ElMThI8M5VrGvtRVUhiv+lcGOQg7Lc
DQIWBqsUpxcn9lOXtwx41cqIdTeI1yCGB6Bvgu6LLLTtAMOddeOpYYeswLWUUumU
TpuSPbTFocMBI+/k4JUEKvWT8UOGujVWCMwnxl09jENoyHD62ekR8pUCT2sS0S9A
lu9A+qlU3vixtOiNAgMBAAGjEzARMA8GA1UdEQQIMAaHBH8AAAEwDQYJKoZIhvcN
AQELBQADggIBAILZhguOVG5byFeA848og1QkluJU2FSrn+AQPVqv5BehthT3p+5e
FW7syX1mwEXKDzWbUfPZuMTAloMDHTXiFtFf85TMO8S9Vnoxt3glunqAhLX5xIcT
tfA5QPS7CIMPnxElkwsWPfIbxm8GnzxYsGZbZQyIfwEPbEYJusmIFqhWm79dhIDj
zBnFBWG8MkVFb0tgRI5dTNFG+QwtCM/ZUpBpb+XAOb78kkFatejWudarzKOP4X3u
2mhWJVrvbUOWHa8kJeD9fCGOqT+8DlKahk3oUlA1pycEotYhphqGvLIwQyEVSlfv
cU6a53oJvcJnEwtwcg1FcOv++msvYubRov5zEfegsO5J2oPOKuKpumhXm5wngd/f
/07FyScYnJyi0xm3ZwqyIH1sPjJMDJqSJHK5/ewWvmXyCagm3Hfk4FWhatwH6WtL
WR8GqYy6FucQBFDs5st33DaANnGzLggroAX5roNuJ6Why7UGAXjVllPtBB4Hm30+
NaE6lxHfv7lBtC2QtFZOGFwgRoPiLpNRzn1QuMSfQ6ZW56pVP7qUymdntTTZ1oMn
nET2UGAQgZPIVb61McQQdzkv0nBBDGyzlTHAbs5DwRhINRbyiq5PNc6UPAR7mvev
Uk+CMe+PZMWR+72CmWh/Dc/7cm+SaquJKcw6yi7RXYE+fgKeCObR3+s4
-----END CERTIFICATE-----
subject=C=de, O=Schneggenport, CN=localhost
issuer=O=Schneggenport, CN=schneggenport-root-ca
---
No client certificate CA names sent
Peer signing digest: SHA256
Peer signature type: rsa_pss_rsae_sha256
Negotiated TLS1.3 group: X25519MLKEM768
---
SSL handshake has read 3190 bytes and written 1613 bytes
Verification error: unable to verify the first certificate
---
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
Protocol: TLSv1.3
Server public key is 4096 bit
This TLS version forbids renegotiation.
Compression: NONE
Expansion: NONE
No ALPN negotiated
Early data was not sent
Verify return code: 21 (unable to verify the first certificate)
---
---
Post-Handshake New Session Ticket arrived:
SSL-Session:
Protocol : TLSv1.3
Cipher : TLS_AES_256_GCM_SHA384
Session-ID: 98C1A1FBAE3D9A3EAAD3FF3B42D816B4BD00F8EBDBD497370CD60DA17A947F5C
Session-ID-ctx:
Resumption PSK: 3E84AAB4A33123FAA389453BF84FAC53EE352370ECC45F79422049EAD1B59A7073CF332E9C6A9E17C23677E8EF14B657
PSK identity: None
PSK identity hint: None
SRP username: None
TLS session ticket lifetime hint: 7200 (seconds)
TLS session ticket:
0000 - d7 32 99 ad c1 06 75 a5-0a 42 bb 42 22 08 e5 77 .2....u..B.B"..w
0010 - 2a 39 e8 cf 88 18 12 18-df 55 53 12 0d 77 75 49 *9.......US..wuI
0020 - 4f 54 f8 d7 c2 17 72 9d-6c 3f 22 8e c3 0a d9 a5 OT....r.l?".....
0030 - c0 f8 93 d8 01 cc b1 b6-a6 f0 75 1a b5 9d 7b 85 ..........u...{.
0040 - 7b 2a 01 19 15 a8 88 bd-30 8d ac 3c 54 0b 42 86 {*......0..<T.B.
0050 - 1e c1 4f d1 16 53 e4 a4-19 6f 3c 66 d0 1f a9 92 ..O..S...o<f....
0060 - 4f a5 08 6d 27 24 b7 a2-c3 98 d4 f8 a5 40 67 b9 O..m'$.......@g.
0070 - 72 68 f6 53 88 63 e6 b8-37 1a 66 1d 49 da 40 08 rh.S.c..7.f.I.@.
0080 - f8 8f a4 7b d2 e1 93 63-3b eb 0e 50 b5 78 7f 16 ...{...c;..P.x..
0090 - b6 ab d4 db 34 43 78 65-04 9f 65 92 2a 10 e2 fb ....4Cxe..e.*...
00a0 - 02 8f 26 87 35 89 f8 14-53 38 c2 92 be fb 54 ce ..&.5...S8....T.
00b0 - 5b 05 b7 6a e8 d2 4d 14-c0 d9 5c a3 c6 58 fe 62 [..j..M...\..X.b
00c0 - 19 ec 8d 5b 52 d1 8d 11-69 d2 89 01 b2 6b 21 30 ...[R...i....k!0
Start Time: 1779269764
Timeout : 7200 (sec)
Verify return code: 21 (unable to verify the first certificate)
Extended master secret: no
Max Early Data: 0
---
read R BLOCK
---
Post-Handshake New Session Ticket arrived:
SSL-Session:
Protocol : TLSv1.3
Cipher : TLS_AES_256_GCM_SHA384
Session-ID: 9907DE404FDF3CCE90D1C68328A5383B4E836FF1572D89FE9C676DD08A91C78A
Session-ID-ctx:
Resumption PSK: 2CD5289923235EBD6C4803FE803A2F2D797F82A67130FE84A060213787A859EA2B9144456F66F2B50CEC0EC551677D52
PSK identity: None
PSK identity hint: None
SRP username: None
TLS session ticket lifetime hint: 7200 (seconds)
TLS session ticket:
0000 - d7 32 99 ad c1 06 75 a5-0a 42 bb 42 22 08 e5 77 .2....u..B.B"..w
0010 - 4c e4 8e 67 ec 71 d2 02-45 af b6 77 df e0 15 56 L..g.q..E..w...V
0020 - e2 ef ac 8f 5e 05 8f f6-68 28 cb b3 a7 a5 21 d2 ....^...h(....!.
0030 - e3 ee 58 f1 cb 67 74 c3-8e e2 7e 34 97 0c 3c 60 ..X..gt...~4..<`
0040 - 21 0c 99 07 4d e3 a0 9a-85 ac 82 66 05 c0 f3 ef !...M......f....
0050 - ab b8 e3 6b e8 31 95 43-07 80 24 0d eb 7b c3 88 ...k.1.C..$..{..
0060 - cb f9 41 0c 75 a2 40 d3-16 2b cf 03 76 84 d8 05 ..A.u.@..+..v...
0070 - 7d 23 a7 9e 18 a6 53 df-75 8f 68 c0 c6 67 17 3f }#....S.u.h..g.?
0080 - ff 2f 9c 58 1e ed 7c 23-e9 b9 c4 78 7b 6b 69 01 ./.X..|#...x{ki.
0090 - 4a 2c ed 83 11 84 00 23-63 f6 61 8e e6 8a 47 d0 J,.....#c.a...G.
00a0 - 9b fc db 7c 51 6d 49 e3-49 6a ba 95 7e 64 e8 e0 ...|QmI.Ij..~d..
00b0 - 6f c7 1c 32 03 93 91 a8-57 53 3d 1b 85 51 a7 14 o..2....WS=..Q..
00c0 - af a2 bd f8 04 9e 38 6d-42 e6 78 20 8c e4 88 4f ......8mB.x ...O
Start Time: 1779269764
Timeout : 7200 (sec)
Verify return code: 21 (unable to verify the first certificate)
Extended master secret: no
Max Early Data: 0
---
read R BLOCK
Mit Ctrl+C bricht man die Verbindung ab.
Beispielimplementierung des Clients
Nun schauen wir uns die Implementierung eines passenden Client-Programms an, welches nicht nur eine SSL-Verbindung aufbaut, sondern auch noch das Zertifikat des Servers prüft.
Wir beginnen wieder mit der main.cpp:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include "SslClient.h"
#include <QCoreApplication>
int main(int argc, char *argv[]) {
QCoreApplication app(argc, argv);
SslClient client;
QObject::connect(&client, &SslClient::finished, &app, &QCoreApplication::quit);
client.connectToServer("localhost", 1234, "Hello from SSL client!");
// client.connectToServer("127.0.0.1", 1234, "Hello from SSL client!");
// client.connectToServer("192.168.178.25", 1234, "Hello from SSL client!");
return app.exec();
}
Die ganze Funktionalität ist in der Klasse SslClient gekapselt.
Das Server-Zertifikat (siehe Abschnitt unten zur Erstellung von Zertifikaten) ist für Hostname localhost und IP 127.0.0.1 ausgestellt. Bei mir ist der Rechner aber auch über die via DHCP vergebene IP 192.168.178.25 erreichbar. Verbindungsversuche zu localhost und 127.0.0.1 sollten gelingen, jedoch sollte der Client eine Verbindung zu 192.168.178.25 verwerfen, da diese IP nicht zum Serverzertifikat passt. Um das zu testen, hat die main() drei alternative Aufrufe.
Client-SSL-Konfiguration
Damit der Client die Authentizität des Servers prüfen kann, muss er zunächst dessen Zertifikat testen. Das gelingt, indem man sich in der Zertifikatskette bis zu einem vertrauenswürdigen Zertifikat hochhangelt. Da es sich um ein selbst ausgestelltes Zertifikat handelt, muss man dem Client also das vertrauenswürdige Wurzelzertifikat geben:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
SslClient::SslClient(QObject *parent)
: QObject(parent)
{
QSslConfiguration config = QSslConfiguration::defaultConfiguration();
// We read our Root CA certificate and use VerifyPeer to verify the server
QFile rootCertFile(QStringLiteral(":/Schneggenport-Root-CA.crt"));
if (!rootCertFile.open(QIODevice::ReadOnly)) {
qCritical() << "Cannot open certificate";
return;
}
QSslCertificate certificate(&rootCertFile, QSsl::Pem);
// set root certificate
config.addCaCertificate( certificate );
// enable peer verification
config.setPeerVerifyMode(QSslSocket::VerifyPeer);
m_socket.setSslConfiguration(config);
connect(&m_socket, &QSslSocket::encrypted, this, &SslClient::onEncrypted);
connect(&m_socket, &QSslSocket::readyRead, this, &SslClient::onReadyRead);
connect(&m_socket, &QSslSocket::disconnected, this, &SslClient::onDisconnected);
connect(&m_socket, &QSslSocket::sslErrors, this, &SslClient::onSslErrors);
connect(&m_socket, &QSslSocket::errorOccurred, this, &SslClient::onErrorOccurred);
}
Das Wurzelzertifikat Schneggenport-Root-CA.crt, welches das Serverzertifikat signiert hat (siehe Abschnitt unten zur Erstellung von Zertifikaten), wird mittels addCaCertificate(() zur Liste der vertrauenswürdigen Wurzelzertifikate hinzugefügt. Außerdem wird der Client aufgefordert, den Server zu prüfen: setPeerVerifyMode(QSslSocket::VerifyPeer). Analog zum SSL-Server werden die relevanten Signale der QSslSocket-Klasse verknüpft.
Die dazugehörigen Slots sehen so aus:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void SslClient::onEncrypted() {
qInfo() << "Handshake complete. Sending:" << m_message;
m_socket.write(m_message);
}
void SslClient::onReadyRead() {
const QByteArray response = m_socket.readAll();
qInfo() << "Server echoed:" << response;
m_socket.disconnectFromHost();
}
void SslClient::onDisconnected() {
qInfo() << "Disconnected.";
emit finished();
}
void SslClient::onSslErrors(const QList<QSslError> &errors) {
for (const QSslError &error : errors)
qWarning() << "SSL error:" << error.errorString();
}
void SslClient::onErrorOccurred(QAbstractSocket::SocketError error) {
qCritical() << "Socket error:" << m_socket.errorString() << "(code" << error << ")";
emit finished();
}
In onReadyRead() wird die Serverantwort ausgegeben und danach die Verbindung beendet.
Nach Schließen der Verbindung wird onDisconnected() aufgerufen, was selbst wieder nur das finished() Signal aussendet (welches die Event-Loop des Client-Programms beendet).
Im Falle eines Kommunikationsabbruchs oder sonstigen Fehlers wird onErrorOccurred() aufgerufen, wobei ebenfalls finished() gesendet wird.
Die eigentliche Verbindung wird in der Funktion connectToServer() aufgebaut:
1
2
3
4
5
void SslClient::connectToServer(const QString &host, quint16 port, const QByteArray &message) {
m_message = message;
qInfo() << "Connecting to" << host << "port" << port;
m_socket.connectToHostEncrypted(host, port);
}
Die gesamte
QAbstractSocket-API ist asynchron, d.h. alle Funktionen wie z.B.connectToHostEncrypted()oderdisconnectFromHost()werden nicht sofort ausgeführt, sondern in der Eventloop verarbeitet. Entsprechend ändert sich der Socketzustand nicht direkt beim Aufruf der Funktionen, sondern erst, wenn die Aktion abgeschlossen ist. Deshalb kann man auch nicht gleich nach Aufruf vonconnectToHostEncrypted()mitm_socket.write()eine Nachricht schreiben, sondern muss warten, bis die verschlüsselte Verbindung steht undonEncrypted()aufgerufen wird.
Testen der Serververbindung und Verifikation
Führt man den Client nun aus, kann man folgendes testen:
Verbindungsaufbau, wenn der Server nicht läuft
Client gibt (in onErrorOccurred()) aus:
1
2
Connecting to "127.0.0.1" port 1234
Socket error: "Connection refused" (code QAbstractSocket::ConnectionRefusedError )
Verbindung zu 127.0.0.1 oder localhost
Client gibt aus:
1
2
3
4
Connecting to "127.0.0.1" port 1234
Handshake complete. Sending: "Hello from SSL client!"
Server echoed: "Hello from SSL client!"
Disconnected.
Verbindung zu 192.168.178.25
Client gibt aus:
1
2
3
4
Connecting to "192.168.178.25" port 1234
SSL error: "The host name did not match any of the valid hosts for this certificate"
Socket error: "The host name did not match any of the valid hosts for this certificate" (code QAbstractSocket::SocketError(13) )
Disconnected.
Die erste Fehlermeldung kommt aus onSslErrors(), die zweite Fehlermeldung wird in onErrorOccurred() geschrieben.
Prüfung des Client-Zertifikats durch den Server
Wenn nun der Server zusätzlich noch prüfen soll, dass der Client befugt ist, überhaupt eine Verbindung aufzubauen, braucht der Client ein eigenes Zertifikat. Dieses Zertifikat muss von einer Zertifikatautorität signiert sein, der der Server vertraut – in unserem Beispiel also von der eigenen Schneggenport-Root-CA.
Das Prinzip ist dabei dasselbe wie bei der Serverprüfung durch den Client, nur in umgekehrter Richtung:
- Der Client legt beim Verbindungsaufbau sein Zertifikat vor.
- Der Server prüft, ob dieses Zertifikat von einer ihm bekannten und vertrauenswürdigen CA signiert wurde.
- Schlägt die Prüfung fehl, wird die Verbindung abgelehnt.
Serverseitige Anpassungen
Der Server muss zwei Dinge tun:
- Das Wurzelzertifikat laden, damit er Client-Zertifikate verifizieren kann.
- Die Peer-Verifikation auf
VerifyPeerumstellen (bisher stand sie aufVerifyNone).
In der Funktion SslServer::listen() werden dazu folgende Änderungen vorgenommen (NEUER CODE):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
bool SslServer::listen(quint16 port) {
// read server certificate and private key files from resources
QFile certFile(QStringLiteral(":/Testserver_auf_Localhost_und_127.0.0.1.crt"));
QFile keyFile(QStringLiteral(":/Serverschlüssel.pem"));
if (!certFile.open(QIODevice::ReadOnly)) {
qCritical() << "Cannot open certificate file.";
return false;
}
if (!keyFile.open(QIODevice::ReadOnly)) {
qCritical() << "Cannot open key file.";
return false;
}
QSslCertificate certificate(&certFile, QSsl::Pem);
QSslKey privateKey(&keyFile, QSsl::Rsa, QSsl::Pem); // RSA type private key
certFile.close();
keyFile.close();
if (certificate.isNull() || privateKey.isNull()) {
qCritical() << "Failed to load certificate or private key.";
return false;
}
// configure QSslConfiguration to be used for each QSslSocket to be created
m_sslConfig = QSslConfiguration::defaultConfiguration();
m_sslConfig.setLocalCertificate(certificate);
m_sslConfig.setPrivateKey(privateKey);
// *** NEUER CODE ANFANG ***
// to check the client, we need to load the root CA
QFile caCertFile(QStringLiteral(":/Schneggenport-Root-CA.crt"));
if (!caCertFile.open(QIODevice::ReadOnly)) {
qCritical() << "Cannot open CA certificate file.";
return false;
}
QSslCertificate caCertificate(&caCertFile, QSsl::Pem);
caCertFile.close();
if (caCertificate.isNull()) {
qCritical() << "Failed to load CA certificate.";
return false;
}
m_sslConfig.addCaCertificate(caCertificate); // add CA certificate
// client must provide certificate
m_sslConfig.setPeerVerifyMode(QSslSocket::VerifyPeer);
// *** NEUER CODE ENDE ***
return QTcpServer::listen(QHostAddress::Any, port);
}
Durch addCaCertificate() teilt man dem Server mit, welcher CA er vertraut. Mit setPeerVerifyMode(QSslSocket::VerifyPeer) wird der Server angewiesen, beim SSL-Handshake ein gültiges Client-Zertifikat zu verlangen. Liegt keines vor oder ist es nicht von einer vertrauenswürdigen CA signiert, schlägt der Handshake fehl und die Verbindung wird abgelehnt.
Am übrigen Server-Code (insbesondere incomingConnection() und SslSocketThread) ändert sich nichts.
Clientseitige Anpassungen
Der Client muss beim Verbindungsaufbau sein eigenes Zertifikat und den dazugehörigen privaten Schlüssel vorlegen. Dies geschieht im Konstruktor von SslClient (auch hier sind die neuen Zeilen mit NEUER CODE gekennzeichnet):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
SslClient::SslClient(QObject *parent)
: QObject(parent)
{
QSslConfiguration config = QSslConfiguration::defaultConfiguration();
// We read our Root CA certificate and use VerifyPeer to verify the server
QFile rootCertFile(QStringLiteral(":/Schneggenport-Root-CA.crt"));
if (!rootCertFile.open(QIODevice::ReadOnly)) {
qCritical() << "Cannot open certificate";
return;
}
QSslCertificate certificate(&rootCertFile, QSsl::Pem);
// set root certificate
config.addCaCertificate( certificate );
// *** NEUER CODE ANFANG ***
// add our own certificate and key for the server to check against
QFile clientCertFile(QStringLiteral(":/ClientZertifikat.crt"));
QFile clientKeyFile(QStringLiteral(":/ClientSchlüssel.pem"));
if (!clientCertFile.open(QIODevice::ReadOnly) || !clientKeyFile.open(QIODevice::ReadOnly)) {
qCritical() << "Cannot open client certificate or key file";
return;
}
QSslCertificate clientCert(&clientCertFile, QSsl::Pem);
QSslKey clientKey(&clientKeyFile, QSsl::Rsa, QSsl::Pem);
clientCertFile.close();
clientKeyFile.close();
if (clientCert.isNull() || clientKey.isNull()) {
qCritical() << "Failed to load client certificate or key";
return;
}
config.setLocalCertificate(clientCert);
config.setPrivateKey(clientKey);
// *** NEUER CODE ENDE ***
// enable peer verification
config.setPeerVerifyMode(QSslSocket::VerifyPeer);
m_socket.setSslConfiguration(config);
connect(&m_socket, &QSslSocket::encrypted, this, &SslClient::onEncrypted);
connect(&m_socket, &QSslSocket::readyRead, this, &SslClient::onReadyRead);
connect(&m_socket, &QSslSocket::disconnected, this, &SslClient::onDisconnected);
connect(&m_socket, &QSslSocket::sslErrors, this, &SslClient::onSslErrors);
connect(&m_socket, &QSslSocket::errorOccurred, this, &SslClient::onErrorOccurred);
}
setLocalCertificate() setzt das Zertifikat, das der Client beim Handshake dem Server präsentiert. setPrivateKey() hinterlegt den zugehörigen privaten Schlüssel, mit dem der Client beweist, dass er tatsächlich der rechtmäßige Inhaber des Zertifikats ist.
Testen der gegenseitigen Authentifizierung
Läuft jetzt der Server mit Client-Verifikation und verbindet sich ein korrekt konfigurierter Client, zeigt der Server beim Handshake:
1
Encrypted connection established with "::ffff:127.0.0.1"
Versucht man hingegen, sich mit einem Client ohne gültiges Zertifikat zu verbinden (z.B. per openssl s_client ohne Client-Zertifikat), erhält der Server folgende Fehlermeldungen im onSslErrors()-Slot:
1
SSL error on "::ffff:127.0.0.1" : "The peer did not present any certificate"
Der Client bekommt seinerseits eine Fehlermeldung, da der Server die Verbindung schließt:
1
Socket error: "The remote host closed the connection" (code QAbstractSocket::RemoteHostClosedError )
Damit ist die gegenseitige Authentifizierung vollständig: Der Client vertraut dem Server nur, wenn dessen Zertifikat von einer bekannten CA stammt und zum angesprochenen Hostnamen passt – und der Server akzeptiert nur Clients, die ein gültiges, von der eigenen CA signiertes Zertifikat vorweisen können.
Server ohne Root CA
Wenn man aus dem Server-Quelltext die Zeile m_sslConfig.addCaCertificate(caCertificate); auskommentiert, dann
kennt der Server unser selbst-signiertes Zertifikat nicht und beim Verbindungsaufbau kommt folgende Fehlermeldung im Server:
1
2
3
4
SSL server listening on port 1234
SSL error on "::ffff:127.0.0.1" : "The root certificate of the certificate chain is self-signed, and untrusted"
Client disconnected: "::ffff:127.0.0.1"
QAbstractSocket::SocketError(13) : "The root certificate of the certificate chain is self-signed, and untrusted"
Erstellung von selbstsignierten Zertifikaten mit der Software XCA
Das graphische Tool xca (zu installieren mit sudo apt install xca) dient der Verwaltung von Zertifikaten und privaten Schlüsseln (und Zertifikatanträgen, welche wir aber hier nicht brauchen).
Nach Start der Software legen wir erstmal eine passwortgeschützte Zertifikatsdatenbank an (Datei->Neue Datenbank).
Wurzelzertifikat erzeugen
Als erstes erzeugen wir das Wurzelzertifikat unserer eigenen Zertifikatautorität:
- Reiter “Zertifikate”
- Neues Zertifikat, es öffnet sich der Dialog X Certificate and Key management
Jetzt der Reihe nach die Reiter durcharbeiten:
Registerkarte: Herkunft
Wir erstellen ein neues Wurzelzertifikat, also ist nur der Radiobutton Selbstsigniertes Zertifikat aktiv.
Als Vorlage in der Auswahlliste “[default] CA” auswählen und dann die Einstellungen aus dieser Vorlage mit Alles übernehmen in den Dialog übertragen.
Registerkarte: Inhaber
Hier geben wir Metadaten über den Zertifikatsinhaber ein. Da es sich um ein selbstsigniertes Zertifikat handelt, sind die meisten Eingaben hier optional.
Zumindest das Feld Interner Name sollte ausgefüllt werden, damit wir das Zertifikat später in der Liste der Zertifikate wiederfinden. Bei Wurzelzertifikaten bietet sich die Verwendung von “root CA” im Namen an. In meinem Beispiel hab ich “Schneggenport-Root-CA” gewählt.
Als erstes erstellen wir einen privaten Schlüssel für dieses Zertifikat. Dazu unten im Dialog auf Erstelle einen neuen Schlüssel klicken und folgende Optionen eingeben:
- Name: Schneggenport-Root-CA (wie das dazugehörige Zertifikat)
- Schlüsseltyp: RSA (die anderen Verschlüsselungsverfahren kann man auch nehmen, Qt::SslSocket unterstützt Rsa, Dsa, EC)
- Schlüssellänge: 4096 Bit (kürzer sollte man nicht wählen)
Nun wird der frisch erstellte Schlüssel ausgewählt.
Registerkarte: Erweiterungen
Für das Wurzelzertifikat müssen wir erstmal nur die Gültigkeit festlegen. Für ein Wurzelzertifikat sind die voreingestellten 10 Jahre sicher eine sinnvolle Wahl. Man kann hier aber auch eine kürzere Gültigkeit einstellen.
Nun den Dialog bestätigen und das Wurzelzertifikat ist fertig.
Die Registerkarten Zertifikate und Private Schlüssel zeigen nun Zertifikat und Schlüssel an. Allerdings existieren diese noch nicht als alleinstehende Dateien - den Export machen wir später.
Serverzertifikat erstellen
Nun gilt es, ein Serverzertifikat zu erstellen, welches mit dem Wurzelzertifikat signiert wird. Dazu wählt man in der Registerkarte Zertifikate das Wurzelzertifikat aus und erstellt mit Neues Zertifikat ein neues Zertifikat. Es öffnet sich erneut der Dialog X Certificate and Key management.
Registerkarte: Herkunft
Hier sollte im Abschnitt “Unterschreiben” nun das vorher erstellte Wurzelzertifikat ausgewählt sein.
Als Vorlage wird diesmal “[default] TLS_Server” gewählt und die Einstellungen werden wieder mit Alles übernehmen in den Dialog übernommen.
Registerkarte: Inhaber
In der nächsten Registerkarte müssen wieder Beschreibungsdaten für den Server eingetragen werden. Als erstes wird wieder ein privater Schlüssel erzeugt. Man könnte zwar auch den vorab erstellten Schlüssel der CA benutzen, aber besser ist es in jedem Fall, jedem Server seinen eigenen privaten Schlüssel zu geben.
Ich habe den internen Namen “Serverschlüssel” verwendet:
Wichtig ist in der Registerkarte Inhaber noch das Feld commonName. Dieses beinhaltet die primäre Serveradresse, auf welcher der Server lauschen sollte. Hier sollte man die URL bzw. den DNS-Eintrag eintragen, der für den Server gilt. Damit kann ein Client dann prüfen, ob der angesprochene Server auch zu seiner Adresse passt.
Ich habe hier zunächst “localhost” eingetragen, sodass zum Testen der Verbindung als URL “localhost” verwendet werden kann.
Registerkarte: Erweiterungen
In dieser Registerkarte wird die Laufzeit des Zertifikats eingetragen. Diese sollte bei einem Server nicht zu lang sein. Kürzere Laufzeiten sind sicherer, bedingen aber regelmäßige Wartungszyklen, wo neue Zertifikate beim Server hinterlegt werden. Da Zertifikate meist (wie in unserem Beispiel) in den Server hineincompiliert werden, bedeutet dies auch immer ein Neuerstellen und Update des Servers.
Falls ein Server über mehrere URLs bzw. DNS Einträge angesprochen werden soll, bspw. wenn Weiterleitungen für Subdomains eingerichtet sind, dann muss man diese URLs/IP-Adressen zusätzlich festlegen.
Dazu die Eigenschaft X509v3 Subject Alternative Name bearbeiten (Schaltfläche Bearbeiten). Im sich öffnenden Dialog auf Hinzufügen gehen, dann in der ersten Tabellenspalte “IP” bzw. “DNS” auswählen und dahinter den alternativen Hostnamen/DNS oder die IP-Adresse eintragen. Dann auf Übernehmen klicken und der Eintrag erscheint im Textfeld (kann man auch selbst so eingeben, der Dialog ist nur eine Format-Hilfe).
Jetzt den Dialog bestätigen und das Serverzertifikat ist fertig.
Erstellung des Client-Zertifikats mit XCA
Das Clientzertifikat benötigt man, wenn der Server selbst die Gültigkeit des Clients testen soll. Die Vorgehensweise ist prinzipiell dieselbe wie beim Serverzertifikat. In der Registerkarte Zertifikate das Wurzelzertifikat auswählen und Neues Zertifikat klicken.
Registerkarte: Herkunft
Als Vorlage wird diesmal [default] TLS_Client gewählt und die Einstellungen mit Alles übernehmen in den Dialog übertragen. Als Unterzeichner sollte das Wurzelzertifikat ausgewählt sein.
Registerkarte: Inhaber
Als interner Name wird “ClientZertifikat” festgelegt.
Für den Client wird ebenfalls ein eigener privater Schlüssel erstellt (Schaltfläche Erstelle einen neuen Schlüssel), bei mir “Clientschlüssel” benannt. Als commonName trägt man einen Namen ein, der den Client identifiziert, z.B. “SslClient”.
Registerkarte: Erweiterungen
Hier wird wieder die Gültigkeit festgelegt. Subject Alternative Names sind für ein Client-Zertifikat nicht nötig, da der Server keine Hostnamen-Prüfung am Client vornimmt.
Den Dialog bestätigen – das Clientzertifikat ist fertig.
Zertifikatsdateien und Schlüsseldateien exportieren
Um die Zertifikate im Server und Client verwenden zu können, müssen die Dateien exportiert werden.
Private Schlüssel
Dazu die Einträge für die privaten Schlüssel auswählen, dann auf Export klicken und dann die Schlüssel als *.pem-Dateien exportieren. Bei mir entstehen so die Schlüssel:
Schneggenport-Root-CA.pemServerschlüssel.pemClientschlüssel.pem
Zertifikate
In der Registerkarte Zertifikate das Wurzelzertifikat ausklappen und dann zunächst das Wurzelzertifikat und dann das Serverzertifikat exportieren. Dabei das Format PEM (*.crt) auswählen. Es entstehen die Zertifikatsdateien:
Schneggenport-Root-CA.crtTestserver_auf_Localhost_und_127.0.0.1.crtClientZertifikat.crt







