Hampir seluruh lalu lintas aplikasi modern melewati HTTPS—protokol HTTP di atas TLS. TLS menyediakan tiga jaminan: kerahasiaan (data terenkripsi), integritas (data tidak dimodifikasi), dan autentikasi (server memang pihak yang diklaim). Tanpa validasi sertifikat yang benar, ketiga jaminan itu runtuh: penyerang dapat berdiri di antara klien dan server, mengenkripsi lalu lintas dengan kunci yang ia kuasai, sementara klien percaya sedang berbicara langsung dengan server asli. Sayangnya, banyak implementasi TLS salah karena pengembang mematikan verifikasi untuk menghindari galat saat pengembangan. Pada chapter ini kita akan memahami fase handshake TLS, langkah validasi rantai sertifikat, pola certificate pinning, serta jebakan umum yang harus dihindari.
Membangun SSLContext #
SSLContext adalah titik masuk API TLS pada JDK. Kode di bawah membangun dua SSLContext: satu dengan trust store default (yang memvalidasi server terhadap CA yang dipercaya oleh JDK), satu lagi dengan TrustManager yang menerima semua sertifikat—pola yang berbahaya dan harus dihindari pada produksi.
// tls_dan_certificate_validation.kt
// TLS handshake concept. Cert chain validation. Pinning. Common pitfalls.
// This demo shows how to build SSLContext (default + insecure) and
// prints the conceptual validation steps without making network calls.
import java.security.KeyStore
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager
import java.security.cert.X509Certificate
// --- Build SSLContext using default trust store (JDK cacerts) ---
// On real connection, this validates server cert against trusted CAs.
fun buildDefaultSslContext(): SSLContext {
val ts = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
ts.init(null as KeyStore?) // null = default trust store (cacerts)
val ctx = SSLContext.getInstance("TLSv1.3")
ctx.init(null, ts.trustManagers, null)
return ctx
}
// --- Anti-pattern: insecure TrustManager that accepts ALL certificates ---
// NEVER do this in production. Shown here so you can recognize the pattern.
fun buildInsecureSslContext(): SSLContext {
val acceptAll = arrayOf(object : X509TrustManager {
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) {}
override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) {}
override fun getAcceptedIssuers(): Array<X509Certificate> = arrayOf()
})
val ctx = SSLContext.getInstance("TLSv1.3")
ctx.init(null, acceptAll, null)
return ctx
}
// --- List trusted CAs from default trust store (shows what JDK trusts) ---
fun listTrustedCaCount(): Int {
val ts = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
ts.init(null as KeyStore?)
var count = 0
for (tm in ts.trustManagers) {
if (tm is X509TrustManager) {
count += tm.acceptedIssuers.size
}
}
return count
}
// --- Concept: TLS handshake phases (no network, just text output) ---
fun describeTlsHandshake(): List<String> = listOf(
"1. ClientHello: kirim TLS version, cipher suites, random bytes, SNI",
"2. ServerHello: pilih TLS version + cipher suite, kirim random bytes",
"3. Certificate: server kirim chain sertifikat X.509 (leaf + intermediates)",
"4. Key Exchange: client + server tukar parameter untuk membuat shared secret",
"5. Finished: kedua belah pihak mengirim MAC atas handshake yang terjadi",
"6. Application Data: kirim data terenkripsi dengan kunci sesi yang disepakati"
)
// --- Concept: certificate chain validation steps ---
fun describeChainValidation(): List<String> = listOf(
"1. Parse sertifikat leaf (server) dan intermediate dari chain",
"2. Periksa rentang waktu: notBefore <= now <= notAfter",
"3. Cocokkan hostname dengan Subject Alternative Name (SAN) atau CN",
"4. Verifikasi tanda tangan sertifikat leaf oleh intermediate",
"5. Verifikasi tanda tangan intermediate oleh root CA yang dipercaya",
"6. Periksa status pencabutan via CRL atau OCSP"
)
// --- Concept: certificate pinning ---
fun describePinning(): List<String> = listOf(
"1. Hitung SHA-256 fingerprint atas sertifikat server (atau public key)",
"2. Bandingkan dengan daftar pin yang dikemas dalam aplikasi",
"3. Tolak koneksi bila fingerprint tidak cocok",
"Catatan: pin harus diperbarui saat sertifikat server dirotasi"
)
fun main() {
println("=== TLS dan Validasi Sertifikat (Demo Konseptual) ===")
println()
println("Catatan: demo ini tidak melakukan handshake TLS nyata ke server.")
println("Kode membangun SSLContext dan menjelaskan langkah-langkah validasi.")
println()
val ctxDefault = buildDefaultSslContext()
val ctxInsecure = buildInsecureSslContext()
println("SSLContext default protocol: ${ctxDefault.protocol}")
println("SSLContext insecure protocol: ${ctxInsecure.protocol}")
println("Jumlah CA dipercaya oleh trust store default: ${listTrustedCaCount()}")
println()
println("=== Fase Handshake TLS ===")
for (line in describeTlsHandshake()) println(line)
println()
println("=== Validasi Rantai Sertifikat ===")
for (line in describeChainValidation()) println(line)
println()
println("=== Certificate Pinning ===")
for (line in describePinning()) println(line)
println()
println("=== Jebakan Umum TLS ===")
println("1. Mematikan verifikasi hostname (setHostnameVerifier yang selalu true)")
println("2. TrustManager yang menerima semua sertifikat (buildInsecureSslContext di atas)")
println("3. Tidak memperbarui daftar pin setelah rotasi sertifikat server")
println("4. Menggunakan TLS 1.0/1.1 yang sudah usang; minimal TLS 1.2, idealnya TLS 1.3")
}Output yang Dihasilkan:
=== TLS dan Validasi Sertifikat (Demo Konseptual) ===
Catatan: demo ini tidak melakukan handshake TLS nyata ke server.
Kode membangun SSLContext dan menjelaskan langkah-langkah validasi.
SSLContext default protocol: TLSv1.3
SSLContext insecure protocol: TLSv1.3
Jumlah CA dipercaya oleh trust store default: 150
=== Fase Handshake TLS ===
1. ClientHello: kirim TLS version, cipher suites, random bytes, SNI
2. ServerHello: pilih TLS version + cipher suite, kirim random bytes
3. Certificate: server kirim chain sertifikat X.509 (leaf + intermediates)
4. Key Exchange: client + server tukar parameter untuk membuat shared secret
5. Finished: kedua belah pihak mengirim MAC atas handshake yang terjadi
6. Application Data: kirim data terenkripsi dengan kunci sesi yang disepakati
=== Validasi Rantai Sertifikat ===
1. Parse sertifikat leaf (server) dan intermediate dari chain
2. Periksa rentang waktu: notBefore <= now <= notAfter
3. Cocokkan hostname dengan Subject Alternative Name (SAN) atau CN
4. Verifikasi tanda tangan sertifikat leaf oleh intermediate
5. Verifikasi tanda tangan intermediate oleh root CA yang dipercaya
6. Periksa status pencabutan via CRL atau OCSP
=== Certificate Pinning ===
1. Hitung SHA-256 fingerprint atas sertifikat server (atau public key)
2. Bandingkan dengan daftar pin yang dikemas dalam aplikasi
3. Tolak koneksi bila fingerprint tidak cocok
Catatan: pin harus diperbarui saat sertifikat server dirotasi
=== Jebakan Umum TLS ===
1. Mematikan verifikasi hostname (setHostnameVerifier yang selalu true)
2. TrustManager yang menerima semua sertifikat (buildInsecureSslContext di atas)
3. Tidak memperbarui daftar pin setelah rotasi sertifikat server
4. Menggunakan TLS 1.0/1.1 yang sudah usang; minimal TLS 1.2, idealnya TLS 1.3Mari kita bedah bagian kunci dari kode di atas:
buildDefaultSslContext: memanggilTrustManagerFactory.getDefaultAlgorithm()laluts.init(null as KeyStore?)yang memuat trust store default (filecacertsbawaan JDK, berisi sekitar 150 CA root). SSLContext yang dihasilkan memvalidasi server terhadap CA-CA tersebut.buildInsecureSslContext: anti-pattern.X509TrustManageranonymous dengan implementasi kosong menerima semua sertifikat, termasuk yang ditandatangani sendiri oleh penyerang. Pola ini sering muncul saat pengembang kesulitan menangani galat SSL di lingkungan dev—sayangnya sering terbawa ke produksi.listTrustedCaCount: menghitung berapa banyak CA yang dipercaya oleh JDK. Angka ini bervariasi antar mesin, namun biasanya sekitar 100–200. Pembaruan JDK secara berkala menambah atau mencabut CA yang sudah tidak dipercaya.describeTlsHandshake: daftar enam fase handshake TLS. ClientHello dan ServerHello menegosiasikan versi dan cipher suite; fase Certificate mengirim rantai sertifikat server; Key Exchange menghasilkan shared secret; Finished menutup handshake dengan MAC verifikasi.describeChainValidation: enam langkah validasi rantai. Sertifikat daun ditandatangani oleh intermediate, intermediate oleh root CA. Hostname cocok dengan SAN atau CN. Waktu valid. Status pencabutan diperiksa via CRL atau OCSP.
Fase Handshake TLS #
Handshake TLS terjadi sekali per koneksi, lalu data aplikasi mengalir di atas saluran terenkripsi. Berikut ringkasan tiap fase:
Klien Server
| ── ClientHello ───────────────→ |
| ←─ ServerHello ──────────────── |
| ←─ Certificate (chain) ─────── |
| ── Key Exchange ──────────────→ |
| ── Finished (MAC) ────────────→ |
| ←─ Finished (MAC) ───────────── |
|═══ Application Data (encrypted) ═|Pada TLS 1.3, handshake lebih ringkas—Key Exchange dan Finished digabung, sehingga total round trip lebih sedikit. TLS 1.3 juga menghapus algoritma lemah seperti RSA key transport dan SHA-1.
Validasi Rantai Sertifikat #
Saat klien menerima sertifikat server, validasi dilakukan dalam beberapa lapis. Sertifikat daun (leaf) berisi nama domain server. Intermediate menandatangani daun. Root CA yang dipercaya menandatangani intermediate. Bila salah satu tautan gagal—tanda tangan tidak cocok, waktu kedaluwarsa, atau hostname tidak sesuai—validasi gagal dan koneksi ditolak.
import javax.net.ssl.HostnameVerifier
import javax.net.ssl.SSLSession
val verifier = HostnameVerifier { hostname, session: SSLSession ->
// Verifier default memeriksa SAN/CN. Jangan timpa kecuali tahu risikonya.
hostname == session.peerPrincipal?.toString()?.substringAfter("CN=")?.substringBefore(",")
}Pada produksi, biarkan verifier default yang memeriksa SAN (Subject Alternative Name). SAN adalah daftar nama domain yang diizinkan untuk sertifikat tersebut; CN sudah dianggap usang untuk pencocokan hostname.
Certificate Pinning #
Pinning menambahkan lapisan pertahanan: walaupun CA root dipercaya, klien hanya menerima sertifikat server tertentu yang fingerprint-nya sudah diketahui. Pinning melindungi dari kompromi CA—kasus di mana CA yang dipercaya salah menerbitkan sertifikat untuk domain yang tidak dimiliki pemilik sah.
import java.security.MessageDigest
import java.security.cert.X509Certificate
fun pinFingerprint(cert: X509Certificate): String {
val md = MessageDigest.getInstance("SHA-256")
val digest = md.digest(cert.encoded)
return digest.joinToString(":") { "%02X".format(it) }
}
fun isPinned(cert: X509Certificate, pinned: Set<String>): Boolean =
pinFingerprint(cert) in pinnedPinning boleh diterapkan pada sertifikat daun (rentan rotasi) atau pada public key (lebih stabil). Pin pada public key lebih disarankan karena tidak berubah saat sertifikat dirotasi selama pasangan kunci sama.
Jebakan Umum TLS #
Berikut empat kesalahan yang sering muncul:
- Mematikan verifikasi hostname:
setHostnameVerifier { _, _ -> true }menerima semua hostname. Sertifikat untukevil.example.comakan diterima untuk koneksi kebank.example.com. - TrustManager yang menerima semua sertifikat:
buildInsecureSslContextdi atas. Menonaktifkan seluruh lapisan validasi TLS. - Pin kedaluwarsa: aplikasi tidak diperbarui saat server merotasi sertifikat. Pengguna tidak dapat konek.
- TLS 1.0/1.1: versi usang yang rentan serangan seperti BEAST dan POODLE. Paksa minimal TLS 1.2, idealnya TLS 1.3.
Referensi Lanjutan #
- OWASP Cheat Sheet Series — panduan OWASP untuk konfigurasi TLS dan validasi sertifikat X.509
Penutup #
TLS memberikan fondasi komunikasi aman, namun konfigurasi yang salah menghapus manfaatnya. Pahami fase handshake, validasi rantai sertifikat, dan kapan pinning diperlukan. Chapter berikutnya akan membahas deserialization security—bagaimana data yang dibaca dari sumber eksternal dapat menjadi vektor serangan remote code execution bila format dan parser tidak dipilih dengan hati-hati.