package client_test import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/tls" "crypto/x509" "crypto/x509/pkix" "encoding/pem" "math/big" "net" "testing" "time" "github.com/enmanuel/unibus/pkg/client" "github.com/enmanuel/unibus/pkg/membership" "github.com/enmanuel/unibus/pkg/room" ) // genTestCA mints a throwaway self-signed CA plus a server certificate (SAN // 127.0.0.1 / localhost) signed by it, mirroring deploy/tls/generate-certs.sh // without shelling out to openssl. It returns the server's *tls.Config (cert it // presents) and the CA pool a client must trust to complete the handshake. func genTestCA(t *testing.T) (server *tls.Config, caPool *x509.CertPool) { t.Helper() // --- CA --- caKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { t.Fatalf("ca key: %v", err) } caTmpl := &x509.Certificate{ SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: "unibus-test-ca"}, NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().Add(24 * time.Hour), IsCA: true, KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature, BasicConstraintsValid: true, } caDER, err := x509.CreateCertificate(rand.Reader, caTmpl, caTmpl, &caKey.PublicKey, caKey) if err != nil { t.Fatalf("ca cert: %v", err) } caCert, err := x509.ParseCertificate(caDER) if err != nil { t.Fatalf("parse ca: %v", err) } // --- server cert signed by the CA --- srvKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { t.Fatalf("server key: %v", err) } srvTmpl := &x509.Certificate{ SerialNumber: big.NewInt(2), Subject: pkix.Name{CommonName: "unibus-test-server"}, NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().Add(24 * time.Hour), KeyUsage: x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, DNSNames: []string{"localhost"}, IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1)}, } srvDER, err := x509.CreateCertificate(rand.Reader, srvTmpl, caCert, &srvKey.PublicKey, caKey) if err != nil { t.Fatalf("server cert: %v", err) } srvCertPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: srvDER}) srvKeyDER, err := x509.MarshalECPrivateKey(srvKey) if err != nil { t.Fatalf("marshal server key: %v", err) } srvKeyPEM := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: srvKeyDER}) srvPair, err := tls.X509KeyPair(srvCertPEM, srvKeyPEM) if err != nil { t.Fatalf("server keypair: %v", err) } pool := x509.NewCertPool() pool.AddCert(caCert) return &tls.Config{Certificates: []tls.Certificate{srvPair}, MinVersion: tls.VersionTLS12}, pool } // TestNatsTLS validates the TLS data plane: a client trusting the bus CA // completes the handshake and uses the bus (golden); a client that does NOT // trust the CA fails the handshake (error path). func TestNatsTLS(t *testing.T) { serverTLS, caPool := genTestCA(t) h := bootHarness(t, membership.AuthOff, false, serverTLS) waitHealth(t, h.ctrlURL) // Golden: client pinning the CA connects over TLS and operates. clientTLS := &tls.Config{RootCAs: caPool, MinVersion: tls.VersionTLS12} a, err := client.NewWithOptions(h.natsURL, h.ctrlURL, mustIdentity(t), client.Options{TLS: clientTLS}) if err != nil { t.Fatalf("client trusting the CA should complete the TLS handshake: %v", err) } defer a.Close() if _, err := a.CreateRoom("room.tls", room.ModeNATS); err != nil { t.Fatalf("TLS client should operate on the bus: %v", err) } // Error path: a client that does not trust the CA fails the handshake. Use an // empty pool (system roots would also reject this private CA, but an empty // pool makes the intent explicit and avoids depending on the host's roots). badTLS := &tls.Config{RootCAs: x509.NewCertPool(), MinVersion: tls.VersionTLS12} if _, err := client.NewWithOptions(h.natsURL, h.ctrlURL, mustIdentity(t), client.Options{TLS: badTLS}); err == nil { t.Fatalf("client without the CA must fail the TLS handshake") } }