Files
unibus/pkg/client/tls_test.go
T
egutierrez 75939a192c test: TLS data plane end to end + CA/keypair loaders
client/tls_test: mints a throwaway CA + server cert in-memory; a client
pinning the CA completes the handshake and operates (golden), a client
without the CA fails the handshake (error path). busauth/tls_test: golden
load of a CA PEM and a server keypair, plus error paths (missing file,
non-PEM). Harness body extracted to bootHarness(ctrlMode, natsAuth, natsTLS).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 12:44:13 +02:00

114 lines
4.0 KiB
Go

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")
}
}