package tlsconf import ( "crypto/tls" "crypto/x509" "net" "net/http" "net/http/httptest" "path/filepath" "testing" "golang.org/x/net/http2" ) func newCA(t *testing.T) *CA { t.Helper() ca, err := NewCA("llmgw test CA") if err != nil { t.Fatal(err) } return ca } // writeCluster issues a server and client certificate and returns the configs // that would load them, exactly as the real components do. func writeCluster(t *testing.T, ca *CA, serverHosts []string, nodeID string) (server, client Config) { t.Helper() dir := t.TempDir() if err := ca.Write(dir); err != nil { t.Fatal(err) } srv, err := ca.IssueServer("llmgw-ctl", serverHosts) if err != nil { t.Fatal(err) } if err := srv.Write(dir, "server"); err != nil { t.Fatal(err) } cli, err := ca.IssueClient(nodeID) if err != nil { t.Fatal(err) } if err := cli.Write(dir, "client"); err != nil { t.Fatal(err) } caPath := filepath.Join(dir, "ca.pem") return Config{CA: caPath, Cert: filepath.Join(dir, "server.pem"), Key: filepath.Join(dir, "server-key.pem")}, Config{CA: caPath, Cert: filepath.Join(dir, "client.pem"), Key: filepath.Join(dir, "client-key.pem")} } func TestMutualAuthenticationSucceeds(t *testing.T) { ca := newCA(t) serverCfg, clientCfg := writeCluster(t, ca, []string{"127.0.0.1"}, "llmgw-node01") stls, err := serverCfg.ServerConfig() if err != nil { t.Fatal(err) } srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // The identity the handler sees is what the control plane keys on. if len(r.TLS.PeerCertificates) == 0 { t.Error("no client certificate reached the handler") return } _, _ = w.Write([]byte(r.TLS.PeerCertificates[0].Subject.CommonName)) })) srv.TLS = stls srv.StartTLS() defer srv.Close() ctls, err := clientCfg.ClientConfig("127.0.0.1") if err != nil { t.Fatal(err) } c := &http.Client{Transport: &http2.Transport{TLSClientConfig: ctls}} resp, err := c.Get(srv.URL) //nolint:noctx // test if err != nil { t.Fatalf("mutual authentication failed: %v", err) } defer resp.Body.Close() buf := make([]byte, 64) n, _ := resp.Body.Read(buf) if got := string(buf[:n]); got != "llmgw-node01" { t.Errorf("the handler saw common name %q", got) } } func TestServerRejectsClientWithoutCertificate(t *testing.T) { // An agent that cannot prove who it is must not reach the control plane at // all; accepting it unauthenticated would leave identity as forgeable as it // was before TLS. ca := newCA(t) serverCfg, _ := writeCluster(t, ca, []string{"127.0.0.1"}, "llmgw-node01") stls, _ := serverCfg.ServerConfig() srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) })) srv.TLS = stls srv.StartTLS() defer srv.Close() pool := x509.NewCertPool() pool.AppendCertsFromPEM(ca.PEM) c := &http.Client{Transport: &http2.Transport{TLSClientConfig: &tls.Config{ RootCAs: pool, ServerName: "127.0.0.1", MinVersion: tls.VersionTLS13, NextProtos: []string{"h2"}, }}} if _, err := c.Get(srv.URL); err == nil { //nolint:noctx // test t.Fatal("a client with no certificate was accepted") } } func TestServerRejectsCertificateFromAnotherCA(t *testing.T) { ca := newCA(t) serverCfg, _ := writeCluster(t, ca, []string{"127.0.0.1"}, "llmgw-node01") // An attacker's own authority, with a certificate naming a real node. other := newCA(t) _, foreign := writeCluster(t, other, []string{"127.0.0.1"}, "llmgw-node01") stls, _ := serverCfg.ServerConfig() srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) })) srv.TLS = stls srv.StartTLS() defer srv.Close() ctls, _ := foreign.ClientConfig("127.0.0.1") // It still needs to trust the server, or the failure would prove nothing. pool := x509.NewCertPool() pool.AppendCertsFromPEM(ca.PEM) ctls.RootCAs = pool c := &http.Client{Transport: &http2.Transport{TLSClientConfig: ctls}} if _, err := c.Get(srv.URL); err == nil { //nolint:noctx // test t.Fatal("a certificate from an unrelated authority was accepted") } } func TestClientRejectsServerWithoutMatchingSAN(t *testing.T) { // The trap worth having a test for: hostname verification reads subject // alternative names and ignores the common name entirely, so a certificate // issued only for a DNS name fails when agents dial an IP. ca := newCA(t) serverCfg, clientCfg := writeCluster(t, ca, []string{"llmgw-ctl.example"}, "llmgw-node01") stls, _ := serverCfg.ServerConfig() srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) })) srv.TLS = stls srv.StartTLS() defer srv.Close() ctls, _ := clientCfg.ClientConfig("127.0.0.1") c := &http.Client{Transport: &http2.Transport{TLSClientConfig: ctls}} _, err := c.Get(srv.URL) //nolint:noctx // test if err == nil { t.Fatal("a certificate with no matching SAN was accepted") } } func TestIssuedCertificatesCarryTheRightUsage(t *testing.T) { // The reason a publicly issued certificate cannot be used here: authorities // stopped including clientAuth, and Go verifies client chains against it. ca := newCA(t) srv, err := ca.IssueServer("llmgw-ctl", []string{"10.0.0.140"}) if err != nil { t.Fatal(err) } cli, err := ca.IssueClient("llmgw-node01") if err != nil { t.Fatal(err) } parse := func(l Leaf) *x509.Certificate { t.Helper() c, err := tls.X509KeyPair(l.CertPEM, l.KeyPEM) if err != nil { t.Fatal(err) } leaf, err := x509.ParseCertificate(c.Certificate[0]) if err != nil { t.Fatal(err) } return leaf } s := parse(srv) if len(s.ExtKeyUsage) != 1 || s.ExtKeyUsage[0] != x509.ExtKeyUsageServerAuth { t.Errorf("server EKU=%v", s.ExtKeyUsage) } if len(s.IPAddresses) != 1 || !s.IPAddresses[0].Equal(net.ParseIP("10.0.0.140")) { t.Errorf("the IP SAN is missing: %v", s.IPAddresses) } c := parse(cli) if len(c.ExtKeyUsage) != 1 || c.ExtKeyUsage[0] != x509.ExtKeyUsageClientAuth { t.Errorf("client EKU=%v", c.ExtKeyUsage) } if c.Subject.CommonName != "llmgw-node01" { t.Errorf("the node id must be the common name, got %q", c.Subject.CommonName) } } func TestServerCertificateRequiresHosts(t *testing.T) { ca := newCA(t) if _, err := ca.IssueServer("llmgw-ctl", nil); err == nil { t.Error("a server certificate with no SAN was issued; every client would reject it") } } func TestCARoundTripsThroughDisk(t *testing.T) { ca := newCA(t) dir := t.TempDir() if err := ca.Write(dir); err != nil { t.Fatal(err) } loaded, err := LoadCA(filepath.Join(dir, "ca.pem"), filepath.Join(dir, "ca-key.pem")) if err != nil { t.Fatal(err) } // A certificate issued by the reloaded authority must verify against the // original, or issuing from a second machine would silently produce // certificates nothing accepts. leaf, err := loaded.IssueClient("llmgw-node02") if err != nil { t.Fatal(err) } pool := x509.NewCertPool() pool.AppendCertsFromPEM(ca.PEM) kp, _ := tls.X509KeyPair(leaf.CertPEM, leaf.KeyPEM) parsed, _ := x509.ParseCertificate(kp.Certificate[0]) if _, err := parsed.Verify(x509.VerifyOptions{ Roots: pool, KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, }); err != nil { t.Errorf("a certificate from the reloaded CA does not verify: %v", err) } } func TestConfigValidation(t *testing.T) { if err := (Config{CA: "x", Cert: "y"}).Validate(); err == nil { t.Error("a certificate without its key was accepted") } if err := (Config{Cert: "y", Key: "z"}).Validate(); err == nil { t.Error("a certificate without a CA was accepted") } if err := (Config{Insecure: true, CA: "x"}).Validate(); err == nil { t.Error("insecure alongside certificate paths was accepted") } if err := (Config{Insecure: true}).Validate(); err != nil { t.Errorf("plain insecure was rejected: %v", err) } if err := (Config{}).Validate(); err != nil { t.Errorf("an empty config was rejected: %v", err) } }