Data yang berpindah lewat HTTP polos bisa dibaca siapa pun yang berada di jalurnya: operator jaringan hotspot, penyedia VPS, sampai perangkat penyadap di tengah. TLS mengenkripsi isi paket sekaligus membuktikan identitas pemilik domain lewat sertifikat, dan HTTP yang berjalan di atasnya itulah HTTPS. Go memanggil TLS lewat package crypto/tls dan mengintegrasikannya ke net/http cukup dengan satu panggilan ListenAndServeTLS. Chapter ini membahas konsep sertifikat, konfigurasi versi TLS modern, lalu membuktikan semuanya dengan server HTTPS lokal dari httptest.

Sertifikat dan Kunci #

Server TLS membutuhkan dua berkas: sertifikat (cert.pem) yang berisi kunci publik dan identitas domain, serta kunci privat (key.pem) yang harus dijaga kerahasiaannya. Keduanya berformat PEM — teks ber-base64 dengan garis pembatas -----BEGIN CERTIFICATE-----. Kepercayaan bersifat berantai: browser sudah menyimpan daftar root CA terkemuka, CA menandatangani sertifikat domain kita, dan rantai itu diverifikasi ulang pada tiap handshake.

Sertifikat dapat ditandatangani sendiri (self-signed) tanpa CA, tetapi klien akan menolaknya karena rantai kepercayaannya tidak dikenali. Untuk produksi, gunakan sertifikat dari CA publik — paling mudah lewat protokol ACME: alat certbot mengurus penerbitan dan pembaruan otomatis, sedangkan program Go bisa memakai package golang.org/x/crypto/acme/autocert (https://pkg.go.dev/golang.org/x/crypto/acme/autocert ) agar sertifikat dikelola langsung oleh server.

Menjalankan HTTPS dengan ListenAndServeTLS #

Bentuk paling ringkas mengganti ListenAndServe dengan ListenAndServeTLS sambil membubuhkan path kedua berkas (fragmen tidak dijalankan karena memakai port sungguhan):

go
srv := &http.Server{
	Addr:    ":8443",
	Handler: mux,
	TLSConfig: &tls.Config{
		MinVersion: tls.VersionTLS12,
	},
}

if err := srv.ListenAndServeTLS("cert.pem", "key.pem"); err != nil {
	log.Fatal(err)
}

Field TLSConfig pada contoh di atas bukan hiasan: di sanalah kebijakan TLS ditetapkan. MinVersion: tls.VersionTLS12 menolak klien yang masih menawarkan TLS 1.0 atau 1.1 — protokol lawas yang sudah dinyatakan tidak aman. Cipher suite lama seperti RC4 dan 3DES juga sudah dinonaktifkan bawaan Go sejak lama.

Satu header penting melengkapi TLS di lapisan aplikasi: Strict-Transport-Security (HSTS) memerintahkan browser memakai HTTPS untuk kunjungan berikutnya, mencegah penurunan kembali ke HTTP:

go
func wajibHTTPS(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Strict-Transport-Security", "max-age=31536000")
		next.ServeHTTP(w, r)
	})
}

Nilai max-age=31536000 berarti komitmen selama satu tahun; header ini sebaiknya dipasang lewat middleware sehingga berlaku untuk seluruh route.

Demo TLS dengan httptest #

Package httptest bisa menjalankan server HTTPS sungguhan dengan sertifikat self-signed yang dibuatnya otomatis. Klien yang dikembalikan ts.Client() sudah diprogram memercayai sertifikat itu, jadi handshake berhasil tanpa menyentuh kepercayaan sistem. Berkas contoh.go menguji empat skenario — koneksi sukses dengan HTTP/2, klien tanpa CA tepercaya, pembatasan versi TLS, dan bentuk ringkas NewTLSServer:

go
package main

import (
	"crypto/tls"
	"crypto/x509"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"strings"
	"time"
)

// namaVersiTLS menerjemahkan kode versi TLS menjadi nama yang mudah dibaca.
func namaVersiTLS(v uint16) string {
	switch v {
	case tls.VersionTLS12:
		return "TLS 1.2"
	case tls.VersionTLS13:
		return "TLS 1.3"
	default:
		return fmt.Sprintf("versi 0x%04x", v)
	}
}

func main() {
	// httptest membuat sertifikat self-signed otomatis khusus untuk demo lokal.
	ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "data rahasia dari server")
	}))
	ts.EnableHTTP2 = true
	ts.StartTLS()
	defer ts.Close()

	// Bagian 1: klien yang memercayai sertifikat server.
	klien := ts.Client()

	resp, err := klien.Get(ts.URL)
	if err != nil {
		fmt.Println("permintaan HTTPS gagal:", err)
		return
	}
	isi, _ := io.ReadAll(resp.Body)
	resp.Body.Close()

	fmt.Println("status respons HTTPS:", resp.Status)
	fmt.Println("protokol yang dinegosiasikan:", resp.Proto)
	fmt.Println("koneksi terenkripsi TLS:", resp.TLS != nil)
	fmt.Println("versi TLS:", namaVersiTLS(resp.TLS.Version))
	fmt.Println("isi respons:", strings.TrimSpace(string(isi)))

	// Bagian 2: klien tanpa CA tepercaya ditolak saat verifikasi sertifikat.
	klienPolos := &http.Client{Timeout: 2 * time.Second}
	_, err = klienPolos.Get(ts.URL)
	fmt.Println("klien tanpa CA tepercaya ditolak:", err != nil)
	var otoritas x509.UnknownAuthorityError
	fmt.Println("penyebab sertifikat tidak dikenal:", errors.As(err, &otoritas))

	// Bagian 3: klien yang dibatasi maksimum TLS 1.2.
	pool := x509.NewCertPool()
	pool.AddCert(ts.Certificate())

	klien12 := &http.Client{
		Timeout: 2 * time.Second,
		Transport: &http.Transport{
			TLSClientConfig: &tls.Config{
				MaxVersion: tls.VersionTLS12,
				RootCAs:    pool,
			},
		},
	}

	resp2, err := klien12.Get(ts.URL)
	if err != nil {
		fmt.Println("permintaan TLS 1.2 gagal:", err)
		return
	}
	io.Copy(io.Discard, resp2.Body)
	resp2.Body.Close()

	fmt.Println("klien dibatasi TLS 1.2, status:", resp2.Status)
	fmt.Println("versi TLS hasil negosiasi:", namaVersiTLS(resp2.TLS.Version))

	// Bagian 4: bentuk ringkas httptest.NewTLSServer.
	tsRingkas := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "versi ringkas")
	}))
	defer tsRingkas.Close()

	resp3, err := tsRingkas.Client().Get(tsRingkas.URL)
	if err != nil {
		fmt.Println("permintaan ke NewTLSServer gagal:", err)
		return
	}
	io.Copy(io.Discard, resp3.Body)
	resp3.Body.Close()
	fmt.Println("NewTLSServer + client bawaan:", resp3.Status, "|", resp3.Proto, "|", namaVersiTLS(resp3.TLS.Version))
}

Output yang Dihasilkan:

text
status respons HTTPS: 200 OK
protokol yang dinegosiasikan: HTTP/2.0
koneksi terenkripsi TLS: true
versi TLS: TLS 1.3
isi respons: data rahasia dari server
klien tanpa CA tepercaya ditolak: true
penyebab sertifikat tidak dikenal: true
klien dibatasi TLS 1.2, status: 200 OK
versi TLS hasil negosiasi: TLS 1.2
NewTLSServer + client bawaan: 200 OK | HTTP/1.1 | TLS 1.3

Mari kita telaah bagian-bagian penting program di atas:

  1. Baris 29-34: httptest.NewUnstartedServer dipakai agar field EnableHTTP2 bisa diisi sebelum StartTLS; satu panggilan httptest.NewTLSServer(handler) melakukan hal yang sama tanpa opsi HTTP/2.
  2. Baris 37: ts.Client() mengembalikan klien yang transport-nya sudah memercayai sertifikat server pengujian.
  3. Baris 39-51: permintaan sukses — status 200 OK, protokol HTTP/2.0 hasil negosiasi ALPN, resp.TLS terisi, versi TLS 1.3.
  4. Baris 53-58: klien polos tanpa CA tepercaya gagal; errors.As menemukan x509.UnknownAuthorityError sebagai akar masalahnya.
  5. Baris 61-62: sertifikat server diambil lewat ts.Certificate() lalu dimasukkan ke x509.CertPool — cara manual membangun kepercayaan pada client kustom.
  6. Baris 64-72: tls.Config{MaxVersion: tls.VersionTLS12} membatasi negosiasi sampai TLS 1.2; RootCAs wajib diisi agar klien kustom ikut memercayai sertifikat pengujian.
  7. Baris 82-83: koneksi tetap berhasil dengan status 200, tetapi versi yang dinegosiasikan turun ke TLS 1.2 — bukti bahwa versi akhir adalah kesepakatan dua pihak.
  8. Baris 85-98: httptest.NewTLSServer(handler) adalah bentuk ringkas tanpa opsi HTTP/2; klien bawaannya tetap memercayai sertifikat dan permintaan tetap sukses dengan status 200.

Dengan TLS, data berpindah aman dan identitas terbukti. Satu kebutuhan klasik server web masih tersisa: menyajikan berkas statis seperti CSS dan gambar — tugas yang dijawab http.FileServer pada chapter penutup section ini.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

contoh.go

Unduh berkas
go
package main

import (
	"crypto/tls"
	"crypto/x509"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"strings"
	"time"
)

// namaVersiTLS menerjemahkan kode versi TLS menjadi nama yang mudah dibaca.
func namaVersiTLS(v uint16) string {
	switch v {
	case tls.VersionTLS12:
		return "TLS 1.2"
	case tls.VersionTLS13:
		return "TLS 1.3"
	default:
		return fmt.Sprintf("versi 0x%04x", v)
	}
}

func main() {
	// httptest membuat sertifikat self-signed otomatis khusus untuk demo lokal.
	ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "data rahasia dari server")
	}))
	ts.EnableHTTP2 = true
	ts.StartTLS()
	defer ts.Close()

	// Bagian 1: klien yang memercayai sertifikat server.
	klien := ts.Client()

	resp, err := klien.Get(ts.URL)
	if err != nil {
		fmt.Println("permintaan HTTPS gagal:", err)
		return
	}
	isi, _ := io.ReadAll(resp.Body)
	resp.Body.Close()

	fmt.Println("status respons HTTPS:", resp.Status)
	fmt.Println("protokol yang dinegosiasikan:", resp.Proto)
	fmt.Println("koneksi terenkripsi TLS:", resp.TLS != nil)
	fmt.Println("versi TLS:", namaVersiTLS(resp.TLS.Version))
	fmt.Println("isi respons:", strings.TrimSpace(string(isi)))

	// Bagian 2: klien tanpa CA tepercaya ditolak saat verifikasi sertifikat.
	klienPolos := &http.Client{Timeout: 2 * time.Second}
	_, err = klienPolos.Get(ts.URL)
	fmt.Println("klien tanpa CA tepercaya ditolak:", err != nil)
	var otoritas x509.UnknownAuthorityError
	fmt.Println("penyebab sertifikat tidak dikenal:", errors.As(err, &otoritas))

	// Bagian 3: klien yang dibatasi maksimum TLS 1.2.
	pool := x509.NewCertPool()
	pool.AddCert(ts.Certificate())

	klien12 := &http.Client{
		Timeout: 2 * time.Second,
		Transport: &http.Transport{
			TLSClientConfig: &tls.Config{
				MaxVersion: tls.VersionTLS12,
				RootCAs:    pool,
			},
		},
	}

	resp2, err := klien12.Get(ts.URL)
	if err != nil {
		fmt.Println("permintaan TLS 1.2 gagal:", err)
		return
	}
	io.Copy(io.Discard, resp2.Body)
	resp2.Body.Close()

	fmt.Println("klien dibatasi TLS 1.2, status:", resp2.Status)
	fmt.Println("versi TLS hasil negosiasi:", namaVersiTLS(resp2.TLS.Version))

	// Bagian 4: bentuk ringkas httptest.NewTLSServer.
	tsRingkas := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "versi ringkas")
	}))
	defer tsRingkas.Close()

	resp3, err := tsRingkas.Client().Get(tsRingkas.URL)
	if err != nil {
		fmt.Println("permintaan ke NewTLSServer gagal:", err)
		return
	}
	io.Copy(io.Discard, resp3.Body)
	resp3.Body.Close()
	fmt.Println("NewTLSServer + client bawaan:", resp3.Status, "|", resp3.Proto, "|", namaVersiTLS(resp3.TLS.Version))
}