Halo, Habr!
Baru-baru ini, saya harus mengacaukan SSL dengan otentikasi bersama ke Spring Reactive Webclient. Tampaknya itu masalah sederhana, tetapi mengakibatkan pengembaraan sumber-sumber JDK dengan akhir yang tak terduga. Pengalaman diperoleh untuk seluruh artikel, yang mungkin berguna bagi insinyur dalam tugas sehari-hari atau dalam persiapan untuk wawancara.
Pernyataan masalah
Ada layanan REST di sisi pelanggan yang berfungsi melalui HTTPS.
Anda harus mengaksesnya dari aplikasi Java klien.Hal pertama yang saya diberikan dalam pencarian ini adalah 2 file dengan ekstensi .pem - sertifikat klien dan kunci pribadi. Saya memeriksa kinerja mereka menggunakan tukang pos: Saya menentukan jalur untuk mereka di pengaturan dan, setelah menarik permintaan, memastikan bahwa server merespons dengan 200 OK dan badan tanggapan yang bermakna. Secara terpisah, saya memeriksa bahwa tanpa sertifikat klien, server mengembalikan status HTTP 500 dan pesan singkat di badan respons bahwa "Pengecualian keamanan" terjadi dengan kode tertentu.
Selanjutnya, Anda harus mengkonfigurasi aplikasi Java klien dengan benar.
Untuk permintaan REST, saya menggunakan Spring Reactive WebClient dengan I / O yang tidak menghalangi.
Dokumentasi memiliki
contoh bagaimana dapat dikustomisasi dengan melemparkan objek SslContext padanya, yang hanya menyimpan sertifikat dan kunci pribadi.
Konfigurasi saya dalam versi sederhana hampir salin-tempel dari dokumentasi:
SslContext sslContext = SslContextBuilder .forClient() .keyManager(…) .build(); ClientHttpConnector connector = new ReactorClientHttpConnector( builder -> builder.sslContext(sslContext)); WebClient webClient = WebClient.builder() .clientConnector(connector).build();
Mengikuti prinsip TDD, saya juga menulis tes yang menggunakan WebTestClient, bukan WebClient, yang menampilkan banyak informasi debug. Penegasan pertama adalah seperti ini:
webTestClient .post() .uri([ ]) .body(BodyInserters.fromObject([ , , ])) .exchange() .expectStatus().isOk()
Tes sederhana ini tidak segera lulus: server mengembalikan 500 dengan tubuh yang sama seolah-olah tukang pos tidak menentukan sertifikat klien.
Secara terpisah, saya perhatikan bahwa selama debugging, saya menyalakan opsi "jangan periksa sertifikat server", yaitu, saya melewati contoh InsecureTrustManagerFactory sebagai TrustManager untuk SslContext. Langkah ini mubazir, tetapi pasti mengecualikan setengah dari opsi.
Informasi debug dalam tes tidak menjelaskan masalah, tetapi sepertinya ada yang salah pada tahap jabat tangan SSL, jadi saya memutuskan untuk membandingkan lebih detail bagaimana koneksi terjadi dalam kedua kasus: untuk tukang pos dan untuk klien Java. Semua ini dapat dilihat menggunakan Wireshark - ini adalah penganalisa lalu lintas jaringan yang sangat populer. Pada saat yang sama, saya melihat bagaimana jabat tangan SSL terjadi dengan otentikasi dua arah, sehingga dapat dikatakan, hidup (mereka suka menanyakan hal ini selama wawancara):
- Pertama-tama, klien mengirimkan pesan Halo Klien yang berisi meta-informasi seperti versi protokol dan daftar algoritma enkripsi yang didukungnya.
- Sebagai tanggapan, server segera mengirim paket pesan berikut: Server Hello, Certificate, Server Key Exchange, Permintaan Sertifikat, Server Hello Done .
Server Hello menunjukkan algoritma enkripsi yang dipilih oleh server (cipher suite). Di dalam Sertifikat terdapat sertifikat server. Server Key Exchange membawa beberapa informasi yang diperlukan untuk enkripsi, tergantung pada algoritma yang dipilih (kami tidak tertarik pada detail sekarang, jadi kami menganggap bahwa ini hanya kunci publik, meskipun ini salah!). Juga, dalam hal otentikasi dua arah, dalam pesan Permintaan Sertifikat , server membuat permintaan untuk sertifikat klien dan menjelaskan format mana yang didukungnya dan penerbit mana yang dipercayai. - Setelah menerima informasi ini, klien memverifikasi sertifikat server dan mengirimkan sertifikatnya, "kunci publik", dan informasi lainnya dalam pesan berikut: Sertifikat, Pertukaran Kunci Klien, Verifikasi Sertifikat . Yang terakhir adalah pesan ChangeCipherSpec , menunjukkan bahwa semua komunikasi lebih lanjut akan terjadi dalam bentuk terenkripsi
- Akhirnya, setelah semua pengocokan ini, server akan memeriksa sertifikat klien dan, jika semuanya baik-baik saja dengan itu, itu akan memberikan jawaban.
Setelah lima belas menit bertahan di lalu lintas, saya perhatikan bahwa klien Java, dalam menanggapi
Permintaan Sertifikat dari server, untuk beberapa alasan tidak mengirim sertifikatnya, tidak seperti klien Postman. Artinya, ada pesan Sertifikat, tetapi kosong.
Selanjutnya, saya perlu melihat dulu pada
spesifikasi protokol TLS , yang secara harfiah mengatakan sebagai berikut:
Jika daftar Certificate_authorities dalam pesan permintaan sertifikat tidak kosong, salah satu sertifikat dalam rantai sertifikat HARUS dikeluarkan oleh salah satu CA yang terdaftar.
Kami berbicara tentang daftar Certificate_authorities yang ditentukan dalam pesan
Permintaan Sertifikat yang berasal dari server. Sertifikat klien (setidaknya salah satu rantai) harus ditandatangani oleh salah satu penerbit yang tercantum dalam daftar ini. Kami menyebutnya
cek X.Saya tidak tahu tentang kondisi ini dan menemukannya ketika saya mencapai kedalaman JDK dalam debugging (saya memilikinya JDK9). Netty's HttpClient, yang mendasari Spring WebClient, menggunakan SslEngine dari JDK secara default. Atau, Anda dapat beralih ke penyedia OpenSSL dengan menambahkan dependensi yang diperlukan, tapi saya akhirnya tidak membutuhkan ini.
Jadi, saya menetapkan breakpoints di dalam kelas sun.security.ssl.ClientHandshaker dan di serverHelloDone message handler saya menemukan cek X yang tidak lulus: tidak ada emiten di rantai sertifikat klien yang ada dalam daftar penerbit yang dipercayai oleh server ( dari pesan
Permintaan Sertifikat dari server).
Saya menoleh ke pelanggan untuk mendapatkan sertifikat baru, tetapi pelanggan itu keberatan bahwa semuanya berjalan baik untuknya, dan menyerahkan skrip Python, yang dengannya ia biasanya memeriksa fungsionalitas sertifikat tersebut. Script melakukan tidak lebih dari mengirim permintaan HTTPS menggunakan perpustakaan Permintaan, dan mengembalikan 200 OK. Saya akhirnya terkejut ketika ikal lama yang baik juga mengembalikan 200 OK. Saya langsung ingat lelucon itu: "Seluruh kompi keluar dari langkah, satu letnan melangkah di kaki."
Curl, tentu saja, utilitas yang memiliki reputasi baik, tetapi standar TLS juga bukan selembar kertas toilet. Tidak tahu harus memeriksa apa lagi, saya memanjat tanpa tujuan berkeliling dokumentasi curl, dan di Github, di mana saya menemukan bug yang terkenal.
Reporter itu menggambarkan dengan tepat tes X: di curl dengan backend default (OpenSSL), itu tidak berjalan, tidak seperti curl dengan backend GnuTLS. Saya tidak terlalu malas, mengumpulkan ikal dari sumber dengan opsi - dengan
-gnutl , dan mengirim permintaan lama. Dan, akhirnya, klien lain, selain JDK, mengembalikan status HTTP 500 bersama dengan "Pengecualian Secutiry"!
Saya menulis tentang hal ini kepada pelanggan sebagai tanggapan atas argumen "Yah, ikal bekerja," dan menerima sertifikat baru, dibuat ulang dan dipasang dengan rapi di server. Dengannya, konfigurasi saya untuk WebClient bekerja dengan baik. Selamat berakhir.
Epik SSL membutuhkan waktu lebih dari dua minggu dengan semua korespondensi (termasuk studi log Java terperinci, mengambil kode proyek lain yang berfungsi untuk pelanggan, dan hanya memilih hidungnya).
Apa yang membuat saya bingung untuk waktu yang lama, selain perbedaan dalam perilaku klien, adalah bahwa server dikonfigurasi sedemikian rupa sehingga sertifikat diminta, tetapi tidak memverifikasi. Namun, ada penjelasan untuk ini dalam spesifikasi:
Juga, jika beberapa aspek rantai sertifikat tidak dapat diterima (mis., Itu tidak ditandatangani oleh CA yang dikenal dan tepercaya), server MUNGKIN dengan kebijaksanaannya melanjutkan jabat tangan (menganggap klien tidak diauthentikasi) atau mengirim peringatan fatal.