github.com/choria-io/go-choria@v0.28.1-0.20240416190746-b3bf9c7d5a45/integration/suites/protov2/v2_test.go (about)

     1  // Copyright (c) 2022-2023, R.I. Pienaar and the Choria Project contributors
     2  //
     3  // SPDX-License-Identifier: Apache-2.0
     4  
     5  package protov2
     6  
     7  import (
     8  	"context"
     9  	"crypto/ed25519"
    10  	"encoding/hex"
    11  	"encoding/json"
    12  	"fmt"
    13  	"os"
    14  	"path/filepath"
    15  	"sync"
    16  	"testing"
    17  	"time"
    18  
    19  	"github.com/choria-io/go-choria/choria"
    20  	"github.com/choria-io/go-choria/client/rpcutilclient"
    21  	"github.com/choria-io/go-choria/config"
    22  	"github.com/choria-io/go-choria/integration/testbroker"
    23  	"github.com/choria-io/go-choria/integration/testutil"
    24  	"github.com/choria-io/go-choria/inter"
    25  	iu "github.com/choria-io/go-choria/internal/util"
    26  	v2 "github.com/choria-io/go-choria/protocol/v2"
    27  	"github.com/choria-io/go-choria/server"
    28  	"github.com/choria-io/tokens"
    29  	"github.com/golang-jwt/jwt/v4"
    30  	. "github.com/onsi/ginkgo/v2"
    31  	. "github.com/onsi/gomega"
    32  	"github.com/onsi/gomega/gbytes"
    33  	"github.com/sirupsen/logrus"
    34  )
    35  
    36  type remoteSignerFunc func(context.Context, []byte, inter.RequestSignerConfig) ([]byte, error)
    37  
    38  func TestV2Protocol(t *testing.T) {
    39  	RegisterFailHandler(Fail)
    40  	RunSpecs(t, "Integration/Protocol V2")
    41  }
    42  
    43  var _ = Describe("Protocol V2", func() {
    44  	var (
    45  		ctx            context.Context
    46  		cancel         context.CancelFunc
    47  		wg             sync.WaitGroup
    48  		logger         *logrus.Logger
    49  		brokerLogBuff  *gbytes.Buffer
    50  		issuerPubK     ed25519.PublicKey
    51  		issuerPubKFile string
    52  		rootDir        string
    53  		err            error
    54  	)
    55  
    56  	BeforeEach(func() {
    57  		rootDir, err = os.MkdirTemp("", "")
    58  		Expect(err).ToNot(HaveOccurred())
    59  
    60  		issuerPubKFile = filepath.Join(rootDir, "issuer")
    61  		issuerPubK, _, err = iu.Ed25519KeyPairToFile(issuerPubKFile)
    62  		Expect(err).ToNot(HaveOccurred())
    63  
    64  		brokerLogBuff, logger = testutil.GbytesLogger(logrus.DebugLevel)
    65  		ctx, cancel = context.WithTimeout(context.Background(), 45*time.Second)
    66  		DeferCleanup(func() {
    67  			cancel()
    68  			Eventually(brokerLogBuff, 5).Should(gbytes.Say("Choria Network Broker shut down"))
    69  			os.RemoveAll(rootDir)
    70  		})
    71  
    72  		tokenFile, _, _, priFile, err := testutil.CreateChoriaTokenAndKeys(rootDir, issuerPubKFile, nil, func(pk ed25519.PublicKey) (jwt.Claims, error) {
    73  			return tokens.NewServerClaims("localhost", []string{"choria"}, "choria", nil, nil, pk, "", time.Hour)
    74  		})
    75  		Expect(err).ToNot(HaveOccurred())
    76  
    77  		cfg, err := iu.ExecuteTemplateFile("testdata/broker.conf", map[string]any{
    78  			"seed":   priFile,
    79  			"token":  tokenFile,
    80  			"issuer": hex.EncodeToString(issuerPubK),
    81  		}, nil)
    82  		Expect(err).ToNot(HaveOccurred())
    83  		Expect(os.WriteFile(filepath.Join(rootDir, "broker.conf"), cfg, 0644)).To(Succeed())
    84  
    85  		_, err = testbroker.StartNetworkBrokerWithConfigFile(ctx, &wg, filepath.Join(rootDir, "broker.conf"), logger)
    86  		Expect(err).ToNot(HaveOccurred())
    87  		Eventually(brokerLogBuff, 1).Should(gbytes.Say("Allowing unverified TLS connections for Organization Issuer issued connections"))
    88  		Eventually(brokerLogBuff, 1).Should(gbytes.Say("Loaded Organization Issuer choria with public key"))
    89  		Eventually(brokerLogBuff, 1).Should(gbytes.Say("Server is ready"))
    90  	})
    91  
    92  	// creates a temporary directory and in it seed, public, jwt file and a config file from template
    93  	createTemp := func(name string, template string, issuerSeedFile string, claimsf func(key ed25519.PublicKey) (jwt.Claims, error)) (td string, cfile string, tfile string, sfile string) {
    94  		var err error
    95  		issPubK := issuerPubK
    96  
    97  		if issuerSeedFile != "" {
    98  			issPubK, _, err = iu.Ed25519KeyPairFromSeedFile(issuerSeedFile)
    99  			Expect(err).ToNot(HaveOccurred())
   100  		}
   101  
   102  		td, err = os.MkdirTemp(rootDir, "")
   103  		Expect(err).ToNot(HaveOccurred())
   104  
   105  		tfn, _, _, sfn, err := testutil.CreateChoriaTokenAndKeys(td, issuerSeedFile, nil, claimsf)
   106  		Expect(err).ToNot(HaveOccurred())
   107  
   108  		cfg, err := iu.ExecuteTemplateFile(template, map[string]any{
   109  			"name":   name,
   110  			"token":  tfn,
   111  			"seed":   sfn,
   112  			"issuer": hex.EncodeToString(issPubK),
   113  		}, nil)
   114  		Expect(err).ToNot(HaveOccurred())
   115  
   116  		tf, err := os.CreateTemp(td, "choria.conf")
   117  		Expect(err).ToNot(HaveOccurred())
   118  		tf.Write(cfg)
   119  		tf.Close()
   120  
   121  		return td, tf.Name(), tfn, sfn
   122  	}
   123  
   124  	startServerInstance := func(cfgFile string, i int) (*gbytes.Buffer, *server.Instance) {
   125  		logbuff, logger := testutil.GbytesLogger(logrus.DebugLevel)
   126  
   127  		name := fmt.Sprintf("srv-%d.example.net", i)
   128  		_, cfile, _, _ := createTemp(name, cfgFile, filepath.Join(rootDir, "issuer"), func(pk ed25519.PublicKey) (jwt.Claims, error) {
   129  			return tokens.NewServerClaims(name, []string{"choria"}, "choria", nil, nil, pk, "ginkgo", time.Minute)
   130  		})
   131  
   132  		srv, err := testutil.StartServerInstance(ctx, &wg, cfile, logger, testutil.ServerWithRPCUtilAgent(), testutil.ServerWithDiscovery())
   133  		Expect(err).ToNot(HaveOccurred())
   134  
   135  		Eventually(logbuff).Should(gbytes.Say("Setting JWT authentication with NONCE signatures for NATS connection"))
   136  		Eventually(logbuff).Should(gbytes.Say("Using TLS Configuration from ed25519\\+jwt based security system"))
   137  		Eventually(logbuff).Should(gbytes.Say("Signing nonce using seed file"))
   138  		Eventually(logbuff).Should(gbytes.Say("Connected to nats://localhost:4222"))
   139  		Eventually(logbuff).Should(gbytes.Say("Registering new agent rpcutil of type rpcutil"))
   140  
   141  		return logbuff, srv
   142  	}
   143  
   144  	createRpcUtilClient := func(perms *tokens.ClientPermissions, signer string, remoteSigner remoteSignerFunc) (*gbytes.Buffer, *rpcutilclient.RpcutilClient, *choria.Framework, *config.Config) {
   145  		logBuff, logger := testutil.GbytesLogger(logrus.DebugLevel)
   146  
   147  		if signer == "" {
   148  			signer = filepath.Join(rootDir, "issuer")
   149  		}
   150  
   151  		_, cfile, _, _ := createTemp("localhost", "testdata/client.conf", signer, func(pk ed25519.PublicKey) (jwt.Claims, error) {
   152  			return tokens.NewClientIDClaims("choria=ginkgo", nil, "choria", nil, "", "ginkgo", time.Minute, perms, pk)
   153  		})
   154  
   155  		cfg, err := config.NewConfig(cfile)
   156  		Expect(err).ToNot(HaveOccurred())
   157  
   158  		opts := []choria.Option{}
   159  		if remoteSigner != nil {
   160  			opts = append(opts, choria.WithCustomRequestSigner(testutil.NewFuncSigner(remoteSigner)))
   161  		}
   162  		cfg.CustomLogger = logger
   163  
   164  		fw, err := choria.NewWithConfig(cfg, opts...)
   165  		Expect(err).ToNot(HaveOccurred())
   166  
   167  		client, err := rpcutilclient.New(fw)
   168  		Expect(err).ToNot(HaveOccurred())
   169  
   170  		return logBuff, client, fw, cfg
   171  	}
   172  
   173  	aaaSignGen := func(forceInvalid bool) remoteSignerFunc {
   174  		return func(ctx context.Context, req []byte, scfg inter.RequestSignerConfig) ([]byte, error) {
   175  			v2Req, err := v2.NewRequest("", "", "", 0, "", "choria")
   176  			if err != nil {
   177  				return nil, err
   178  			}
   179  
   180  			err = json.Unmarshal(req, v2Req)
   181  			if err != nil {
   182  				return nil, err
   183  			}
   184  
   185  			signerPub, _, err := iu.Ed25519KeyPairToFile(filepath.Join(rootDir, "fn_signer.seed"))
   186  			Expect(err).ToNot(HaveOccurred())
   187  
   188  			signer := filepath.Join(rootDir, "issuer")
   189  			// we support generating failing signatures on purpose
   190  			if forceInvalid {
   191  				signer = filepath.Join(rootDir, "fn_signer.seed")
   192  			}
   193  
   194  			// create a new directory with our aaa signer tokens, seed etc with the delegator permission
   195  			_, cfile, tfile, _ := createTemp("localhost", "testdata/client.conf", signer, func(pk ed25519.PublicKey) (jwt.Claims, error) {
   196  				return tokens.NewClientIDClaims("fn_signer", nil, "choria", nil, "", "", time.Hour, &tokens.ClientPermissions{AuthenticationDelegator: true}, signerPub)
   197  			})
   198  
   199  			// we now create a config for that delegated signer and make sure we use the right seed since createTemp() will have made one too
   200  			cfg, err := config.NewConfig(cfile)
   201  			Expect(err).ToNot(HaveOccurred())
   202  			cfg.CustomLogger = logger
   203  			cfg.Choria.ChoriaSecuritySeedFile = filepath.Join(rootDir, "fn_signer.seed")
   204  
   205  			// signer needs its own security instances
   206  			fw, err := choria.NewWithConfig(cfg)
   207  			Expect(err).ToNot(HaveOccurred())
   208  
   209  			// this is what aaa service does
   210  			v2Req.SetCallerID("delegated_client")
   211  			v2SReq, err := fw.NewSecureRequest(context.Background(), v2Req)
   212  			if err != nil {
   213  				return nil, err
   214  			}
   215  
   216  			token, err := os.ReadFile(tfile)
   217  			Expect(err).ToNot(HaveOccurred())
   218  
   219  			v2SReq.SetSigner(token)
   220  
   221  			return v2SReq.JSON()
   222  		}
   223  	}
   224  
   225  	Describe("Basic Operation", func() {
   226  		It("Should default to the choria collective when no collections are given", func() {
   227  			startServerInstance("testdata/server.conf", 1)
   228  			_, _, _, cfg := createRpcUtilClient(nil, "", nil)
   229  
   230  			// ensures server.conf does not in fact have these settings set
   231  			Expect(cfg.HasOption("collectives")).To(BeFalse())
   232  			Expect(cfg.HasOption("main_collective")).To(BeFalse())
   233  
   234  			Expect(cfg.Collectives).To(Equal([]string{"choria"}))
   235  		})
   236  
   237  		It("Should fail clients with unknown issuers", func() {
   238  			_, _, err := iu.Ed25519KeyPairToFile(filepath.Join(rootDir, "rogue_issuer"))
   239  			Expect(err).ToNot(HaveOccurred())
   240  
   241  			serverLogbuff, _ := startServerInstance("testdata/server.conf", 1)
   242  			clientLogbuff, client, _, _ := createRpcUtilClient(&tokens.ClientPermissions{FleetManagement: true}, filepath.Join(rootDir, "rogue_issuer"), nil)
   243  
   244  			client.OptionTargets([]string{"srv-1.example.net"})
   245  			ctx, cancl := context.WithTimeout(ctx, time.Second)
   246  			defer cancl()
   247  
   248  			_, err = client.Ping().Do(ctx)
   249  			Expect(err).To(HaveOccurred())
   250  			Eventually(clientLogbuff).Should(gbytes.Say("Setting JWT authentication with NONCE signatures for NATS connection"))
   251  			Eventually(clientLogbuff).Should(gbytes.Say("Signing nonce using seed file"))
   252  			Eventually(clientLogbuff).Should(gbytes.Say("Initial connection to the Broker failed on try 1: nats: Authorization Violation"))
   253  			Expect(serverLogbuff).ShouldNot(gbytes.Say("Handling message .+ for rpcutil#ping from choria=ginkgo"))
   254  		})
   255  
   256  		It("Should fail for clients without fleet access", func() {
   257  			serverLogbuff, _ := startServerInstance("testdata/server.conf", 1)
   258  
   259  			// first just no fleet management
   260  			clientLogbuff, client, _, _ := createRpcUtilClient(&tokens.ClientPermissions{FleetManagement: false}, "", nil)
   261  
   262  			client.OptionTargets([]string{"srv-1.example.net"})
   263  			res, err := client.Ping().Do(ctx)
   264  			Expect(err).ToNot(HaveOccurred())
   265  			Expect(res.Stats().OKCount()).To(Equal(0))
   266  
   267  			Eventually(clientLogbuff).Should(gbytes.Say("Setting JWT authentication with NONCE signatures for NATS connection"))
   268  			Eventually(clientLogbuff).Should(gbytes.Say("Signing nonce using seed file"))
   269  
   270  			// without fleet access one cannot communicate with the signer even so this should fail
   271  			Eventually(brokerLogBuff).Should(gbytes.Say(`Publish Violation.+choria.node.srv-1.example.net`))
   272  
   273  			// second we allow it only when signed, and we're not signing here
   274  			clientLogbuff, client, _, _ = createRpcUtilClient(&tokens.ClientPermissions{SignedFleetManagement: true}, "", nil)
   275  
   276  			client.OptionTargets([]string{"srv-1.example.net"})
   277  			res, err = client.Ping().Do(ctx)
   278  			Expect(err).ToNot(HaveOccurred())
   279  			Expect(res.Stats().OKCount()).To(Equal(0))
   280  
   281  			Eventually(clientLogbuff).Should(gbytes.Say("Setting JWT authentication with NONCE signatures for NATS connection"))
   282  			Eventually(clientLogbuff).Should(gbytes.Say("Signing nonce using seed file"))
   283  			Eventually(serverLogbuff).Should(gbytes.Say("access denied: requires authority delegation"))
   284  		})
   285  
   286  		It("Should support signed clients", func() {
   287  			serverLogbuff, _ := startServerInstance("testdata/server.conf", 1)
   288  
   289  			var forceFail bool
   290  
   291  			// we create a client that has a custom remote signer configured, the remote signer does not call any remote AAA server but instead calls a local callback
   292  			// the local callback will do 1 valid request followed by all future ones signed by an invalid issuer.  This should fully allow 1 request as if it was against
   293  			// AAA Server that's correctly configured in the issuer and then just forever fail as being from another issuer
   294  			clientLogbuff, client, _, _ := createRpcUtilClient(&tokens.ClientPermissions{SignedFleetManagement: true}, "", func(ctx context.Context, req []byte, scfg inter.RequestSignerConfig) ([]byte, error) {
   295  				signed, err := aaaSignGen(forceFail)(ctx, req, scfg)
   296  				if err != nil {
   297  					return nil, err
   298  				}
   299  				forceFail = true
   300  
   301  				return signed, nil
   302  			})
   303  
   304  			client.OptionTargets([]string{"srv-1.example.net"})
   305  			res, err := client.Ping().Do(ctx)
   306  			Expect(err).ToNot(HaveOccurred())
   307  			Expect(res.Stats().OKCount()).To(Equal(1))
   308  
   309  			Eventually(clientLogbuff).Should(gbytes.Say("Setting JWT authentication with NONCE signatures for NATS connection"))
   310  			Eventually(clientLogbuff).Should(gbytes.Say("Signing nonce using seed file"))
   311  
   312  			// we need to make sure it was done for delegated_client which is set by the signer
   313  			Eventually(serverLogbuff).Should(gbytes.Say("Allowing delegator fn_signer to authorize caller delegated_client who holds token choria=ginkgo"))
   314  			Eventually(serverLogbuff).Should(gbytes.Say("Handling message .+ for rpcutil#ping from delegated_client"))
   315  
   316  			res, err = client.Ping().Do(ctx)
   317  			Expect(err).ToNot(HaveOccurred())
   318  			Expect(res.Stats().OKCount()).To(Equal(0))
   319  			Eventually(serverLogbuff).Should(gbytes.Say("could not parse client token: could not parse client id token: ed25519: verification error"))
   320  			Eventually(serverLogbuff).Should(gbytes.Say("Could not decode incoming request: secure request messages created from Transport Message did not pass security validation"))
   321  		})
   322  
   323  		It("Should allow servers and clients to communicate without AAA", func() {
   324  			serverLogbuff, _ := startServerInstance("testdata/server.conf", 1)
   325  			clientLogbuff, client, _, _ := createRpcUtilClient(&tokens.ClientPermissions{FleetManagement: true}, "", nil)
   326  
   327  			client.OptionTargets([]string{"srv-1.example.net"})
   328  			res, err := client.Ping().Do(ctx)
   329  			Expect(err).ToNot(HaveOccurred())
   330  			Expect(res.Stats().OKCount()).To(Equal(1))
   331  
   332  			Eventually(clientLogbuff).Should(gbytes.Say("Setting JWT authentication with NONCE signatures for NATS connection"))
   333  			Eventually(clientLogbuff).Should(gbytes.Say("Signing nonce using seed file"))
   334  			Eventually(serverLogbuff).Should(gbytes.Say("Handling message .+ for rpcutil#ping from choria=ginkgo"))
   335  		})
   336  	})
   337  })