Bab sebelumnya menutup dengan pertanyaan menggantung: bagaimana pembaca mempercayai bahwa kunci publik di depannya benar-benar milik pihak yang dituju? Transport Layer Security (TLS) menjawabnya dengan infrastruktur kepercayaan berlapis — sertifikat, rantai penandatanganan, dan otoritas sertifikat — yang bekerja setiap kali HTTPS dibuka. Kode Java kita hampir selalu menjadi klien TLS: HttpClient, JDBC over TLS, hingga koneksi ke message broker. Kabar terbaiknya: klien Java memvalidasi sertifikat secara bawaan, dan sebagian besar insiden keamanan TLS di aplikasi justru berasal dari kode yang menonaktifkan validasi itu demi “mengatasi” error pengembangan. Bab ini membedah handshake TLS, rantai kepercayaan, dan verifikasi hostname — lalu menutup dengan pola mTLS untuk komunikasi antar layanan.
Handshake TLS dalam Beberapa Langkah #
Sebelum aplikasi mengirim satu byte data, TLS membangun kanal terenkripsi lewat jabat tangan yang disepakati. TLS 1.3 — versi aktif bawaan JDK — merangkai prosesnya dalam satu perjalanan bolak-balik:
Dua momen pada diagram itu adalah inti keamanan kita. Pertama, Certificate: server mengirim rantai sertifikat berisi identitas dan kunci publiknya. Kedua, langkah “validasi rantai + cek hostname” — inilah satu-satunya kesempatan klien memastikan bahwa server di ujung koneksi benar-benar pihak yang diklaim. Setelah jabat tangan selesai, seluruh komunikasi memakai enkripsi simetris dengan kunci sesi yang dibangkitkan bersama — persis komposisi asimetris-untuk-kunci, simetris-untuk-data dari bab Fundamentals Cryptography.
Rantai Kepercayaan dari Root CA #
Validasi rantai bekerja seperti surat keterangan beruntun. Sertifikat leaf milik example.com ditandatangani kunci privat intermediate CA; sertifikat intermediate itu sendiri ditandatangani root CA; dan root CA yang menjadi ujung rantai harus sudah kita percayai lebih dulu — daftar kepercayaan itu tidak diunduh dari internet, melainkan dibundel JVM dalam berkas cacerts. Program berikut memeriksa kemampuan TLS JDK dan menghitung berapa root CA yang dipercaya di luar kotak:
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyStore;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
public class KemampuanTls {
public static void main(String[] args) throws Exception {
// 1) Kemampuan protokol bawaan JDK.
SSLContext konteks = SSLContext.getDefault();
System.out.println("SSLContext bawaan : " + konteks.getProtocol());
try (SSLSocket soket = (SSLSocket) konteks.getSocketFactory().createSocket()) {
System.out.println("Protokol aktif : "
+ String.join(", ", soket.getEnabledProtocols()));
System.out.println("Cipher suite aktif : "
+ soket.getEnabledCipherSuites().length + " suite");
}
// 2) Muatan kepercayaan bawaan: daftar root CA di berkas cacerts.
Path cacerts = Path.of(System.getProperty("java.home"), "lib", "security", "cacerts");
KeyStore penyimpanan = KeyStore.getInstance(KeyStore.getDefaultType());
try (var masukan = Files.newInputStream(cacerts)) {
penyimpanan.load(masukan, "changeit".toCharArray());
}
System.out.println("Root CA terpercaya JDK : " + penyimpanan.size()
+ " entri (cacerts)");
}
}Output yang Dihasilkan:
SSLContext bawaan : Default
Protokol aktif : TLSv1.3, TLSv1.2
Cipher suite aktif : 31 suite
Root CA terpercaya JDK : 118 entri (cacerts)Output memperlihatkan JDK 25 mengaktifkan TLS 1.3 dan 1.2 dengan 31 cipher suite, dan memercayai 118 root CA — jumlah pastinya berbeda antar versi JDK, tetapi polanya sama: produsen browser dan sistem operasi memelihara daftar akar kepercayaan, dan OpenJDK mendistribusikannya bersama JDK. Saat koneksi HTTPS gagal dengan SunCertPathBuilderException: unable to find valid certification path to requested target, artinya rantai sertifikat server tidak bermuara ke salah satu dari 118 entri itu — sertifikat kedaluwarsa, self-signed, atau intermediate yang lupa disertakan server.
Bedah baris-per-baris untuk bagian trust store:
- Baris 23-24 (
Path.of(System.getProperty("java.home"), "lib", "security", "cacerts")): lokasi trust store ditemukan lewatjava.home— bukan path absolut — sehingga kode tetap benar di sistem operasi dan instalasi JDK mana pun. - Baris 25-28 (
KeyStore.getInstance(...)danload(masukan, "changeit".toCharArray())):cacertsadalah KeyStore berformat PKCS12 dengan sandi bawaanchangeit— sandi ini bukan rahasia (tertulis di dokumentasi resmi JDK) karena fungsinya menjaga integritas berkas, bukan kerahasiaan. Operasiloadmem-parse seluruh entri. - Baris 29 (
penyimpanan.size()): jumlah entri sama dengan jumlah sertifikat root yang akan diterimaSSLContextbawaan saat memvalidasi rantai.
Validasi Hostname yang Sering Dilupakan #
Rantai sertifikat yang sah belum cukup — sertifikat tersebut harus milik server yang kita tuju. Sertifikat membawa daftar hostname sah di field Subject Alternative Name; klien wajib memeriksa bahwa hostname pada URL cocok dengan daftar itu. Tanpa pemeriksaan ini, sertifikat sah milik evil.example bisa dipakai menyamar sebagai bank.example bila penyerang berhasil mengarahkan lalu lintas kita ke servernya.
Di sini letak kebahagiaan bekerja dengan Java modern: HttpClient bawaan melakukan keduanya — validasi rantai ke root CA dan verifikasi hostname — tanpa satu baris kode tambahan:
var klien = java.net.http.HttpClient.newHttpClient();
var permintaan = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("https://example.com"))
.build();
// Sertifikat kedaluwarsa, hostname tak cocok, atau rantai tak dikenal
// -> java.net.http.HttpRequest melontarkan SSLHandshakeException / SSLPeerUnverifiedException.
// Tidak ada kode validasi yang perlu kita tulis.Praktik yang menyertainya sederhana: biarkan validasi bawaan bekerja, dan perlakukan exception SSL sebagai kegagalan koneksi yang memutus permintaan (fail closed). Untuk kebutuhan uji, solusi yang benar adalah menambahkan sertifikat uji ke trust store khusus pengembangan — bukan mematikan validasi.
mTLS Dua Arah untuk Komunikasi Antar Layanan #
Model umum TLS hanya mengautentikasi server. Mutual TLS (mTLS) menambah otentikasi ke arah sebaliknya: klien juga mengirim sertifikatnya, dan server memverifikasi rantai klien sebelum menerima koneksi. Arsitektur zero trust antar microservice memakai pola ini — setiap layanan membawa identitas sertifikatnya sendiri, dan koneksi hanya terjadi bila kedua pihak saling memverifikasi.
// 1) KeyStore berisi sertifikat + kunci privat layanan kita (identitas klien).
System.setProperty("javax.net.ssl.keyStore", "/etc/app/identitas.p12");
System.setProperty("javax.net.ssl.keyStorePassword", System.getenv("KEYSTORE_PASSWORD"));
System.setProperty("javax.net.ssl.keyStoreType", "PKCS12");
// 2) TrustStore berisi CA internal yang berhak menandatangani mitra.
System.setProperty("javax.net.ssl.trustStore", "/etc/app/ca-internal.p12");
System.setProperty("javax.net.ssl.trustStorePassword", System.getenv("TRUSTSTORE_PASSWORD"));
System.setProperty("javax.net.ssl.trustStoreType", "PKCS12");
// SSLContext bawaan otomatis memuat keduanya; sertifikat klien
// dikirim saat server meminta (mutual authentication).Perhatikan bahwa sandi KeyStore dibaca dari variabel lingkungan — kembali ke pola Secrets Management, bukan konstanta di kode. Sertifikat mTLS internal sebaiknya berumur pendek (90 hari atau kurang) dan dirotasi otomatis, sehingga kebocoran satu sertifikat berdampak sempit — prinsip rotasi dari bab rahasia, diterapkan pada identitas.
TLS kini menjaga data bergerak antar sistem. Bab berikutnya menghadapi ancaman yang datang menyamar sebagai data biasa: Deserialization Security — ketika berkas atau pesan yang tampak seperti objek tersimpan ternyata membawa kode yang tereksekusi saat objek itu dibangun kembali.
Referensi #
- Oracle: javax.net.ssl Package (JSSE)
- Oracle: java.net.http HttpClient
- OWASP Transport Layer Protection Cheat Sheet
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
KemampuanTls.java
Unduh berkasimport java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyStore;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
public class KemampuanTls {
public static void main(String[] args) throws Exception {
// 1) Kemampuan protokol bawaan JDK.
SSLContext konteks = SSLContext.getDefault();
System.out.println("SSLContext bawaan : " + konteks.getProtocol());
try (SSLSocket soket = (SSLSocket) konteks.getSocketFactory().createSocket()) {
System.out.println("Protokol aktif : "
+ String.join(", ", soket.getEnabledProtocols()));
System.out.println("Cipher suite aktif : "
+ soket.getEnabledCipherSuites().length + " suite");
}
// 2) Muatan kepercayaan bawaan: daftar root CA di berkas cacerts.
Path cacerts = Path.of(System.getProperty("java.home"), "lib", "security", "cacerts");
KeyStore penyimpanan = KeyStore.getInstance(KeyStore.getDefaultType());
try (var masukan = Files.newInputStream(cacerts)) {
penyimpanan.load(masukan, "changeit".toCharArray());
}
System.out.println("Root CA terpercaya JDK : " + penyimpanan.size()
+ " entri (cacerts)");
}
}