Verlauf eines einzelnen SSL-Handshakes

Hallo Habr!

Vor kurzem musste ich SSL mit gegenseitiger Authentifizierung an den Spring Reactive Webclient schrauben. Es scheint eine einfache Angelegenheit zu sein, aber sie führte zu einem Wandern in den JDK-Quellen mit einem unerwarteten Ende. Es wurden Erfahrungen für einen ganzen Artikel gesammelt, die für Ingenieure bei alltäglichen Aufgaben oder zur Vorbereitung auf ein Interview nützlich sein können.


Erklärung des Problems


Auf Kundenseite gibt es einen REST-Service, der über HTTPS funktioniert.
Sie müssen von einer Client-Java-Anwendung darauf zugreifen.

Das erste, was mir bei dieser Quest gegeben wurde, waren 2 Dateien mit der Erweiterung .pem - ein Client-Zertifikat und ein privater Schlüssel. Ich habe ihre Leistung mit Postman überprüft: Ich habe die Pfade zu ihnen in den Einstellungen angegeben und nach dem Abrufen einer Anfrage sichergestellt, dass der Server mit 200 OK und einem aussagekräftigen Antworttext antwortet. Unabhängig davon habe ich überprüft, dass der Server ohne Client-Zertifikat einen HTTP-Status von 500 und eine kurze Nachricht im Hauptteil der Antwort zurückgibt, dass eine "Sicherheitsausnahme" mit einem bestimmten Code aufgetreten ist.

Als Nächstes sollten Sie die Client-Java-Anwendung korrekt konfigurieren.

Für REST-Anforderungen habe ich Spring Reactive WebClient mit nicht blockierenden E / A verwendet.
Die Dokumentation enthält ein Beispiel dafür, wie es angepasst werden kann, indem ein SslContext-Objekt darauf geworfen wird, in dem nur Zertifikate und private Schlüssel gespeichert sind.

Meine Konfiguration in einer vereinfachten Version wurde fast aus der Dokumentation kopiert und eingefügt:

SslContext sslContext = SslContextBuilder .forClient() .keyManager(…) /*  ,   .pem      (, , ). PEM      . /    openssl .     KeyManagerFactory. */ .build(); ClientHttpConnector connector = new ReactorClientHttpConnector( builder -> builder.sslContext(sslContext)); WebClient webClient = WebClient.builder() .clientConnector(connector).build(); 

Nach dem TDD-Prinzip habe ich auch einen Test geschrieben, der WebTestClient anstelle von WebClient verwendet und eine Reihe von Debugging-Informationen anzeigt. Die allererste Behauptung war wie folgt:

 webTestClient .post() .uri([  ]) .body(BodyInserters.fromObject([ ,   ,  ])) .exchange() .expectStatus().isOk() 

Dieser einfache Test wurde nicht sofort bestanden: Der Server gab 500 mit demselben Text zurück, als ob Postman kein Client-Zertifikat angegeben hätte.

Unabhängig davon stelle ich fest, dass ich während des Debuggens die Option "Serverzertifikat nicht prüfen" aktiviert habe, dh die InsecureTrustManagerFactory-Instanz als TrustManager für SslContext übergeben habe. Diese Maßnahme war überflüssig, schloss jedoch mit Sicherheit die Hälfte der Optionen aus.

Die Debugging-Informationen im Test haben das Problem nicht beleuchtet, aber es sah so aus, als ob in der SSL-Handshake-Phase ein Fehler aufgetreten ist. Daher habe ich beschlossen, die Verbindung in beiden Fällen genauer zu vergleichen: für Postman und für den Java-Client. All dies kann mit Wireshark gesehen werden - dies ist ein so beliebter Netzwerkverkehrsanalysator. Gleichzeitig habe ich gesehen, wie SSL-Handshake sozusagen live mit bidirektionaler Authentifizierung abläuft (das fragen sie gerne in Interviews):

  • Zunächst sendet der Client eine Client-Hello- Nachricht mit Metainformationen wie der Protokollversion und der Liste der von ihm unterstützten Verschlüsselungsalgorithmen
  • Als Antwort sendet der Server sofort ein Paket der folgenden Nachrichten: Server Hallo, Zertifikat, Server Key Exchange, Zertifikatanforderung, Server Hallo Fertig .
    Server Hello gibt den vom Server ausgewählten Verschlüsselungsalgorithmus an (Cipher Suite). Im Zertifikat befindet sich das Serverzertifikat. Der Serverschlüsselaustausch enthält je nach gewähltem Algorithmus einige Informationen, die für die Verschlüsselung erforderlich sind (die Details interessieren uns derzeit nicht, daher gehen wir davon aus, dass dies nur ein öffentlicher Schlüssel ist, obwohl dies falsch ist!). Bei der bidirektionalen Authentifizierung fordert der Server in der Zertifikatanforderungsnachricht ein Clientzertifikat an und erläutert, welche Formate er unterstützt und welchen Ausstellern er vertraut.
  • Nachdem der Client diese Informationen erhalten hat, überprüft er das Zertifikat des Servers und sendet sein Zertifikat, seinen „öffentlichen Schlüssel“ und andere Informationen in den folgenden Nachrichten: Zertifikat, Client-Schlüsselaustausch, Zertifikatüberprüfung . Die letzte ist die ChangeCipherSpec- Nachricht, die angibt, dass die gesamte weitere Kommunikation in verschlüsselter Form erfolgt
  • Nach all dem Mischen überprüft der Server schließlich das Zertifikat des Clients und gibt eine Antwort, wenn alles in Ordnung ist.

Nach fünfzehn Minuten im Datenverkehr bemerkte ich, dass der Java-Client als Antwort auf die Zertifikatanforderung vom Server aus irgendeinem Grund sein Zertifikat im Gegensatz zum Postman-Client nicht sendet. Das heißt, es gibt eine Zertifikatnachricht, die jedoch leer ist.

Als nächstes müsste ich mir zuerst die TLS-Protokollspezifikation ansehen, die wörtlich Folgendes sagt:
Wenn die Liste "certificate_authorities" in der Zertifikatanforderungsnachricht nicht leer war, MUSS eines der Zertifikate in der Zertifikatkette von einer der aufgelisteten Zertifizierungsstellen ausgestellt werden.
Wir sprechen über die Liste certificate_authorities, die in der Zertifikatanforderungsnachricht vom Server angegeben ist. Ein Client-Zertifikat (mindestens eine der Ketten) muss von einem der in dieser Liste aufgeführten Emittenten signiert sein. Wir nennen dies den X-Check .

Ich wusste nichts über diesen Zustand und fand ihn, als ich beim Debuggen die Tiefen des JDK erreichte (ich habe es JDK9). Nettys HttpClient, der Spring WebClient zugrunde liegt, verwendet standardmäßig SslEngine aus dem JDK. Alternativ können Sie es zum OpenSSL-Anbieter wechseln, indem Sie die erforderlichen Abhängigkeiten hinzufügen, aber das habe ich letztendlich nicht benötigt.
Daher habe ich Haltepunkte innerhalb der sun.security.ssl.ClientHandshaker-Klasse festgelegt und im Handler für die serverHelloDone-Nachricht eine X-Prüfung gefunden, die nicht bestanden wurde: Keiner der Aussteller in der Clientzertifikatskette befand sich in der Liste der Aussteller, denen der Server vertraut ( von Zertifikatanforderungsnachricht vom Server).

Ich wandte mich an den Kunden, um ein neues Zertifikat zu erhalten, aber der Kunde beanstandete, dass alles für ihn gut funktionierte, und gab Python ein Skript, mit dem er normalerweise die Zertifikate auf Funktionalität überprüfte. Das Skript hat lediglich eine HTTPS-Anforderung mithilfe der Anforderungsbibliothek gesendet und 200 OK zurückgegeben. Ich war schließlich überrascht, als die gute alte Locke auch 200 OK zurückgab. Ich erinnerte mich sofort an den Witz: "Die ganze Firma geht aus dem Takt, ein Leutnant tritt ins Bein."

Curl ist natürlich ein seriöses Dienstprogramm, aber der TLS-Standard ist auch kein Stück Toilettenpapier. Da ich nicht wusste, was ich sonst noch überprüfen sollte, kletterte ich ziellos durch die Curl-Dokumentation und auf Github, wo ich einen so berühmten Käfer fand.

Der Reporter beschrieb genau den X-Test: Beim Curl mit dem Standard-Backend (OpenSSL) wurde er nicht ausgeführt, im Gegensatz zum Curl mit dem GnuTLS-Backend. Ich war nicht zu faul, sammelte Locken von der Quelle mit der Option --with-gnutls und schickte eine langmütige Anfrage. Und schließlich gab ein anderer Client neben dem JDK den HTTP-Status 500 zusammen mit der „Secutiry-Ausnahme“ zurück!

Ich schrieb dem Kunden darüber als Antwort auf das Argument „Nun, Curl funktioniert“ und erhielt ein neues Zertifikat, das neu generiert und ordentlich auf dem Server installiert wurde. Damit hat meine Konfiguration für WebClient einwandfrei funktioniert. Happy End.

Das SSL-Epos dauerte mehr als zwei Wochen mit der gesamten Korrespondenz (es umfasste das Studium detaillierter Java-Protokolle, die Auswahl des Codes eines anderen Projekts, das für den Kunden funktioniert, und die Auswahl der Nase).

Was mich neben dem Unterschied im Clientverhalten lange Zeit verwirrte, war, dass der Server so konfiguriert war, dass das Zertifikat angefordert, aber nicht überprüft wurde. Es gibt jedoch Erklärungen dafür in der Spezifikation:
Wenn ein Aspekt der Zertifikatkette nicht akzeptabel war (z. B. nicht von einer bekannten, vertrauenswürdigen Zertifizierungsstelle signiert wurde), kann der Server nach eigenem Ermessen entweder den Handshake fortsetzen (wenn der Client nicht authentifiziert ist) oder eine schwerwiegende Warnung senden.

Source: https://habr.com/ru/post/de414909/


All Articles