package botlink import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/tls" "crypto/x509" "crypto/x509/pkix" "encoding/pem" "math/big" "net" "os" "path/filepath" "testing" "time" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "scrabble/pkg/mtls" botlinkv1 "scrabble/pkg/proto/botlink/v1" ) // mintCerts writes a CA, a server leaf (IP SAN 127.0.0.1) and a client leaf into a // temp dir, returning their file paths. It exercises the real pkg/mtls loaders. func mintCerts(t *testing.T) (caFile, srvCert, srvKey, cliCert, cliKey string) { t.Helper() dir := t.TempDir() caKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) caTmpl := &x509.Certificate{ SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: "test-ca"}, NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().Add(time.Hour), IsCA: true, KeyUsage: x509.KeyUsageCertSign, BasicConstraintsValid: true, } caDER, err := x509.CreateCertificate(rand.Reader, caTmpl, caTmpl, &caKey.PublicKey, caKey) if err != nil { t.Fatalf("ca: %v", err) } caCert, _ := x509.ParseCertificate(caDER) leaf := func(cn string, eku x509.ExtKeyUsage, ips []net.IP) (certPath, keyPath string) { key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) tmpl := &x509.Certificate{ SerialNumber: big.NewInt(time.Now().UnixNano()), Subject: pkix.Name{CommonName: cn}, NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().Add(time.Hour), KeyUsage: x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{eku}, IPAddresses: ips, } der, err := x509.CreateCertificate(rand.Reader, tmpl, caCert, &key.PublicKey, caKey) if err != nil { t.Fatalf("leaf %s: %v", cn, err) } certPath = filepath.Join(dir, cn+".crt") keyPath = filepath.Join(dir, cn+".key") writePEM(t, certPath, "CERTIFICATE", der) keyDER, _ := x509.MarshalPKCS8PrivateKey(key) writePEM(t, keyPath, "PRIVATE KEY", keyDER) return certPath, keyPath } caFile = filepath.Join(dir, "ca.crt") writePEM(t, caFile, "CERTIFICATE", caDER) srvCert, srvKey = leaf("server", x509.ExtKeyUsageServerAuth, []net.IP{net.ParseIP("127.0.0.1")}) cliCert, cliKey = leaf("client", x509.ExtKeyUsageClientAuth, nil) return caFile, srvCert, srvKey, cliCert, cliKey } func writePEM(t *testing.T, path, typ string, der []byte) { t.Helper() if err := os.WriteFile(path, pem.EncodeToMemory(&pem.Block{Type: typ, Bytes: der}), 0o600); err != nil { t.Fatalf("write %s: %v", path, err) } } // startMTLSHub starts the Hub behind a real mTLS gRPC listener and returns its // address and the CA/client cert paths. func startMTLSHub(t *testing.T) (hub *Hub, addr, caFile, cliCert, cliKey string) { t.Helper() caFile, srvCert, srvKey, cliCert, cliKey := mintCerts(t) tlsCfg, err := mtls.ServerConfig(srvCert, srvKey, caFile) if err != nil { t.Fatalf("server config: %v", err) } lis, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("listen: %v", err) } hub = NewHub(nil, nil, nil) srv := grpc.NewServer(grpc.Creds(credentials.NewTLS(tlsCfg))) botlinkv1.RegisterBotLinkServer(srv, hub) go func() { _ = srv.Serve(lis) }() t.Cleanup(srv.Stop) return hub, lis.Addr().String(), caFile, cliCert, cliKey } // TestMTLSValidClientDelivers verifies a CA-signed bot connects and receives a // pushed command. func TestMTLSValidClientDelivers(t *testing.T) { hub, addr, caFile, cliCert, cliKey := startMTLSHub(t) tlsCfg, err := mtls.ClientConfig(cliCert, cliKey, caFile, "127.0.0.1") if err != nil { t.Fatalf("client config: %v", err) } conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(credentials.NewTLS(tlsCfg))) if err != nil { t.Fatalf("dial: %v", err) } t.Cleanup(func() { _ = conn.Close() }) ctx := t.Context() bot := &fakeBot{delivered: true, ack: true} bot.connect(t, ctx, hub, botlinkv1.NewBotLinkClient(conn)) delivered, err := hub.SendAwait(ctx, SendToUserCommand("42", "hi")) if err != nil || !delivered { t.Fatalf("SendAwait = (%v, %v), want (true, nil)", delivered, err) } } // TestMTLSRejectsClientWithoutCert verifies the gateway refuses a client that // presents no CA-signed certificate — mTLS is the sole guard on the bot-link. func TestMTLSRejectsClientWithoutCert(t *testing.T) { hub, addr, caFile, _, _ := startMTLSHub(t) caPEM, _ := os.ReadFile(caFile) pool := x509.NewCertPool() pool.AppendCertsFromPEM(caPEM) // Trusts the server, but presents no client certificate. noCert := credentials.NewTLS(&tls.Config{RootCAs: pool, ServerName: "127.0.0.1", MinVersion: tls.VersionTLS13}) conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(noCert)) if err != nil { t.Fatalf("dial: %v", err) } t.Cleanup(func() { _ = conn.Close() }) stream, err := botlinkv1.NewBotLinkClient(conn).Link(t.Context()) if err == nil { err = stream.Send(&botlinkv1.FromBot{Msg: &botlinkv1.FromBot_Hello{Hello: &botlinkv1.Hello{InstanceId: "rogue"}}}) if err == nil { _, err = stream.Recv() } } if err == nil { t.Fatal("expected the handshake to be rejected without a client certificate") } hub.mu.Lock() n := len(hub.links) hub.mu.Unlock() if n != 0 { t.Errorf("connected bots = %d, want 0 (rogue must not register)", n) } }