| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154 |
- package web
- import (
- "crypto/tls"
- "crypto/x509"
- "net/http"
- "net/http/httptest"
- "strconv"
- "testing"
- "github.com/mhsanaei/3x-ui/v3/internal/util/crypto"
- )
- // TestPanelTLSAcceptsClientWithoutClientCert characterizes the invariant the
- // mTLS work must preserve: the panel's HTTPS listener — configured today with a
- // server certificate and NO ClientAuth — completes the TLS handshake for a
- // client that presents no client certificate (i.e. every browser). When mTLS is
- // wired into web.go, the no-CA path must keep this behavior byte-for-byte. P1.6
- // extends this file with the VerifyClientCertIfGiven + ClientCAs cases.
- func TestPanelTLSAcceptsClientWithoutClientCert(t *testing.T) {
- srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
- w.WriteHeader(http.StatusOK)
- }))
- defer srv.Close()
- // Precondition: like web.go today, the listener requests no client cert.
- if srv.TLS.ClientAuth != tls.NoClientCert {
- t.Fatalf("precondition: ClientAuth = %v, want NoClientCert", srv.TLS.ClientAuth)
- }
- // srv.Client() trusts the server's self-signed cert and presents NO client cert.
- resp, err := srv.Client().Get(srv.URL)
- if err != nil {
- t.Fatalf("request without a client certificate failed: %v", err)
- }
- defer resp.Body.Close()
- if resp.StatusCode != http.StatusOK {
- t.Fatalf("status = %d, want 200", resp.StatusCode)
- }
- }
- // TestApplyNodeMtls exercises the listener policy applied by web.go: a nil pool
- // leaves the listener unchanged (no client auth, browsers work); a set pool is
- // request-but-don't-require, so no-cert clients still handshake while a
- // CA-signed client cert is verified and a foreign cert is rejected.
- func TestApplyNodeMtls(t *testing.T) {
- ca, err := crypto.GenerateNodeCA("test ca")
- if err != nil {
- t.Fatalf("GenerateNodeCA: %v", err)
- }
- clientPEM, err := crypto.IssueClientCert(ca, "master")
- if err != nil {
- t.Fatalf("IssueClientCert: %v", err)
- }
- clientCert, err := tls.X509KeyPair(clientPEM.CertPEM, clientPEM.KeyPEM)
- if err != nil {
- t.Fatalf("client X509KeyPair: %v", err)
- }
- caPool := x509.NewCertPool()
- if !caPool.AppendCertsFromPEM(ca.CertPEM) {
- t.Fatal("append CA to pool")
- }
- otherCA, err := crypto.GenerateNodeCA("other ca")
- if err != nil {
- t.Fatalf("GenerateNodeCA(other): %v", err)
- }
- foreignPEM, err := crypto.IssueClientCert(otherCA, "intruder")
- if err != nil {
- t.Fatalf("IssueClientCert(foreign): %v", err)
- }
- foreignCert, err := tls.X509KeyPair(foreignPEM.CertPEM, foreignPEM.KeyPEM)
- if err != nil {
- t.Fatalf("foreign X509KeyPair: %v", err)
- }
- newServer := func(pool *x509.CertPool) *httptest.Server {
- srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- n := 0
- if r.TLS != nil {
- n = len(r.TLS.VerifiedChains)
- }
- w.Header().Set("X-Verified-Chains", strconv.Itoa(n))
- w.WriteHeader(http.StatusOK)
- }))
- srv.TLS = &tls.Config{}
- applyNodeMtls(srv.TLS, pool)
- srv.StartTLS()
- return srv
- }
- // clientFor forces the client to present cert via GetClientCertificate so the
- // server's verification is what's under test (the default Certificates path
- // would let the Go client silently withhold a cert whose CA the server didn't
- // advertise, masking the reject behavior).
- clientFor := func(srv *httptest.Server, cert *tls.Certificate) *http.Client {
- roots := x509.NewCertPool()
- roots.AddCert(srv.Certificate())
- cfg := &tls.Config{RootCAs: roots}
- if cert != nil {
- c := *cert
- cfg.GetClientCertificate = func(*tls.CertificateRequestInfo) (*tls.Certificate, error) {
- return &c, nil
- }
- }
- return &http.Client{Transport: &http.Transport{TLSClientConfig: cfg}}
- }
- t.Run("nil pool leaves the listener without client auth", func(t *testing.T) {
- srv := newServer(nil)
- defer srv.Close()
- if srv.TLS.ClientAuth != tls.NoClientCert {
- t.Fatalf("nil pool must not set ClientAuth, got %v", srv.TLS.ClientAuth)
- }
- resp, err := clientFor(srv, nil).Get(srv.URL)
- if err != nil {
- t.Fatalf("no-cert client failed: %v", err)
- }
- resp.Body.Close()
- })
- t.Run("pool set still accepts a no-cert client", func(t *testing.T) {
- srv := newServer(caPool)
- defer srv.Close()
- resp, err := clientFor(srv, nil).Get(srv.URL)
- if err != nil {
- t.Fatalf("no-cert client must still handshake under VerifyClientCertIfGiven: %v", err)
- }
- defer resp.Body.Close()
- if got := resp.Header.Get("X-Verified-Chains"); got != "0" {
- t.Fatalf("no-cert client verified chains = %s, want 0", got)
- }
- })
- t.Run("pool set verifies the master client cert", func(t *testing.T) {
- srv := newServer(caPool)
- defer srv.Close()
- resp, err := clientFor(srv, &clientCert).Get(srv.URL)
- if err != nil {
- t.Fatalf("master client cert must be accepted: %v", err)
- }
- defer resp.Body.Close()
- if got := resp.Header.Get("X-Verified-Chains"); got != "1" {
- t.Fatalf("master cert verified chains = %s, want 1 (cert was not verified)", got)
- }
- })
- t.Run("pool set rejects a foreign-CA client cert", func(t *testing.T) {
- srv := newServer(caPool)
- defer srv.Close()
- if _, err := clientFor(srv, &foreignCert).Get(srv.URL); err == nil {
- t.Fatal("a client cert from an untrusted CA must fail the handshake")
- }
- })
- }
|