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