go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/gce/vmtoken/vmtoken_test.go (about)

     1  // Copyright 2019 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 vmtoken
    16  
    17  import (
    18  	"context"
    19  	"encoding/base64"
    20  	"fmt"
    21  	"testing"
    22  	"time"
    23  
    24  	"go.chromium.org/luci/common/clock/testclock"
    25  
    26  	. "github.com/smartystreets/goconvey/convey"
    27  	. "go.chromium.org/luci/common/testing/assertions"
    28  )
    29  
    30  func TestMatches(t *testing.T) {
    31  	t.Parallel()
    32  
    33  	Convey("Matches", t, func() {
    34  		Convey("no payload", func() {
    35  			c := context.Background()
    36  			So(Matches(c, "instance", "zone", "project"), ShouldBeFalse)
    37  		})
    38  
    39  		Convey("payload", func() {
    40  			c := withPayload(context.Background(), &Payload{
    41  				Instance: "instance",
    42  				Project:  "project",
    43  				Zone:     "zone",
    44  			})
    45  
    46  			Convey("mismatch", func() {
    47  				So(Matches(c, "mismatch", "zone", "project"), ShouldBeFalse)
    48  				So(Matches(c, "instance", "mismatch", "project"), ShouldBeFalse)
    49  				So(Matches(c, "instance", "zone", "mismatch"), ShouldBeFalse)
    50  			})
    51  
    52  			Convey("match", func() {
    53  				So(Matches(c, "instance", "zone", "project"), ShouldBeTrue)
    54  			})
    55  		})
    56  	})
    57  }
    58  
    59  func TestVerify(t *testing.T) {
    60  	t.Parallel()
    61  
    62  	// This is a real token produced by GCE, without the signature.
    63  	const realTokenUnsigned = `eyJhbGciOiJSUzI1NiIsImtpZCI6IjA5MDVkNmY5Y2Q5YjBmMWY4NTJl` +
    64  		`OGIyMDdlOGY2NzNhYmNhNGJmNzUiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwczovL2V4Y` +
    65  		`W1wbGUuY29tIiwiYXpwIjoiMTE1NjE1Njc0NzIzMTA1NTU1Nzg4IiwiZW1haWwiOiJjaHJvb` +
    66  		`WUtc3dhcm1pbmdAY2hyb21lY29tcHV0ZS5nb29nbGUuY29tLmlhbS5nc2VydmljZWFjY291b` +
    67  		`nQuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImV4cCI6MTU1MzU2ODAzMiwiZ29vZ2xlI` +
    68  		`jp7ImNvbXB1dGVfZW5naW5lIjp7Imluc3RhbmNlX2NyZWF0aW9uX3RpbWVzdGFtcCI6MTUzO` +
    69  		`TM4OTQzNSwiaW5zdGFuY2VfaWQiOiI3NDI1MzUwMzU1MDI1NzM0NTUwIiwiaW5zdGFuY2Vfb` +
    70  		`mFtZSI6InN3YXJtNC1jNyIsInByb2plY3RfaWQiOiJnb29nbGUuY29tOmNocm9tZWNvbXB1d` +
    71  		`GUiLCJwcm9qZWN0X251bWJlciI6MTgyNjE1NTA2OTc5LCJ6b25lIjoidXMtY2VudHJhbDEtY` +
    72  		`iJ9fSwiaWF0IjoxNTUzNTY0NDMyLCJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb` +
    73  		`20iLCJzdWIiOiIxMTU2MTU2NzQ3MjMxMDU1NTU3ODgifQ`
    74  
    75  	// "Real" token, except the signature is mocked.
    76  	var realToken = realTokenUnsigned + "." + b64("REDACTED_SIGNATURE")
    77  
    78  	// Parameters encoded in the token above.
    79  	const (
    80  		testKeyID    = "0905d6f9cd9b0f1f852e8b207e8f673abca4bf75"
    81  		testIat      = 1553564432
    82  		testExp      = testIat + 3600
    83  		testProject  = "google.com:chromecompute"
    84  		testZone     = "us-central1-b"
    85  		testInstance = "swarm4-c7"
    86  		testAudience = "https://example.com"
    87  	)
    88  
    89  	Convey("With mocked certs and time", t, func() {
    90  		ctx, _ := testclock.UseTime(context.Background(), time.Unix(testIat, 0))
    91  		certs := mockedCerts{}
    92  
    93  		Convey("Decode real token", func() {
    94  			payload, err := verifyImpl(ctx, realToken, &certs)
    95  			So(err, ShouldBeNil)
    96  			So(payload, ShouldResemble, &Payload{
    97  				Project:  testProject,
    98  				Zone:     testZone,
    99  				Instance: testInstance,
   100  				Audience: testAudience,
   101  			})
   102  			So(certs.calls, ShouldHaveLength, 1)
   103  			So(certs.calls[0], ShouldResemble, checkSignatureCall{
   104  				key:       testKeyID,
   105  				signed:    []byte(realTokenUnsigned),
   106  				signature: []byte("REDACTED_SIGNATURE"),
   107  			})
   108  		})
   109  
   110  		Convey("Token used too soon", func() {
   111  			ctx, _ = testclock.UseTime(context.Background(), time.Unix(testIat-60, 0))
   112  			_, err := verifyImpl(ctx, realToken, &certs)
   113  			So(err, ShouldErrLike, "bad JWT: too early (now 1553564372 < iat 1553564432)")
   114  		})
   115  
   116  		Convey("Token used too late", func() {
   117  			ctx, _ = testclock.UseTime(context.Background(), time.Unix(testExp+60, 0))
   118  			_, err := verifyImpl(ctx, realToken, &certs)
   119  			So(err, ShouldErrLike, "bad JWT: expired (now 1553568092 > exp 1553568032)")
   120  		})
   121  
   122  		Convey("Bad JWT structure", func() {
   123  			_, err := verifyImpl(ctx, realTokenUnsigned, &certs) // no signature part
   124  			So(err, ShouldErrLike, "expected 3 components")
   125  		})
   126  
   127  		Convey("Not base64 header", func() {
   128  			_, err := verifyImpl(ctx, "!!!!.AAAA.AAAA", &certs)
   129  			So(err, ShouldErrLike, "bad JWT header: not base64")
   130  		})
   131  
   132  		Convey("Not JSON header", func() {
   133  			_, err := verifyImpl(ctx, b64("huh")+".AAAA.AAAA", &certs)
   134  			So(err, ShouldErrLike, "bad JWT header: not JSON")
   135  		})
   136  
   137  		Convey("Wrong algo", func() {
   138  			_, err := verifyImpl(ctx, b64(`{"alg":"huh"}`)+".AAAA.AAAA", &certs)
   139  			So(err, ShouldErrLike, `bad JWT: only RS256 alg is supported, not "huh"`)
   140  		})
   141  
   142  		Convey("Missing key ID", func() {
   143  			_, err := verifyImpl(ctx, b64(`{"alg":"RS256"}`)+".AAAA.AAAA", &certs)
   144  			So(err, ShouldErrLike, `bad JWT: missing the signing key ID in the header`)
   145  		})
   146  
   147  		Convey("Bad base64 signature", func() {
   148  			_, err := verifyImpl(ctx, hdr()+".AAAA.!!!!", &certs)
   149  			So(err, ShouldErrLike, "bad JWT: can't base64 decode the signature")
   150  		})
   151  
   152  		Convey("Signature check error", func() {
   153  			certs.err = fmt.Errorf("boom")
   154  			_, err := verifyImpl(ctx, hdr()+".AAAA."+b64("sig"), &certs)
   155  			So(err, ShouldErrLike, "bad JWT: bad signature: boom")
   156  		})
   157  
   158  		Convey("Bad payload", func() {
   159  			_, err := verifyImpl(ctx, hdr()+".!!!!."+b64("sig"), &certs)
   160  			So(err, ShouldErrLike, "bad JWT payload: not base64")
   161  		})
   162  
   163  		Convey("Missing `google.compute_engine` section", func() {
   164  			_, err := verifyImpl(ctx, hdr()+"."+b64(`{}`)+"."+b64("sig"), &certs)
   165  			So(err, ShouldErrLike, "no google.compute_engine in the GCE VM token, use 'full' format")
   166  		})
   167  	})
   168  }
   169  
   170  func hdr() string {
   171  	return b64(`{"alg":"RS256","kid":"key id"}`)
   172  }
   173  
   174  func b64(s string) string {
   175  	return base64.RawURLEncoding.EncodeToString([]byte(s))
   176  }
   177  
   178  type mockedCerts struct {
   179  	calls []checkSignatureCall
   180  	err   error
   181  }
   182  
   183  type checkSignatureCall struct {
   184  	key       string
   185  	signed    []byte
   186  	signature []byte
   187  }
   188  
   189  func (m *mockedCerts) CheckSignature(key string, signed, signature []byte) error {
   190  	m.calls = append(m.calls, checkSignatureCall{key, signed, signature})
   191  	return m.err
   192  }