go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/swarming/server/botsrv/botsrv_test.go (about) 1 // Copyright 2022 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package botsrv 16 17 import ( 18 "bytes" 19 "context" 20 "encoding/json" 21 "io" 22 "net" 23 "net/http" 24 "net/http/httptest" 25 "testing" 26 "time" 27 28 "google.golang.org/protobuf/proto" 29 "google.golang.org/protobuf/types/known/timestamppb" 30 31 "go.chromium.org/luci/auth/identity" 32 "go.chromium.org/luci/common/clock/testclock" 33 "go.chromium.org/luci/server/auth" 34 "go.chromium.org/luci/server/auth/authtest" 35 "go.chromium.org/luci/server/auth/openid" 36 "go.chromium.org/luci/server/router" 37 "go.chromium.org/luci/server/secrets" 38 "go.chromium.org/luci/tokenserver/auth/machine" 39 40 internalspb "go.chromium.org/luci/swarming/proto/internals" 41 "go.chromium.org/luci/swarming/server/hmactoken" 42 43 . "github.com/smartystreets/goconvey/convey" 44 . "go.chromium.org/luci/common/testing/assertions" 45 ) 46 47 type testRequest struct { 48 Dimensions map[string][]string 49 PollToken []byte 50 SessionToken []byte 51 } 52 53 func (r *testRequest) ExtractPollToken() []byte { return r.PollToken } 54 func (r *testRequest) ExtractSessionToken() []byte { return r.SessionToken } 55 func (r *testRequest) ExtractDimensions() map[string][]string { return r.Dimensions } 56 func (r *testRequest) ExtractDebugRequest() any { return r } 57 58 func TestBotHandler(t *testing.T) { 59 t.Parallel() 60 61 Convey("With server", t, func() { 62 now := time.Date(2044, time.April, 4, 4, 4, 4, 4, time.UTC) 63 ctx := context.Background() 64 ctx, _ = testclock.UseTime(ctx, now) 65 66 ctx = auth.WithState(ctx, &authtest.FakeState{ 67 Identity: "bot:ignored", 68 UserExtra: &machine.MachineTokenInfo{ 69 FQDN: "bot.fqdn", 70 }, 71 }) 72 73 srv := &Server{ 74 router: router.New(), 75 hmacSecret: hmactoken.NewStaticSecret(secrets.Secret{ 76 Active: []byte("secret"), 77 Passive: [][]byte{[]byte("also-secret")}, 78 }), 79 } 80 81 var lastBody *testRequest 82 var lastRequest *Request 83 var nextResponse Response 84 var nextError error 85 InstallHandler(srv, "/test", func(_ context.Context, body *testRequest, r *Request) (Response, error) { 86 lastBody = body 87 lastRequest = r 88 return nextResponse, nextError 89 }) 90 91 callRaw := func(body []byte, ct string, mockedResp Response, mockedErr error) (b *testRequest, req *Request, status int, resp string) { 92 lastRequest = nil 93 nextResponse = mockedResp 94 nextError = mockedErr 95 rq := httptest.NewRequest("POST", "/test", bytes.NewReader(body)).WithContext(ctx) 96 if ct != "" { 97 rq.Header.Set("Content-Type", ct) 98 } 99 rw := httptest.NewRecorder() 100 srv.router.ServeHTTP(rw, rq) 101 res := rw.Result() 102 if res.StatusCode == http.StatusOK { 103 So(res.Header.Get("Content-Type"), ShouldEqual, "application/json; charset=utf-8") 104 } 105 respBody, _ := io.ReadAll(res.Body) 106 return lastBody, lastRequest, res.StatusCode, string(respBody) 107 } 108 109 call := func(body testRequest, mockedResp Response, mockedErr error) (b *testRequest, req *Request, status int, resp string) { 110 blob, err := json.Marshal(&body) 111 So(err, ShouldBeNil) 112 return callRaw(blob, "application/json; charset=utf-8", mockedResp, mockedErr) 113 } 114 115 makePollState := func(id string) *internalspb.PollState { 116 return &internalspb.PollState{ 117 Id: id, 118 Expiry: timestamppb.New(now.Add(5 * time.Minute)), 119 RbeInstance: "some-rbe-instance", 120 EnforcedDimensions: []*internalspb.PollState_Dimension{ 121 {Key: "id", Values: []string{"bot-id"}}, 122 }, 123 AuthMethod: &internalspb.PollState_LuciMachineTokenAuth{ 124 LuciMachineTokenAuth: &internalspb.PollState_LUCIMachineTokenAuth{ 125 MachineFqdn: "bot.fqdn", 126 }, 127 }, 128 } 129 } 130 131 Convey("Happy path with poll token", func() { 132 pollState := makePollState("poll-state-id") 133 134 req := testRequest{ 135 Dimensions: map[string][]string{ 136 "id": {"bot-id"}, 137 "pool": {"pool"}, 138 }, 139 PollToken: genToken(pollState, []byte("also-secret")), 140 } 141 142 body, seenReq, status, resp := call(req, "some-response", nil) 143 So(status, ShouldEqual, http.StatusOK) 144 So(resp, ShouldEqual, "\"some-response\"\n") 145 So(body, ShouldResemble, &req) 146 So(seenReq.BotID, ShouldEqual, "bot-id") 147 So(seenReq.SessionID, ShouldEqual, "") 148 So(seenReq.SessionTokenExpired, ShouldBeFalse) 149 So(seenReq.PollState, ShouldResembleProto, pollState) 150 }) 151 152 Convey("Happy path with session token", func() { 153 pollState := makePollState("poll-state-id") 154 155 req := testRequest{ 156 Dimensions: map[string][]string{ 157 "id": {"bot-id"}, 158 "pool": {"pool"}, 159 }, 160 SessionToken: genToken(&internalspb.BotSession{ 161 RbeBotSessionId: "bot-session-id", 162 PollState: pollState, 163 Expiry: timestamppb.New(now.Add(5 * time.Minute)), 164 }, []byte("also-secret")), 165 } 166 167 body, seenReq, status, resp := call(req, "some-response", nil) 168 So(status, ShouldEqual, http.StatusOK) 169 So(resp, ShouldEqual, "\"some-response\"\n") 170 So(body, ShouldResemble, &req) 171 So(seenReq.BotID, ShouldEqual, "bot-id") 172 So(seenReq.SessionID, ShouldEqual, "bot-session-id") 173 So(seenReq.SessionTokenExpired, ShouldBeFalse) 174 So(seenReq.PollState, ShouldResembleProto, pollState) 175 }) 176 177 Convey("Happy path with both tokens", func() { 178 pollStateInPollToken := makePollState("in-poll-token") 179 pollStateInSessionToken := makePollState("in-session-token") 180 181 req := testRequest{ 182 Dimensions: map[string][]string{ 183 "id": {"bot-id"}, 184 "pool": {"pool"}, 185 }, 186 PollToken: genToken(pollStateInPollToken, []byte("also-secret")), 187 SessionToken: genToken(&internalspb.BotSession{ 188 RbeBotSessionId: "bot-session-id", 189 PollState: pollStateInSessionToken, 190 Expiry: timestamppb.New(now.Add(5 * time.Minute)), 191 }, []byte("also-secret")), 192 } 193 194 body, seenReq, status, resp := call(req, "some-response", nil) 195 So(status, ShouldEqual, http.StatusOK) 196 So(resp, ShouldEqual, "\"some-response\"\n") 197 So(body, ShouldResemble, &req) 198 So(seenReq.BotID, ShouldEqual, "bot-id") 199 So(seenReq.SessionID, ShouldEqual, "bot-session-id") 200 So(seenReq.SessionTokenExpired, ShouldBeFalse) 201 So(seenReq.PollState, ShouldResembleProto, pollStateInPollToken) 202 }) 203 204 Convey("Happy path with session token and expired poll token", func() { 205 pollStateInPollToken := makePollState("in-poll-token") 206 pollStateInPollToken.Expiry = timestamppb.New(now.Add(-5 * time.Minute)) 207 208 pollStateInSessionToken := makePollState("in-session-token") 209 210 req := testRequest{ 211 Dimensions: map[string][]string{ 212 "id": {"bot-id"}, 213 "pool": {"pool"}, 214 }, 215 PollToken: genToken(pollStateInPollToken, []byte("also-secret")), 216 SessionToken: genToken(&internalspb.BotSession{ 217 RbeBotSessionId: "bot-session-id", 218 PollState: pollStateInSessionToken, 219 Expiry: timestamppb.New(now.Add(5 * time.Minute)), 220 }, []byte("also-secret")), 221 } 222 223 body, seenReq, status, resp := call(req, "some-response", nil) 224 So(status, ShouldEqual, http.StatusOK) 225 So(resp, ShouldEqual, "\"some-response\"\n") 226 So(body, ShouldResemble, &req) 227 So(seenReq.BotID, ShouldEqual, "bot-id") 228 So(seenReq.SessionID, ShouldEqual, "bot-session-id") 229 So(seenReq.SessionTokenExpired, ShouldBeFalse) 230 231 // Used the session token. 232 So(seenReq.PollState, ShouldResembleProto, pollStateInSessionToken) 233 }) 234 235 Convey("Happy path with poll token and expired session token", func() { 236 pollStateInPollToken := makePollState("in-poll-token") 237 238 pollStateInSessionToken := makePollState("in-session-token") 239 pollStateInSessionToken.Expiry = timestamppb.New(now.Add(-5 * time.Minute)) 240 241 req := testRequest{ 242 Dimensions: map[string][]string{ 243 "id": {"bot-id"}, 244 "pool": {"pool"}, 245 }, 246 PollToken: genToken(pollStateInPollToken, []byte("also-secret")), 247 SessionToken: genToken(&internalspb.BotSession{ 248 RbeBotSessionId: "bot-session-id", 249 PollState: pollStateInSessionToken, 250 Expiry: pollStateInSessionToken.Expiry, 251 }, []byte("also-secret")), 252 } 253 254 body, seenReq, status, resp := call(req, "some-response", nil) 255 So(status, ShouldEqual, http.StatusOK) 256 So(resp, ShouldEqual, "\"some-response\"\n") 257 So(body, ShouldResemble, &req) 258 So(seenReq.BotID, ShouldEqual, "bot-id") 259 So(seenReq.SessionID, ShouldEqual, "") 260 So(seenReq.SessionTokenExpired, ShouldBeTrue) 261 262 // Used the poll token. 263 So(seenReq.PollState, ShouldResembleProto, pollStateInPollToken) 264 }) 265 266 Convey("Wrong bot credentials", func() { 267 pollState := &internalspb.PollState{ 268 Id: "poll-state-id", 269 Expiry: timestamppb.New(now.Add(5 * time.Minute)), 270 RbeInstance: "some-rbe-instance", 271 EnforcedDimensions: []*internalspb.PollState_Dimension{ 272 {Key: "id", Values: []string{"bot-id"}}, 273 }, 274 AuthMethod: &internalspb.PollState_LuciMachineTokenAuth{ 275 LuciMachineTokenAuth: &internalspb.PollState_LUCIMachineTokenAuth{ 276 MachineFqdn: "another.fqdn", 277 }, 278 }, 279 } 280 281 req := testRequest{ 282 Dimensions: map[string][]string{ 283 "id": {"bot-id"}, 284 }, 285 PollToken: genToken(pollState, []byte("also-secret")), 286 } 287 288 _, seenReq, status, resp := call(req, "some-response", nil) 289 So(seenReq, ShouldBeNil) 290 So(status, ShouldEqual, http.StatusUnauthorized) 291 So(resp, ShouldContainSubstring, "bad bot credentials: wrong FQDN in the LUCI machine token") 292 }) 293 294 Convey("Bad Content-Type", func() { 295 _, seenReq, status, resp := callRaw([]byte("ignored"), "application/x-www-form-urlencoded", nil, nil) 296 So(seenReq, ShouldBeNil) 297 So(status, ShouldEqual, http.StatusBadRequest) 298 So(resp, ShouldContainSubstring, "bad content type") 299 }) 300 301 Convey("Not JSON", func() { 302 _, seenReq, status, resp := callRaw([]byte("what is this"), "application/json; charset=utf-8", nil, nil) 303 So(seenReq, ShouldBeNil) 304 So(status, ShouldEqual, http.StatusBadRequest) 305 So(resp, ShouldContainSubstring, "failed to deserialized") 306 }) 307 308 Convey("Wrong poll token", func() { 309 req := testRequest{ 310 PollToken: genToken(&internalspb.BotSession{ 311 RbeBotSessionId: "not-a-poll-token", 312 Expiry: timestamppb.New(now.Add(5 * time.Minute)), 313 }, []byte("also-secret")), 314 } 315 _, seenReq, status, resp := call(req, "some-response", nil) 316 So(seenReq, ShouldBeNil) 317 So(status, ShouldEqual, http.StatusUnauthorized) 318 So(resp, ShouldContainSubstring, "failed to verify poll token: invalid payload type") 319 }) 320 321 Convey("Wrong session token", func() { 322 req := testRequest{ 323 SessionToken: genToken(&internalspb.PollState{ 324 Id: "not-a-session-token", 325 Expiry: timestamppb.New(now.Add(5 * time.Minute)), 326 }, []byte("also-secret")), 327 } 328 _, seenReq, status, resp := call(req, "some-response", nil) 329 So(seenReq, ShouldBeNil) 330 So(status, ShouldEqual, http.StatusUnauthorized) 331 So(resp, ShouldContainSubstring, "failed to verify session token: invalid payload type") 332 }) 333 334 Convey("Expired poll token", func() { 335 req := testRequest{ 336 PollToken: genToken(&internalspb.PollState{ 337 Id: "poll-state-id", 338 Expiry: timestamppb.New(now.Add(-5 * time.Minute)), 339 }, []byte("also-secret")), 340 } 341 _, seenReq, status, resp := call(req, "some-response", nil) 342 So(seenReq, ShouldBeNil) 343 So(status, ShouldEqual, http.StatusUnauthorized) 344 So(resp, ShouldContainSubstring, "no valid poll or state token") 345 }) 346 347 Convey("Expired session token", func() { 348 req := testRequest{ 349 SessionToken: genToken(&internalspb.BotSession{ 350 RbeBotSessionId: "session-id", 351 Expiry: timestamppb.New(now.Add(-5 * time.Minute)), 352 PollState: makePollState("poll-state-id"), 353 }, []byte("also-secret")), 354 } 355 _, seenReq, status, resp := call(req, "some-response", nil) 356 So(seenReq, ShouldBeNil) 357 So(status, ShouldEqual, http.StatusUnauthorized) 358 So(resp, ShouldContainSubstring, "no valid poll or state token") 359 }) 360 361 Convey("Expired session and poll tokens", func() { 362 req := testRequest{ 363 PollToken: genToken(&internalspb.PollState{ 364 Id: "poll-state-id", 365 Expiry: timestamppb.New(now.Add(-5 * time.Minute)), 366 }, []byte("also-secret")), 367 SessionToken: genToken(&internalspb.BotSession{ 368 RbeBotSessionId: "session-id", 369 Expiry: timestamppb.New(now.Add(-5 * time.Minute)), 370 PollState: makePollState("poll-state-id"), 371 }, []byte("also-secret")), 372 } 373 _, seenReq, status, resp := call(req, "some-response", nil) 374 So(seenReq, ShouldBeNil) 375 So(status, ShouldEqual, http.StatusUnauthorized) 376 So(resp, ShouldContainSubstring, "no valid poll or state token") 377 }) 378 379 Convey("Session token with no session ID", func() { 380 req := testRequest{ 381 SessionToken: genToken(&internalspb.BotSession{ 382 Expiry: timestamppb.New(now.Add(5 * time.Minute)), 383 PollState: makePollState("poll-state-id"), 384 }, []byte("also-secret")), 385 } 386 _, seenReq, status, resp := call(req, "some-response", nil) 387 So(seenReq, ShouldBeNil) 388 So(status, ShouldEqual, http.StatusBadRequest) 389 So(resp, ShouldContainSubstring, "no session ID") 390 }) 391 392 Convey("Poll state dimension overrides", func() { 393 pollState := &internalspb.PollState{ 394 Id: "poll-state-id", 395 Expiry: timestamppb.New(now.Add(5 * time.Minute)), 396 RbeInstance: "correct-rbe-instance", 397 EnforcedDimensions: []*internalspb.PollState_Dimension{ 398 {Key: "id", Values: []string{"correct-bot-id"}}, 399 {Key: "keep", Values: []string{"a", "b"}}, 400 {Key: "override-1", Values: []string{"a"}}, 401 {Key: "override-2", Values: []string{"b", "a"}}, 402 {Key: "inject", Values: []string{"a"}}, 403 }, 404 AuthMethod: &internalspb.PollState_LuciMachineTokenAuth{ 405 LuciMachineTokenAuth: &internalspb.PollState_LUCIMachineTokenAuth{ 406 MachineFqdn: "bot.fqdn", 407 }, 408 }, 409 } 410 411 req := testRequest{ 412 Dimensions: map[string][]string{ 413 "id": {"wrong-bot-id"}, 414 "pool": {"pool"}, 415 "keep": {"a", "b"}, 416 "override-1": {"a", "b"}, 417 "override-2": {"a", "b"}, 418 "keep-extra": {"a"}, 419 }, 420 PollToken: genToken(pollState, []byte("also-secret")), 421 } 422 423 body, seenReq, status, _ := call(req, nil, nil) 424 So(status, ShouldEqual, http.StatusOK) 425 So(body, ShouldResemble, &testRequest{ 426 Dimensions: map[string][]string{ 427 "id": {"correct-bot-id"}, 428 "pool": {"pool"}, 429 "keep": {"a", "b"}, 430 "override-1": {"a"}, 431 "override-2": {"b", "a"}, 432 "keep-extra": {"a"}, 433 "inject": {"a"}, 434 }, 435 PollToken: req.PollToken, 436 }) 437 So(seenReq.BotID, ShouldEqual, "correct-bot-id") 438 }) 439 }) 440 } 441 442 func TestCheckCredentials(t *testing.T) { 443 t.Parallel() 444 445 Convey("No creds", t, func() { 446 ctx := auth.WithState(context.Background(), &authtest.FakeState{ 447 Identity: identity.AnonymousIdentity, 448 }) 449 450 err := checkCredentials(ctx, &internalspb.PollState{ 451 AuthMethod: &internalspb.PollState_GceAuth{ 452 GceAuth: &internalspb.PollState_GCEAuth{ 453 GceProject: "some-project", 454 GceInstance: "some-instance", 455 }, 456 }, 457 }) 458 So(err, ShouldErrLike, "expecting GCE VM token auth") 459 460 err = checkCredentials(ctx, &internalspb.PollState{ 461 AuthMethod: &internalspb.PollState_ServiceAccountAuth_{ 462 ServiceAccountAuth: &internalspb.PollState_ServiceAccountAuth{ 463 ServiceAccount: "some-account@example.com", 464 }, 465 }, 466 }) 467 So(err, ShouldErrLike, "expecting service account credentials") 468 469 err = checkCredentials(ctx, &internalspb.PollState{ 470 AuthMethod: &internalspb.PollState_LuciMachineTokenAuth{ 471 LuciMachineTokenAuth: &internalspb.PollState_LUCIMachineTokenAuth{ 472 MachineFqdn: "some.fqdn", 473 }, 474 }, 475 }) 476 So(err, ShouldErrLike, "expecting LUCI machine token auth") 477 478 err = checkCredentials(ctx, &internalspb.PollState{ 479 AuthMethod: &internalspb.PollState_IpAllowlistAuth{}, 480 IpAllowlist: "some-ip-allowlist", 481 }) 482 So(err, ShouldErrLike, "is not in the allowlist") 483 }) 484 485 Convey("GCE auth", t, func() { 486 ctx := auth.WithState(context.Background(), &authtest.FakeState{ 487 Identity: "bot:ignored", 488 UserExtra: &openid.GoogleComputeTokenInfo{ 489 Project: "some-project", 490 Instance: "some-instance", 491 }, 492 }) 493 494 // OK. 495 err := checkCredentials(ctx, &internalspb.PollState{ 496 AuthMethod: &internalspb.PollState_GceAuth{ 497 GceAuth: &internalspb.PollState_GCEAuth{ 498 GceProject: "some-project", 499 GceInstance: "some-instance", 500 }, 501 }, 502 }) 503 So(err, ShouldBeNil) 504 505 // Wrong parameters #1. 506 err = checkCredentials(ctx, &internalspb.PollState{ 507 AuthMethod: &internalspb.PollState_GceAuth{ 508 GceAuth: &internalspb.PollState_GCEAuth{ 509 GceProject: "another-project", 510 GceInstance: "some-instance", 511 }, 512 }, 513 }) 514 So(err, ShouldErrLike, "wrong GCE VM token") 515 516 // Wrong parameters #2. 517 err = checkCredentials(ctx, &internalspb.PollState{ 518 AuthMethod: &internalspb.PollState_GceAuth{ 519 GceAuth: &internalspb.PollState_GCEAuth{ 520 GceProject: "some-project", 521 GceInstance: "another-instance", 522 }, 523 }, 524 }) 525 So(err, ShouldErrLike, "wrong GCE VM token") 526 }) 527 528 Convey("Service account auth", t, func() { 529 ctx := auth.WithState(context.Background(), &authtest.FakeState{ 530 Identity: "user:some-account@example.com", 531 }) 532 533 // OK. 534 err := checkCredentials(ctx, &internalspb.PollState{ 535 AuthMethod: &internalspb.PollState_ServiceAccountAuth_{ 536 ServiceAccountAuth: &internalspb.PollState_ServiceAccountAuth{ 537 ServiceAccount: "some-account@example.com", 538 }, 539 }, 540 }) 541 So(err, ShouldBeNil) 542 543 // Wrong email. 544 err = checkCredentials(ctx, &internalspb.PollState{ 545 AuthMethod: &internalspb.PollState_ServiceAccountAuth_{ 546 ServiceAccountAuth: &internalspb.PollState_ServiceAccountAuth{ 547 ServiceAccount: "another-account@example.com", 548 }, 549 }, 550 }) 551 So(err, ShouldErrLike, "wrong service account") 552 }) 553 554 Convey("Machine token auth", t, func() { 555 ctx := auth.WithState(context.Background(), &authtest.FakeState{ 556 Identity: "bot:ignored", 557 UserExtra: &machine.MachineTokenInfo{ 558 FQDN: "some.fqdn", 559 }, 560 }) 561 562 // OK. 563 err := checkCredentials(ctx, &internalspb.PollState{ 564 AuthMethod: &internalspb.PollState_LuciMachineTokenAuth{ 565 LuciMachineTokenAuth: &internalspb.PollState_LUCIMachineTokenAuth{ 566 MachineFqdn: "some.fqdn", 567 }, 568 }, 569 }) 570 So(err, ShouldBeNil) 571 572 // Wrong FQDN. 573 err = checkCredentials(ctx, &internalspb.PollState{ 574 AuthMethod: &internalspb.PollState_LuciMachineTokenAuth{ 575 LuciMachineTokenAuth: &internalspb.PollState_LUCIMachineTokenAuth{ 576 MachineFqdn: "another.fqdn", 577 }, 578 }, 579 }) 580 So(err, ShouldErrLike, "wrong FQDN in the LUCI machine token") 581 }) 582 583 Convey("IP allowlist", t, func() { 584 ctx := auth.WithState(context.Background(), &authtest.FakeState{ 585 Identity: identity.AnonymousIdentity, 586 PeerIPOverride: net.ParseIP("127.1.1.1"), 587 FakeDB: authtest.NewFakeDB( 588 authtest.MockIPAllowlist("127.1.1.1", "good"), 589 authtest.MockIPAllowlist("127.2.2.2", "bad"), 590 ), 591 }) 592 593 // OK. 594 err := checkCredentials(ctx, &internalspb.PollState{ 595 AuthMethod: &internalspb.PollState_IpAllowlistAuth{}, 596 IpAllowlist: "good", 597 }) 598 So(err, ShouldBeNil) 599 600 // Wrong IP. 601 err = checkCredentials(ctx, &internalspb.PollState{ 602 AuthMethod: &internalspb.PollState_IpAllowlistAuth{}, 603 IpAllowlist: "bad", 604 }) 605 So(err, ShouldErrLike, "bot IP 127.1.1.1 is not in the allowlist") 606 }) 607 } 608 609 func genToken(msg proto.Message, secret []byte) []byte { 610 tok, err := hmactoken.NewStaticSecret(secrets.Secret{Active: secret}).GenerateToken(msg) 611 if err != nil { 612 panic(err) 613 } 614 return tok 615 }