Als wir die Ende-zu-Ende-Verschlüsselung in sgcWebSockets vorgestellt haben, war die Zusage klar: Alice und Bob leiten mit ECDH ein gemeinsames Geheimnis ab, verschlüsseln mit AES-GCM, und der Server, der den Verkehr weiterleitet, sieht nichts als Chiffretext. Dieser Teil ist solide. Davor gibt es jedoch einen Schritt, den die Verschlüsselung selbst nicht schützen kann, und genau dort passieren die echten Angriffe: Alice muss Bobs öffentlichen Schlüssel irgendwo herbekommen, und sie bekommt ihn über den Server.
Stellen Sie sich den Angriff konkret vor. Alice fragt den Server nach Bobs öffentlichem Schlüssel. Ein kompromittierter Server, ein bösartiges Relay oder wer auch immer diesen Hop übernommen hat, leitet Bobs Schlüssel nicht weiter. Er erzeugt ein eigenes Schlüsselpaar und übergibt Alice seinen eigenen öffentlichen Schlüssel, beschriftet mit "Bob". Dann macht er dasselbe in die andere Richtung und übergibt Bob einen anderen eigenen Schlüssel, beschriftet mit "Alice". Alice verschlüsselt nun perfekt, an den Angreifer. Der Angreifer entschlüsselt, liest mit, schreibt möglicherweise um, verschlüsselt erneut an Bobs echten Schlüssel und leitet weiter. Bob entschlüsselt perfekt, vom Angreifer. Beide Seiten sehen ein grünes Schloss. Beide Seiten irren sich. Das ist der klassische Man-in-the-Middle-Schlüsseltausch, und kein noch so gutes AES-GCM behebt ihn, weil beide Hälften der Unterhaltung tatsächlich korrekt verschlüsselt sind. Sie sind nur an die falsche Person verschlüsselt.
Ab Version 2026.7 schließt sgcWebSockets diese Lücke. Jeder Endpunkt kann einen langlebigen Identitätsschlüssel besitzen, seinen kurzlebigen Verschlüsselungsschlüssel damit signieren, und die Gegenstelle prüft diese Signatur, bevor sie überhaupt ein gemeinsames Geheimnis ableitet. Ihre Anwendung entscheidet, was mit dem Fingerabdruck der Gegenstelle geschieht: ihn beim ersten Kontakt akzeptieren und anheften, ihn außerhalb des Kanals vergleichen, oder alles ablehnen, was gar nicht signiert ist. Die Funktion ist standardmäßig deaktiviert, und das Format auf der Leitung bleibt unverändert, bis Sie sie einschalten.
Verschlüsselung ist keine Authentifizierung
Das sind zwei verschiedene Eigenschaften, und es lohnt sich, deutlich zu sagen, welche davon Sie haben.
Verschlüsselung beantwortet die Frage "Kann ein Dritter das lesen?". ECDH plus AES-GCM beantwortet sie: nein. Authentifizierung beantwortet die Frage "Ist der Schlüssel, an den ich verschlüsselt habe, wirklich der Schlüssel der Person, mit der ich zu sprechen glaube?". Reines ECDH beantwortet das überhaupt nicht. Es einigt sich bereitwillig mit jedem auf ein gemeinsames Geheimnis, der einen Schlüssel geschickt hat, und es hat keine Meinung dazu, wer das war.
Alles in diesem Beitrag dreht sich um die zweite Frage. Nichts davon ändert die Cipher Suite, die Schlüsselableitung oder das Nachrichtenformat. Es fügt eine Signatur über den kurzlebigen Schlüssel hinzu, und eine Stelle, an der Ihre Anwendung Ja oder Nein zur Gegenstelle dahinter sagen kann.
Langlebige Identitätsschlüssel
In einer E2EE-Sitzung gibt es jetzt zwei Arten von Schlüsseln, und sie auseinanderzuhalten ist die ganze Idee.
Der kurzlebige Verschlüsselungsschlüssel ist das ECDH-Schlüsselpaar, das der Client bereits verwendet hat. Es existiert für die Dauer der Sitzung, aus ihm wird das gemeinsame Geheimnis abgeleitet, und es kann jederzeit neu erzeugt werden. Es sagt nichts darüber aus, wer Sie sind.
Der Identitätsschlüssel ist ein langlebiges ECDSA-P-256-Schlüsselpaar. Sie erzeugen es einmal, Sie speichern die private Hälfte auf dem Gerät, und Sie behalten es über Neustarts, erneute Verbindungen und neue Sitzungen hinweg. Seine einzige Aufgabe ist es, den kurzlebigen Schlüssel zu signieren: "Der kurzlebige öffentliche Schlüssel, den Sie gerade erhalten haben, wurde wirklich vom Inhaber dieses Identitätsschlüssels veröffentlicht". Die Gegenstelle prüft diese Signatur mit dem öffentlichen Identitätsschlüssel, der daneben mitgereist ist, und der Server, der beides weiterhin sehen und weiterleiten kann, kann die Signatur nicht fälschen, weil er den privaten Identitätsschlüssel nicht besitzt.
Damit schrumpft das Man-in-the-Middle-Problem auf eine einzige Frage: Gehört dieser öffentliche Identitätsschlüssel wirklich meiner Gegenstelle? Das ist eine Frage, die Ihre Anwendung beantworten kann, denn anders als ein zufälliger kurzlebiger Schlüssel ist ein Identitätsschlüssel stabil, er lässt sich also merken, anheften und von einem Menschen vergleichen.
Einen Identitätsschlüssel erzeugen und speichern
Eine Hilfsfunktion in sgcSSL_E2EE erzeugt das Schlüsselpaar als PEM-Strings. Rufen Sie sie einmal pro Installation und pro Benutzer auf, und speichern Sie den privaten Schlüssel dort, wo Ihre Anwendung Geheimnisse ablegt. Senden Sie den privaten Schlüssel niemals irgendwohin.
uses
sgcSSL_E2EE;
var
vPrivateKeyPEM, vPublicKeyPEM: string;
begin
// ... generate ONCE, then persist. Regenerating it on every start
// ... defeats the whole point: your peers would see a new identity every time.
sgcE2EE_CreateIdentityKeyPair(vPrivateKeyPEM, vPublicKeyPEM);
SaveIdentity(vPrivateKeyPEM, vPublicKeyPEM); // your own secure storage
end;
Dasselbe Schlüsselpaar lässt sich auch vom E2EE-Client selbst mit GenerateIdentityKeyPair erzeugen, wenn Sie es lieber an der Komponente halten:
var
vPrivateKeyPEM, vPublicKeyPEM: string;
begin
oE2EEClient.GenerateIdentityKeyPair(vPrivateKeyPEM, vPublicKeyPEM);
end;
Die Speicherung ist Ihre Entscheidung, und sie ist der Teil, der Sorgfalt verdient. Der private Schlüssel ist das, was beweist, dass Sie Sie sind. Legen Sie ihn in den Keystore der Plattform, in ein per DPAPI geschütztes Blob, in eine verschlüsselte Einstellungsdatei, wohin auch immer Ihr Bedrohungsmodell ihn verortet. Wenn er abhandenkommt, kann ein Angreifer diese Identität annehmen, und der Fingerabdruck, den Ihre Benutzer verglichen haben, stimmt weiterhin überein.
Einschalten
Die Identitätssignatur liegt unter E2EE_Options.Identity. Laden Sie das gespeicherte Schlüsselpaar, setzen Sie Enabled, und der Client signiert von da an seinen kurzlebigen Schlüssel und prüft die Signaturen, die er empfängt.
uses
sgcWebSocket, sgcWebSocket_Protocols;
begin
WSClient := TsgcWebSocketClient.Create(nil);
WSClient.Host := '127.0.0.1';
WSClient.Port := 80;
E2EE := TsgcWSPClient_E2EE.Create(nil);
E2EE.Client := WSClient;
E2EE.E2EE_Options.UserId := 'client-1';
// ... identity verification
E2EE.E2EE_Options.Identity.Enabled := True;
E2EE.E2EE_Options.Identity.PrivateKey := LoadIdentityPrivateKey; // PEM
E2EE.E2EE_Options.Identity.PublicKey := LoadIdentityPublicKey; // PEM
E2EE.OnE2EEVerifyPeerIdentity := OnE2EEVerifyPeerIdentityEvent;
E2EE.OnE2EEKeyChange := OnE2EEKeyChangeEvent;
WSClient.Active := True;
end;
Ab hier wird der kurzlebige öffentliche Schlüssel, den der Client veröffentlicht, vom öffentlichen Identitätsschlüssel und einer Signatur über diesen kurzlebigen Schlüssel begleitet. Wenn der Schlüssel einer Gegenstelle eintrifft, wird die Signatur gegen den Identitätsschlüssel geprüft, der mit ihm gekommen ist. Lässt sich die Signatur nicht verifizieren, wird der Schlüssel der Gegenstelle abgelehnt und es wird kein gemeinsames Geheimnis daraus abgeleitet.
Achten Sie genau darauf, was eine gültige Signatur beweist und was nicht. Sie beweist, dass der kurzlebige Schlüssel vom Inhaber dieses privaten Identitätsschlüssels signiert wurde. Sie beweist für sich genommen nicht, dass dieser Identitätsschlüssel Ihrer Gegenstelle gehört, denn ein Man in the Middle kann seinen eigenen Identitätsschlüssel mit seiner eigenen, völlig gültigen Signatur vorlegen. Für diese letzte Meile sind die nächsten beiden Abschnitte da.
Eine Gegenstelle prüfen: Trust on first use und Pinning
Sobald eine Signatur verifiziert ist, wird OnE2EEVerifyPeerIdentity mit der Benutzerkennung der Gegenstelle, dem öffentlichen Identitätsschlüssel und dessen Fingerabdruck ausgelöst. Der Parameter aAccept ist ein var Parameter, und er kommt als True herein, wenn Sie also nichts tun, wird die Gegenstelle akzeptiert. Die Entscheidung liegt bewusst bei Ihnen, denn nur Ihre Anwendung weiß, ob sie diesen Kontakt schon einmal gesehen hat.
Das übliche Muster ist Trust on first use plus Pinning. Wenn Sie einen Benutzer zum ersten Mal sehen, merken Sie sich den Fingerabdruck. Jedes weitere Mal vergleichen Sie. Stimmt er überein, akzeptieren Sie stillschweigend. Stimmt er nicht überein, akzeptieren Sie nicht, und sagen Sie es dem Benutzer.
procedure TForm1.OnE2EEVerifyPeerIdentityEvent(Sender: TObject;
const aUserId, aIdentityPublicKey, aFingerprint: string; var aAccept: Boolean);
var
vPinned: string;
begin
vPinned := GetPinnedFingerprint(aUserId); // '' the first time we see this user
if vPinned = '' then
begin
// ... trust on first use: remember it, and from now on it must not change
SetPinnedFingerprint(aUserId, aFingerprint);
aAccept := True;
DoLog('pinned ' + aUserId + ': ' + aFingerprint);
end
else if SameText(vPinned, aFingerprint) then
begin
// ... same identity key as last time
aAccept := True;
end
else
begin
// ... a different identity key for a user we already know. Refuse it and
// ... let the user decide, do not silently trust it.
aAccept := False;
DoLog('REJECTED ' + aUserId + ': fingerprint mismatch');
end;
end;
Trust on first use hat eine ehrliche Einschränkung, die man aussprechen sollte: Wenn der Angreifer beim allerersten Kontakt bereits in der Mitte saß, heften Sie den Angreifer an. Was es Ihnen einbringt, ist, dass der Angreifer von Anfang an da sein und für immer dort bleiben muss, und dass jeder spätere Versuch, den Schlüssel auszutauschen, laut wird. Wenn Sie auch die Lücke beim Erstkontakt schließen wollen, vergleichen Sie den Fingerabdruck außerhalb des Kanals, und darum geht es im nächsten Abschnitt.
Fingerabdrücke, die Ihre Benutzer wirklich vergleichen können
Ein Fingerabdruck ist ein Digest des öffentlichen Identitätsschlüssels, erzeugt von sgcE2EE_IdentityFingerprint. Zwei Endpunkte mit demselben Identitätsschlüssel erzeugen denselben Fingerabdruck, und ein Man in the Middle mit einem anderen Schlüssel kann keinen passenden erzeugen.
Das ist genau der Mechanismus hinter der "Sicherheitsnummer" von Signal und dem "Sicherheitscode" von WhatsApp. Sein Wert liegt darin, dass ein Mensch ihn über einen Kanal vergleichen kann, den der Angreifer nicht kontrolliert. Alice liest ihn Bob am Telefon vor, oder die beiden zeigen einander persönlich einen QR-Code, oder sie fügen ihn in einen bereits vertrauenswürdigen Chat ein. Stimmen die beiden Werte überein, ist niemand in der Mitte. Unterscheiden sie sich, ist jemand da.
Geben Sie ihn Ihren Benutzern in einer Form, die sie tatsächlich vorlesen können. Den Digest in kurze Blöcke zu gruppieren genügt:
uses
sgcSSL_E2EE;
function FormatFingerprint(const aFingerprint: string): string;
var
i: Integer;
begin
// ... 'A1B2C3D4...' -> 'A1B2 C3D4 ...' so a human can read it over the phone
Result := '';
for i := 1 to Length(aFingerprint) do
begin
if (i > 1) and ((i - 1) mod 4 = 0) then
Result := Result + ' ';
Result := Result + aFingerprint[i];
end;
end;
procedure TForm1.ShowMyFingerprint;
begin
lblFingerprint.Caption :=
FormatFingerprint(sgcE2EE_IdentityFingerprint(LoadIdentityPublicKey));
end;
Zeigen Sie Ihren eigenen Fingerabdruck in der Anwendung, zeigen Sie den Fingerabdruck der Gegenstelle neben dem Kontakt, und lassen Sie den Benutzer einen Kontakt als verifiziert markieren, sobald er die beiden verglichen hat. Das ist der Punkt, an dem die Verschlüsselung tatsächlich das bedeutet, was Ihre Benutzer glauben, dass sie bedeutet.
Wenn sich der Identitätsschlüssel einer Gegenstelle ändert
OnE2EEKeyChange wird ausgelöst, wenn ein Benutzer einen anderen Identitätsschlüssel vorlegt als den, den Sie zuletzt von ihm gesehen haben, und es übergibt Ihnen sowohl den alten als auch den neuen Fingerabdruck.
Seien Sie vorsichtig damit, wie Sie das darstellen. Ein geänderter Identitätsschlüssel ist nicht automatisch ein Angriff. Ein Benutzer, der die App neu installiert, ein Gerät zurückgesetzt oder sich auf einem neuen Telefon angemeldet hat, hat völlig legitim einen neuen Identitätsschlüssel, und das ist mit Abstand die häufigste Ursache. Es ist allerdings genau das Signal, das auch ein Schlüsseltausch erzeugen würde, es ist also der Moment, in dem der Benutzer informiert werden sollte, und der Moment, in dem Ihre Anwendung das Pinning verwerfen und einen neuen Vergleich außerhalb des Kanals verlangen möchte.
procedure TForm1.OnE2EEKeyChangeEvent(Sender: TObject;
const aUserId, aOldFingerprint, aNewFingerprint: string);
begin
// ... "Your security code with <user> has changed."
// ... Usually a reinstall or a new device. Sometimes not.
DoLog(Format('identity key changed for %s: %s -> %s',
[aUserId, aOldFingerprint, aNewFingerprint]));
ClearPinnedFingerprint(aUserId); // force a re-verification
ShowSecurityCodeChangedWarning(aUserId, FormatFingerprint(aNewFingerprint));
end;
Kombinieren Sie es mit dem Handler weiter oben: OnE2EEKeyChange sagt Ihnen, dass sich der Schlüssel geändert hat, in OnE2EEVerifyPeerIdentity entscheiden Sie, ob Sie weiterreden.
Im Zweifel abweisen mit RequireAuthentication
Mit gesetztem Identity.Enabled und RequireAuthentication auf seiner Vorgabe False läuft der Client im Best-Effort-Modus. Gegenstellen, die eine gültige Signatur vorlegen, werden verifiziert. Gegenstellen, die überhaupt keine Signatur vorlegen, ein älterer Client, ein Client, der die Identität nicht eingeschaltet hat, werden weiterhin akzeptiert. Das ist während eines Rollouts bequem, wenn noch nicht jeder Endpunkt aktualisiert wurde, aber es ist keine Sicherheitsgrenze: Ein Angreifer kann die Signatur einfach entfernen und wie ein alter Client aussehen.
RequireAuthentication := True ist der Schalter, der im Zweifel abweist. Ein unsignierter Schlüssel einer Gegenstelle, oder einer, dessen Signatur sich nicht verifizieren lässt, wird abgelehnt statt akzeptiert.
// ... reject any peer that does not present a valid identity signature
E2EE.E2EE_Options.Identity.Enabled := True;
E2EE.E2EE_Options.Identity.PrivateKey := LoadIdentityPrivateKey;
E2EE.E2EE_Options.Identity.PublicKey := LoadIdentityPublicKey;
E2EE.E2EE_Options.Identity.RequireAuthentication := True;
Rollen Sie es in zwei Schritten aus. Liefern Sie Identitätsschlüssel mit ausgeschaltetem RequireAuthentication aus, warten Sie, bis Ihre Flotte sie hat, und schalten Sie es dann ein. Sobald es eingeschaltet ist, funktioniert ein Downgrade-Angriff, der die Signatur entfernt, nicht mehr, weil eine fehlende Signatur eine Ablehnung ist statt eines Achselzuckens.
Gruppenchats
Gruppennachrichten bekommen dieselbe Behandlung. Jedes Mitglied signiert seinen kurzlebigen Schlüssel mit seinem eigenen Identitätsschlüssel, und der öffentliche Identitätsschlüssel und die Signatur reisen mit dem Eintrag jedes Mitglieds mit, der Beitritt zu einer Gruppe verifiziert also jedes Mitglied, an das Sie gleich verschlüsseln werden, nicht nur die Gegenstelle, die Sie eingeladen hat. OnE2EEVerifyPeerIdentity wird pro Mitglied ausgelöst, dieselbe Pinning-Logik, die Sie für Eins-zu-eins-Chats geschrieben haben, gilt also unverändert, und mit einem Mitglied, dessen Schlüssel Sie ablehnen, leiten Sie kein Geheimnis ab. RequireAuthentication gilt in einer Gruppe genauso wie in einer direkten Unterhaltung.
Standardmäßig abwärtskompatibel
Identity.Enabled ist ab Werk False. Ist es ausgeschaltet, wird kein Identitätsschlüssel gesendet, keine Signatur erzeugt, keine erwartet, und der Schlüsselaustausch ist Byte für Byte derselbe wie zuvor. Ein Update der Bibliothek ändert an einer bestehenden E2EE-Installation nichts, bis Sie die Property ausdrücklich setzen.
Die Identitätsfelder sind auf der Leitung additiv, ein Client mit eingeschalteter Identität spricht also weiterhin mit einem Client ohne sie, solange RequireAuthentication ausgeschaltet ist. Genau das erlaubt es Ihnen, die Funktion schrittweise über eine Flotte auszurollen, die Sie nicht auf einmal aktualisieren.
Verfügbarkeit
Die E2EE-Identitätsprüfung erscheint in sgcWebSockets 2026.7 für Delphi 7 bis 13 und C++Builder, auf Win32/Win64, Linux64, macOS, Android und iOS. Sie ist Teil des E2EE-Protokolls, verfügbar in den Editionen Enterprise und All-Access, und sie funktioniert für Eins-zu-eins-Nachrichten und für Gruppenchats.
Kunden mit einem aktiven Abonnement können den neuen Build im Kundenbereich herunterladen. Testbenutzer finden den aktualisierten Installer unter esegece.com/products/websockets/download.
Fragen, Feedback oder Hilfe beim Einbau der Identitätsprüfung in Ihre App? Kontaktieren Sie uns, Sie erhalten eine Antwort von den Leuten, die den Code geschrieben haben.
