github.com/choria-io/go-choria@v0.28.1-0.20240416190746-b3bf9c7d5a45/protocol/v2/secure_request_test.go (about)

     1  // Copyright (c) 2022, R.I. Pienaar and the Choria Project contributors
     2  //
     3  // SPDX-License-Identifier: Apache-2.0
     4  
     5  package v2
     6  
     7  import (
     8  	"context"
     9  	"fmt"
    10  
    11  	"github.com/choria-io/go-choria/inter"
    12  	imock "github.com/choria-io/go-choria/inter/imocks"
    13  	"github.com/choria-io/go-choria/protocol"
    14  	"github.com/golang/mock/gomock"
    15  	. "github.com/onsi/ginkgo/v2"
    16  	. "github.com/onsi/gomega"
    17  	"github.com/sirupsen/logrus"
    18  	"github.com/tidwall/gjson"
    19  )
    20  
    21  var _ = Describe("SecureRequest", func() {
    22  	var mockctl *gomock.Controller
    23  	var security *imock.MockSecurityProvider
    24  	var tech inter.SecurityTechnology
    25  	var req protocol.Request
    26  	var err error
    27  
    28  	BeforeEach(func() {
    29  		logrus.SetLevel(logrus.FatalLevel)
    30  		mockctl = gomock.NewController(GinkgoT())
    31  		security = imock.NewMockSecurityProvider(mockctl)
    32  
    33  		security.EXPECT().BackingTechnology().DoAndReturn(func() inter.SecurityTechnology {
    34  			return tech
    35  		}).AnyTimes()
    36  
    37  		req, err = NewRequest("ginkgo", "ginkgo.example.net", "up=ginkgo", 60, "1234", "choria")
    38  		Expect(err).ToNot(HaveOccurred())
    39  		req.SetMessage([]byte("hello"))
    40  
    41  		tech = inter.SecurityTechnologyED25519JWT
    42  		protocol.Secure = "true"
    43  	})
    44  
    45  	AfterEach(func() {
    46  		mockctl.Finish()
    47  	})
    48  
    49  	Describe("NewSecureRequest", func() {
    50  		It("Should require the correct security technology", func() {
    51  			tech = inter.SecurityTechnologyX509
    52  			_, err := NewSecureRequest(nil, security)
    53  			Expect(err).To(MatchError(ErrIncorrectProtocol))
    54  		})
    55  
    56  		It("Should support insecure operation", func() {
    57  			protocol.Secure = "false"
    58  			sreq, err := NewSecureRequest(req, security)
    59  			Expect(err).ToNot(HaveOccurred())
    60  			Expect(sreq.(*SecureRequest).CallerJWT).To(Equal(""))
    61  		})
    62  
    63  		It("Should handle token lookup failures", func() {
    64  			security.EXPECT().TokenBytes().Return(nil, fmt.Errorf("ginkgo"))
    65  
    66  			sreq, err := NewSecureRequest(req, security)
    67  			Expect(err).To(MatchError("ginkgo"))
    68  			Expect(sreq).To(BeNil())
    69  		})
    70  
    71  		It("Should handle signing failures", func() {
    72  			security.EXPECT().TokenBytes().Return([]byte("token"), nil)
    73  			security.EXPECT().SignBytes(gomock.AssignableToTypeOf([]byte{})).Return(nil, fmt.Errorf("stub failure")).AnyTimes()
    74  
    75  			sreq, err := NewSecureRequest(req, security)
    76  			Expect(err).To(MatchError("stub failure"))
    77  			Expect(sreq).To(BeNil())
    78  		})
    79  
    80  		It("Should produce a correct secure request", func() {
    81  			security.EXPECT().TokenBytes().Return([]byte("token"), nil)
    82  			security.EXPECT().SignBytes(gomock.AssignableToTypeOf([]byte{})).Return([]byte("stub sig"), nil).AnyTimes()
    83  
    84  			sreq, err := NewSecureRequest(req, security)
    85  			Expect(err).ToNot(HaveOccurred())
    86  
    87  			r := sreq.(*SecureRequest)
    88  			Expect(r.CallerJWT).To(Equal("token"))
    89  			Expect(r.Signature).To(Equal([]byte("stub sig")))
    90  			Expect(r.SignerJWT).To(Equal(""))
    91  			Expect(r.MessageBody).To(ContainSubstring("io.choria.protocol.v2.request"))
    92  		})
    93  	})
    94  
    95  	Describe("NewRemoteSignedSecureRequest", func() {
    96  		It("Should require the correct security technology", func() {
    97  			tech = inter.SecurityTechnologyX509
    98  			_, err := NewRemoteSignedSecureRequest(context.Background(), nil, security)
    99  			Expect(err).To(MatchError(ErrIncorrectProtocol))
   100  		})
   101  
   102  		Describe("Should support insecure or remote signing operation", func() {
   103  			It("Should handle signing failures", func() {
   104  				security.EXPECT().RemoteSignRequest(gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("simulated failure"))
   105  				_, err := NewRemoteSignedSecureRequest(context.Background(), req, security)
   106  				Expect(err).To(MatchError("simulated failure"))
   107  			})
   108  
   109  			It("Should not call remote sign for the signing agent", func() {
   110  				security.EXPECT().RemoteSignRequest(gomock.Any(), gomock.Any()).Times(0)
   111  
   112  				// will call NewSecureRequest() and we have no expect on RemoteSignRequest()
   113  				security.EXPECT().TokenBytes().Return([]byte("token"), nil)
   114  				security.EXPECT().SignBytes(gomock.AssignableToTypeOf([]byte{})).Return([]byte("stub sig"), nil).AnyTimes()
   115  
   116  				req, err = NewRequest(protocol.RemoteSigningAgent, "ginkgo.example.net", "up=ginkgo", 60, "1234", "choria")
   117  				Expect(err).ToNot(HaveOccurred())
   118  				req.SetMessage([]byte("hello"))
   119  
   120  				_, err := NewRemoteSignedSecureRequest(context.Background(), req, security)
   121  				Expect(err).ToNot(HaveOccurred())
   122  			})
   123  		})
   124  
   125  		It("Should check the secure request is signed by a signer", func() {
   126  			security.EXPECT().RemoteSignRequest(gomock.Any(), gomock.AssignableToTypeOf([]byte{})).DoAndReturn(func(_ context.Context, reqj []byte) ([]byte, error) {
   127  				Expect(gjson.GetBytes(reqj, "agent").String()).To(Equal("ginkgo"))
   128  				Expect(gjson.GetBytes(reqj, "protocol").String()).To(Equal(string(protocol.RequestV2)))
   129  
   130  				security.EXPECT().TokenBytes().Return([]byte("token"), nil)
   131  				security.EXPECT().SignBytes(gomock.AssignableToTypeOf([]byte{})).Return([]byte("stub sig"), nil).AnyTimes()
   132  
   133  				signed, err := NewSecureRequest(req, security)
   134  				Expect(err).ToNot(HaveOccurred())
   135  
   136  				signedj, err := signed.JSON()
   137  				Expect(err).ToNot(HaveOccurred())
   138  
   139  				return signedj, nil
   140  			})
   141  
   142  			sreq, err := NewRemoteSignedSecureRequest(context.Background(), req, security)
   143  			Expect(err).To(MatchError("remote signer did not set a signer JWT"))
   144  			Expect(sreq).To(BeNil())
   145  		})
   146  
   147  		It("Should produce a correct secure request", func() {
   148  			security.EXPECT().RemoteSignRequest(gomock.Any(), gomock.AssignableToTypeOf([]byte{})).DoAndReturn(func(_ context.Context, reqj []byte) ([]byte, error) {
   149  				Expect(gjson.GetBytes(reqj, "agent").String()).To(Equal("ginkgo"))
   150  				Expect(gjson.GetBytes(reqj, "protocol").String()).To(Equal(string(protocol.RequestV2)))
   151  
   152  				security.EXPECT().TokenBytes().Return([]byte("token"), nil).Times(2)
   153  				security.EXPECT().SignBytes(gomock.AssignableToTypeOf([]byte{})).Return([]byte("stub sig"), nil).AnyTimes()
   154  
   155  				signed, err := NewSecureRequest(req, security)
   156  				Expect(err).ToNot(HaveOccurred())
   157  
   158  				signed.(*SecureRequest).SignerJWT = "signer jwt"
   159  				signedj, err := signed.JSON()
   160  				Expect(err).ToNot(HaveOccurred())
   161  
   162  				return signedj, nil
   163  			})
   164  
   165  			sreq, err := NewRemoteSignedSecureRequest(context.Background(), req, security)
   166  			Expect(err).ToNot(HaveOccurred())
   167  			Expect(sreq.(*SecureRequest).SignerJWT).To(Equal("signer jwt"))
   168  		})
   169  	})
   170  
   171  	Describe("NewSecureRequestFromTransport", func() {
   172  		It("Should require the correct security technology", func() {
   173  			tech = inter.SecurityTechnologyX509
   174  			_, err := NewSecureRequestFromTransport(nil, security, false)
   175  			Expect(err).To(MatchError(ErrIncorrectProtocol))
   176  		})
   177  
   178  		It("Should detect invalid payloads", func() {
   179  			sr, err := NewSecureRequestFromTransport(&TransportMessage{Data: []byte("{}")}, security, false)
   180  			Expect(err).To(MatchError(ErrInvalidJSON))
   181  			Expect(sr).To(BeNil())
   182  		})
   183  
   184  		It("Should support skipping validation", func() {
   185  			security.EXPECT().TokenBytes().Return([]byte("token"), nil)
   186  			security.EXPECT().SignBytes(gomock.AssignableToTypeOf([]byte{})).Return([]byte("stub sig"), nil).AnyTimes()
   187  			security.EXPECT().VerifySignatureBytes(gomock.Any(), gomock.Any(), gomock.Any()).Return(false, "").Times(0)
   188  
   189  			tsreq, err := NewSecureRequest(req, security)
   190  			Expect(err).ToNot(HaveOccurred())
   191  			t, err := NewTransportMessage("ginkgo.example.net")
   192  			Expect(err).ToNot(HaveOccurred())
   193  			Expect(t.SetRequestData(tsreq)).To(Succeed())
   194  
   195  			sreq, err := NewSecureRequestFromTransport(t, security, true)
   196  			Expect(err).ToNot(HaveOccurred())
   197  			r := sreq.(*SecureRequest)
   198  			Expect(r.CallerJWT).To(Equal("token"))
   199  			Expect(r.Signature).To(Equal([]byte("stub sig")))
   200  			Expect(r.SignerJWT).To(Equal(""))
   201  			Expect(r.MessageBody).To(ContainSubstring("io.choria.protocol.v2.request"))
   202  		})
   203  
   204  		It("Should handle validation failures", func() {
   205  			security.EXPECT().TokenBytes().Return([]byte("token"), nil)
   206  			security.EXPECT().SignBytes(gomock.AssignableToTypeOf([]byte{})).Return([]byte("stub sig"), nil).AnyTimes()
   207  			security.EXPECT().VerifySignatureBytes(gomock.Any(), gomock.Any(), gomock.Any()).Return(false, "").Times(1)
   208  
   209  			tsreq, err := NewSecureRequest(req, security)
   210  			Expect(err).ToNot(HaveOccurred())
   211  			t, err := NewTransportMessage("ginkgo.example.net")
   212  			Expect(err).ToNot(HaveOccurred())
   213  			Expect(t.SetRequestData(tsreq)).To(Succeed())
   214  
   215  			sreq, err := NewSecureRequestFromTransport(t, security, false)
   216  			Expect(err).To(MatchError("secure request messages created from Transport Message did not pass security validation"))
   217  			Expect(sreq).To(BeNil())
   218  		})
   219  
   220  		It("Should validate and produce a correct secure request", func() {
   221  			security.EXPECT().TokenBytes().Return([]byte("token"), nil)
   222  			security.EXPECT().SignBytes(gomock.AssignableToTypeOf([]byte{})).Return([]byte("stub sig"), nil).AnyTimes()
   223  			security.EXPECT().VerifySignatureBytes(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, "signer.example.net").Times(1)
   224  			security.EXPECT().ShouldAllowCaller(gomock.Any(), gomock.Any()).DoAndReturn(func(caller string, jwts ...[]byte) (bool, error) {
   225  				Expect(caller).To(Equal("up=ginkgo"))
   226  				return true, nil
   227  			})
   228  
   229  			tsreq, err := NewSecureRequest(req, security)
   230  			Expect(err).ToNot(HaveOccurred())
   231  			t, err := NewTransportMessage("ginkgo.example.net")
   232  			Expect(err).ToNot(HaveOccurred())
   233  			Expect(t.SetRequestData(tsreq)).To(Succeed())
   234  
   235  			sreq, err := NewSecureRequestFromTransport(t, security, false)
   236  			Expect(err).ToNot(HaveOccurred())
   237  			r := sreq.(*SecureRequest)
   238  			Expect(r.CallerJWT).To(Equal("token"))
   239  			Expect(r.Signature).To(Equal([]byte("stub sig")))
   240  			Expect(r.SignerJWT).To(Equal(""))
   241  			Expect(r.MessageBody).To(ContainSubstring("io.choria.protocol.v2.request"))
   242  		})
   243  	})
   244  
   245  	Describe("SetMessage", func() {
   246  		It("Should handle invalid requests", func() {
   247  			sreq := &SecureRequest{security: security}
   248  			err := sreq.SetMessage(&Request{})
   249  			Expect(err).To(MatchError(ErrInvalidJSON))
   250  			Expect(err.Error()).To(HavePrefix("could not JSON encode reply message"))
   251  		})
   252  
   253  		It("Should support insecure operation", func() {
   254  			protocol.Secure = "false"
   255  			sreq := &SecureRequest{security: security}
   256  			err := sreq.SetMessage(req)
   257  			Expect(err).ToNot(HaveOccurred())
   258  			Expect(sreq.Signature).To(Equal([]byte("insecure")))
   259  		})
   260  
   261  		It("Should sign the body and store it", func() {
   262  			security.EXPECT().SignBytes(gomock.AssignableToTypeOf([]byte{})).Return([]byte("stub sig"), nil).AnyTimes()
   263  			sreq := &SecureRequest{security: security}
   264  			err := sreq.SetMessage(req)
   265  			Expect(err).ToNot(HaveOccurred())
   266  			Expect(sreq.Signature).To(Equal([]byte("stub sig")))
   267  		})
   268  	})
   269  
   270  	Describe("Valid", func() {
   271  		It("Should support insecure operation", func() {
   272  			protocol.Secure = "false"
   273  			sreq := &SecureRequest{}
   274  			Expect(sreq.Valid()).To(BeTrue())
   275  		})
   276  
   277  		It("Should detect signature validation failures", func() {
   278  			security.EXPECT().TokenBytes().Return([]byte("caller jwt"), nil).AnyTimes()
   279  			security.EXPECT().SignBytes(gomock.AssignableToTypeOf([]byte{})).Return([]byte("stub sig"), nil).AnyTimes()
   280  			security.EXPECT().VerifySignatureBytes(gomock.Any(), gomock.Any(), gomock.Any()).Return(false, "").Times(1)
   281  
   282  			sreq, err := NewSecureRequest(req, security)
   283  			Expect(err).ToNot(HaveOccurred())
   284  			Expect(sreq.Valid()).To(BeFalse())
   285  		})
   286  
   287  		It("Should handle disallowed callers", func() {
   288  			security.EXPECT().TokenBytes().Return([]byte("caller jwt"), nil).AnyTimes()
   289  			security.EXPECT().SignBytes(gomock.AssignableToTypeOf([]byte{})).Return([]byte("stub sig"), nil).AnyTimes()
   290  			security.EXPECT().VerifySignatureBytes(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, "ginkgo").Times(1)
   291  			security.EXPECT().ShouldAllowCaller(gomock.Any(), gomock.Any()).Return(false, fmt.Errorf("simulated failure"))
   292  
   293  			sreq, err := NewSecureRequest(req, security)
   294  			Expect(err).ToNot(HaveOccurred())
   295  			Expect(sreq.Valid()).To(BeFalse())
   296  		})
   297  
   298  		It("Should do correct validations", func() {
   299  			security.EXPECT().TokenBytes().Return([]byte("caller jwt"), nil).AnyTimes()
   300  			security.EXPECT().SignBytes(gomock.AssignableToTypeOf([]byte{})).Return([]byte("stub sig"), nil).AnyTimes()
   301  
   302  			security.EXPECT().VerifySignatureBytes(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(body []byte, sig []byte, public ...[]byte) (bool, string) {
   303  				Expect(body).To(ContainSubstring("io.choria.protocol.v2.request"))
   304  				Expect(body).To(ContainSubstring(`"message":"aGVsbG8="`))
   305  				Expect(sig).To(Equal([]byte("stub sig")))
   306  				Expect(public).To(HaveLen(2))
   307  				Expect(public[0]).To(Equal([]byte("caller jwt")))
   308  				Expect(public[1]).To(Equal([]byte("signer jwt")))
   309  
   310  				return true, "ginkgo"
   311  			}).Times(1)
   312  
   313  			security.EXPECT().RemoteSignRequest(gomock.Any(), gomock.AssignableToTypeOf([]byte{})).DoAndReturn(func(_ context.Context, reqj []byte) ([]byte, error) {
   314  				signed, err := NewSecureRequest(req, security)
   315  				Expect(err).ToNot(HaveOccurred())
   316  
   317  				signed.(*SecureRequest).SignerJWT = "signer jwt"
   318  				signedj, err := signed.JSON()
   319  				Expect(err).ToNot(HaveOccurred())
   320  
   321  				return signedj, nil
   322  			}).AnyTimes()
   323  
   324  			security.EXPECT().ShouldAllowCaller(gomock.Any(), gomock.Any()).DoAndReturn(func(caller string, public ...[]byte) (bool, error) {
   325  				Expect(caller).To(Equal("up=ginkgo"))
   326  				Expect(public).To(HaveLen(2))
   327  				Expect(public[0]).To(Equal([]byte("caller jwt")))
   328  				Expect(public[1]).To(Equal([]byte("signer jwt")))
   329  
   330  				return false, nil
   331  			}).Times(1)
   332  
   333  			sreq, err := NewRemoteSignedSecureRequest(context.Background(), req, security)
   334  			Expect(err).ToNot(HaveOccurred())
   335  			Expect(sreq.Valid()).To(BeTrue())
   336  		})
   337  	})
   338  
   339  	Describe("IsValidJSON", func() {
   340  		It("Should detect invalid JSON data", func() {
   341  			sr := &SecureRequest{}
   342  			err := sr.IsValidJSON([]byte("{}"))
   343  			Expect(err).To(MatchError("supplied JSON document does not pass schema validation: missing properties: 'protocol', 'request', 'signature', 'caller'"))
   344  		})
   345  
   346  		It("Should accept valid JSON data", func() {
   347  			security.EXPECT().TokenBytes().Return([]byte("token"), nil)
   348  			security.EXPECT().SignBytes(gomock.AssignableToTypeOf([]byte{})).Return([]byte("stub sig"), nil).AnyTimes()
   349  
   350  			sreq, err := NewSecureRequest(req, security)
   351  			Expect(err).ToNot(HaveOccurred())
   352  
   353  			j, err := sreq.JSON()
   354  			Expect(err).ToNot(HaveOccurred())
   355  
   356  			Expect(sreq.IsValidJSON(j)).ToNot(HaveOccurred())
   357  		})
   358  	})
   359  })