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 }