package client_test import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/tls" "crypto/x509" "crypto/x509/pkix" "encoding/hex" "encoding/pem" "math/big" "net" "sync" "testing" "time" "github.com/enmanuel/unibus/pkg/client" "github.com/enmanuel/unibus/pkg/frame" "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") } } // TestSecureBusEndToEnd is the headline golden of issue 0001: with ALL three // layers active at once — control-plane request signing (enforce), NATS nkey // auth, and TLS — two registered peers run an encrypted room end to end. A // creates a Matrix-policy room, invites B, A publishes and B decrypts. This // proves the layers compose: signed HTTP control plane + authenticated, // encrypted data plane + E2E room content. func TestSecureBusEndToEnd(t *testing.T) { serverTLS, caPool := genTestCA(t) h := bootHarness(t, membership.AuthEnforce, true, serverTLS) waitHealth(t, h.ctrlURL) clientTLS := &tls.Config{RootCAs: caPool, MinVersion: tls.VersionTLS12} secure := func(t *testing.T, handle string) (*client.Client, membership.AuthMode) { id := mustIdentity(t) if err := h.store.AddUser(hex.EncodeToString(id.SignPub), handle, membership.RoleMember); err != nil { t.Fatalf("register %s: %v", handle, err) } c, err := client.NewWithOptions(h.natsURL, h.ctrlURL, id, client.Options{UseNkey: true, TLS: clientTLS}) if err != nil { t.Fatalf("connect %s securely: %v", handle, err) } return c, 0 } a, _ := secure(t, "alice") defer a.Close() b, _ := secure(t, "bob") defer b.Close() roomID, err := a.CreateRoom("room.secure", room.ModeMatrix) if err != nil { t.Fatalf("A create encrypted room over secure bus: %v", err) } if err := a.Invite(roomID, b.Endpoint()); err != nil { t.Fatalf("A invite B: %v", err) } if err := b.Join(roomID); err != nil { t.Fatalf("B join: %v", err) } var mu sync.Mutex var got []string sub, err := b.Subscribe(roomID, func(_ frame.Frame, plaintext []byte) { mu.Lock() got = append(got, string(plaintext)) mu.Unlock() }) if err != nil { t.Fatalf("B subscribe: %v", err) } defer sub.Unsubscribe() time.Sleep(150 * time.Millisecond) const msg = "mensaje sobre bus seguro (auth+TLS+E2E)" if err := a.Publish(roomID, []byte(msg)); err != nil { t.Fatalf("A publish: %v", err) } if !waitFor(&mu, &got, func(rs []string) bool { for _, r := range rs { if r == msg { return true } } return false }, 2*time.Second) { t.Fatalf("B did not receive/decrypt the message over the secured bus; got %v", snapshot(&mu, &got)) } }