blob: f633b015f7f34ba073899625e28e781e6446cdf1 [file] [log] [blame]
// Copyright 2019 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package vmtoken
import (
"context"
"encoding/base64"
"fmt"
"testing"
"time"
"go.chromium.org/luci/common/clock/testclock"
"go.chromium.org/luci/common/testing/ftt"
"go.chromium.org/luci/common/testing/truth/assert"
"go.chromium.org/luci/common/testing/truth/should"
)
func TestMatches(t *testing.T) {
t.Parallel()
ftt.Run("Matches", t, func(t *ftt.Test) {
t.Run("no payload", func(t *ftt.Test) {
c := context.Background()
assert.Loosely(t, Matches(c, "instance", "zone", "project"), should.BeFalse)
})
t.Run("payload", func(t *ftt.Test) {
c := withPayload(context.Background(), &Payload{
Instance: "instance",
Project: "project",
Zone: "zone",
})
t.Run("mismatch", func(t *ftt.Test) {
assert.Loosely(t, Matches(c, "mismatch", "zone", "project"), should.BeFalse)
assert.Loosely(t, Matches(c, "instance", "mismatch", "project"), should.BeFalse)
assert.Loosely(t, Matches(c, "instance", "zone", "mismatch"), should.BeFalse)
})
t.Run("match", func(t *ftt.Test) {
assert.Loosely(t, Matches(c, "instance", "zone", "project"), should.BeTrue)
})
})
})
}
func TestVerify(t *testing.T) {
t.Parallel()
// This is a real token produced by GCE, without the signature.
const realTokenUnsigned = `eyJhbGciOiJSUzI1NiIsImtpZCI6IjA5MDVkNmY5Y2Q5YjBmMWY4NTJl` +
`OGIyMDdlOGY2NzNhYmNhNGJmNzUiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwczovL2V4Y` +
`W1wbGUuY29tIiwiYXpwIjoiMTE1NjE1Njc0NzIzMTA1NTU1Nzg4IiwiZW1haWwiOiJjaHJvb` +
`WUtc3dhcm1pbmdAY2hyb21lY29tcHV0ZS5nb29nbGUuY29tLmlhbS5nc2VydmljZWFjY291b` +
`nQuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImV4cCI6MTU1MzU2ODAzMiwiZ29vZ2xlI` +
`jp7ImNvbXB1dGVfZW5naW5lIjp7Imluc3RhbmNlX2NyZWF0aW9uX3RpbWVzdGFtcCI6MTUzO` +
`TM4OTQzNSwiaW5zdGFuY2VfaWQiOiI3NDI1MzUwMzU1MDI1NzM0NTUwIiwiaW5zdGFuY2Vfb` +
`mFtZSI6InN3YXJtNC1jNyIsInByb2plY3RfaWQiOiJnb29nbGUuY29tOmNocm9tZWNvbXB1d` +
`GUiLCJwcm9qZWN0X251bWJlciI6MTgyNjE1NTA2OTc5LCJ6b25lIjoidXMtY2VudHJhbDEtY` +
`iJ9fSwiaWF0IjoxNTUzNTY0NDMyLCJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb` +
`20iLCJzdWIiOiIxMTU2MTU2NzQ3MjMxMDU1NTU3ODgifQ`
// "Real" token, except the signature is mocked.
var realToken = realTokenUnsigned + "." + b64("REDACTED_SIGNATURE")
// Parameters encoded in the token above.
const (
testKeyID = "0905d6f9cd9b0f1f852e8b207e8f673abca4bf75"
testIat = 1553564432
testExp = testIat + 3600
testProject = "google.com:chromecompute"
testZone = "us-central1-b"
testInstance = "swarm4-c7"
testAudience = "https://example.com"
)
ftt.Run("With mocked certs and time", t, func(t *ftt.Test) {
ctx, _ := testclock.UseTime(context.Background(), time.Unix(testIat, 0))
certs := mockedCerts{}
t.Run("Decode real token", func(t *ftt.Test) {
payload, err := verifyImpl(ctx, realToken, &certs)
assert.Loosely(t, err, should.BeNil)
assert.Loosely(t, payload, should.Resemble(&Payload{
Project: testProject,
Zone: testZone,
Instance: testInstance,
Audience: testAudience,
}))
assert.Loosely(t, certs.calls, should.HaveLength(1))
assert.Loosely(t, certs.calls[0], should.Resemble(checkSignatureCall{
key: testKeyID,
signed: []byte(realTokenUnsigned),
signature: []byte("REDACTED_SIGNATURE"),
}))
})
t.Run("Token used too soon", func(t *ftt.Test) {
ctx, _ = testclock.UseTime(context.Background(), time.Unix(testIat-60, 0))
_, err := verifyImpl(ctx, realToken, &certs)
assert.Loosely(t, err, should.ErrLike("bad JWT: too early (now 1553564372 < iat 1553564432)"))
})
t.Run("Token used too late", func(t *ftt.Test) {
ctx, _ = testclock.UseTime(context.Background(), time.Unix(testExp+60, 0))
_, err := verifyImpl(ctx, realToken, &certs)
assert.Loosely(t, err, should.ErrLike("bad JWT: expired (now 1553568092 > exp 1553568032)"))
})
t.Run("Bad JWT structure", func(t *ftt.Test) {
_, err := verifyImpl(ctx, realTokenUnsigned, &certs) // no signature part
assert.Loosely(t, err, should.ErrLike("expected 3 components"))
})
t.Run("Not base64 header", func(t *ftt.Test) {
_, err := verifyImpl(ctx, "!!!!.AAAA.AAAA", &certs)
assert.Loosely(t, err, should.ErrLike("bad JWT header: not base64"))
})
t.Run("Not JSON header", func(t *ftt.Test) {
_, err := verifyImpl(ctx, b64("huh")+".AAAA.AAAA", &certs)
assert.Loosely(t, err, should.ErrLike("bad JWT header: not JSON"))
})
t.Run("Wrong algo", func(t *ftt.Test) {
_, err := verifyImpl(ctx, b64(`{"alg":"huh"}`)+".AAAA.AAAA", &certs)
assert.Loosely(t, err, should.ErrLike(`bad JWT: only RS256 alg is supported, not "huh"`))
})
t.Run("Missing key ID", func(t *ftt.Test) {
_, err := verifyImpl(ctx, b64(`{"alg":"RS256"}`)+".AAAA.AAAA", &certs)
assert.Loosely(t, err, should.ErrLike(`bad JWT: missing the signing key ID in the header`))
})
t.Run("Bad base64 signature", func(t *ftt.Test) {
_, err := verifyImpl(ctx, hdr()+".AAAA.!!!!", &certs)
assert.Loosely(t, err, should.ErrLike("bad JWT: can't base64 decode the signature"))
})
t.Run("Signature check error", func(t *ftt.Test) {
certs.err = fmt.Errorf("boom")
_, err := verifyImpl(ctx, hdr()+".AAAA."+b64("sig"), &certs)
assert.Loosely(t, err, should.ErrLike("bad JWT: bad signature: boom"))
})
t.Run("Bad payload", func(t *ftt.Test) {
_, err := verifyImpl(ctx, hdr()+".!!!!."+b64("sig"), &certs)
assert.Loosely(t, err, should.ErrLike("bad JWT payload: not base64"))
})
t.Run("Missing `google.compute_engine` section", func(t *ftt.Test) {
_, err := verifyImpl(ctx, hdr()+"."+b64(`{}`)+"."+b64("sig"), &certs)
assert.Loosely(t, err, should.ErrLike("no google.compute_engine in the GCE VM token, use 'full' format"))
})
})
}
func hdr() string {
return b64(`{"alg":"RS256","kid":"key id"}`)
}
func b64(s string) string {
return base64.RawURLEncoding.EncodeToString([]byte(s))
}
type mockedCerts struct {
calls []checkSignatureCall
err error
}
type checkSignatureCall struct {
key string
signed []byte
signature []byte
}
func (m *mockedCerts) CheckSignature(key string, signed, signature []byte) error {
m.calls = append(m.calls, checkSignatureCall{key, signed, signature})
return m.err
}