xref: /aosp_15_r20/external/tink/go/jwt/jwt_encoding_test.go (revision e7b1675dde1b92d52ec075b0a92829627f2c52a5)
1// Copyright 2022 Google LLC
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////////////////////////////////////////////////////////////////////////////////
16
17package jwt
18
19import (
20	"strings"
21	"testing"
22	"time"
23
24	"github.com/google/go-cmp/cmp"
25
26	tpb "github.com/google/tink/go/proto/tink_go_proto"
27)
28
29func TestKIDForNonTinkKeysIsNil(t *testing.T) {
30	for _, op := range []tpb.OutputPrefixType{
31		tpb.OutputPrefixType_LEGACY,
32		tpb.OutputPrefixType_RAW,
33		tpb.OutputPrefixType_CRUNCHY} {
34		if kid := keyID(1234, op); kid != nil {
35			t.Errorf("keyID(1234, %q) = %q, want nil", op, *kid)
36		}
37	}
38}
39
40func TestKeyIDForTinkKey(t *testing.T) {
41	want := "GsapRA"
42	kid := keyID(0x1ac6a944, tpb.OutputPrefixType_TINK)
43	if kid == nil {
44		t.Errorf("KeyID(0x1ac6a944, %q) = nil, want %q", tpb.OutputPrefixType_TINK, want)
45	}
46	if kid != nil && !cmp.Equal(*kid, want) {
47		t.Errorf("KeyID(0x1ac6a944, %q) = %q, want %q", tpb.OutputPrefixType_TINK, *kid, want)
48	}
49}
50
51type payloadTestCase struct {
52	tag       string
53	rawJWT    *RawJWT
54	opts      *RawJWTOptions
55	tinkKID   *string
56	customKID *string
57	algorithm string
58}
59
60func refString(a string) *string {
61	return &a
62}
63
64func refTime(ts int64) *time.Time {
65	t := time.Unix(ts, 0)
66	return &t
67}
68
69func TestBase64Encode(t *testing.T) {
70	// Examples from: https://datatracker.ietf.org/doc/html/rfc7515#appendix-A.1.1
71	want := "eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ"
72	payload := []byte{123, 34, 105, 115, 115, 34, 58, 34, 106, 111, 101, 34, 44, 13, 10, 32, 34, 101, 120, 112, 34, 58, 49, 51, 48, 48, 56, 49, 57, 51, 56,
73		48, 44, 13, 10, 32, 34, 104, 116, 116, 112, 58, 47, 47, 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, 109, 47, 105, 115, 95, 114, 111, 111, 116, 34, 58, 116, 114, 117, 101, 125}
74	got := base64Encode(payload)
75	if got != want {
76		t.Errorf("base64Encode() got %q want %q", got, want)
77	}
78}
79
80func TestBase64Decode(t *testing.T) {
81	// Examples from: https://datatracker.ietf.org/doc/html/rfc7515#appendix-A.1.1
82	want := []byte{123, 34, 105, 115, 115, 34, 58, 34, 106, 111, 101, 34, 44, 13, 10, 32, 34, 101, 120, 112, 34, 58, 49, 51, 48, 48, 56, 49, 57, 51, 56,
83		48, 44, 13, 10, 32, 34, 104, 116, 116, 112, 58, 47, 47, 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, 109, 47, 105, 115, 95, 114, 111, 111, 116, 34, 58, 116, 114, 117, 101, 125}
84	got, err := base64Decode("eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ")
85	if err != nil {
86		t.Errorf("base64Decode() err = %v, want nil", err)
87	}
88	if !cmp.Equal(got, want) {
89		t.Errorf("base64Decode() got %q, want %q", got, want)
90	}
91}
92
93func TestInvalidCharactersFailBase64Decode(t *testing.T) {
94	if _, err := base64Decode("iLA0KIC&hD"); err == nil {
95		t.Errorf("base64Decode() err = nil, want error")
96	}
97}
98
99func TestEncodeStaticHeaderWithPayloadIssuerTokenForSigning(t *testing.T) {
100	opts := &RawJWTOptions{
101		WithoutExpiration: true,
102		Issuer:            refString("tink-issuer"),
103	}
104	// Header 'RS256' alg from: https://datatracker.ietf.org/doc/html/rfc7515#appendix-A.2.1
105	// Payload: `{"iss":"tink-issuer"}`
106	wantUnsigned := "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJ0aW5rLWlzc3VlciJ9"
107	rawJWT, err := NewRawJWT(opts)
108	if err != nil {
109		t.Fatalf("generating valid RawJWT: %v", err)
110	}
111	unsigned, err := createUnsigned(rawJWT, "RS256", nil, nil)
112	if err != nil {
113		t.Errorf("createUnsigned() err = %v, want nil", err)
114	}
115
116	if unsigned != wantUnsigned {
117		t.Errorf("got unsigned %q, want %q", unsigned, wantUnsigned)
118	}
119}
120
121func TestEncodeHeaderWithHeaderFieldsAndEmptyPayload(t *testing.T) {
122	type testCase struct {
123		tag                 string
124		opts                *RawJWTOptions
125		wantHeaderSubstring string
126		customKID           *string
127		tinkKID             *string
128	}
129	for _, tc := range []testCase{
130		{
131			tag: "type header",
132			opts: &RawJWTOptions{
133				WithoutExpiration: true,
134				TypeHeader:        refString("JWT"),
135			},
136			wantHeaderSubstring: `"typ":"JWT"`,
137		},
138		{
139			tag: "custom kid",
140			opts: &RawJWTOptions{
141				WithoutExpiration: true,
142			},
143			customKID:           refString("custom"),
144			wantHeaderSubstring: `"kid":"custom"`,
145		},
146		{
147			tag: "tink kid",
148			opts: &RawJWTOptions{
149				WithoutExpiration: true,
150			},
151			tinkKID:             refString("tink"),
152			wantHeaderSubstring: `"kid":"tink"`,
153		},
154	} {
155		rawJWT, err := NewRawJWT(tc.opts)
156		if err != nil {
157			t.Fatalf("generating valid RawJWT: %v", err)
158		}
159		unsigned, err := createUnsigned(rawJWT, "RS256", tc.tinkKID, tc.customKID)
160		if err != nil {
161			t.Errorf("createUnsigned() err = %v, want nil", err)
162		}
163		token := strings.Split(unsigned, ".")
164		if len(token) != 2 {
165			t.Errorf("token[0] not encoded in compact serialization format")
166		}
167		header, err := base64Decode(token[0])
168		if err != nil {
169			t.Errorf("base64Decode(token[0] = %q)", token[0])
170		}
171		if !strings.Contains(string(header), tc.wantHeaderSubstring) {
172			t.Errorf("header %q, doesn't contain: %q", string(header), tc.wantHeaderSubstring)
173		}
174		wantPayload := "e30" // `{}`
175		if string(token[1]) != wantPayload {
176			t.Errorf("token[1] = %q, want %q", token[1], wantPayload)
177		}
178	}
179}
180
181func TestCreateUnsignedWithNilRawJWTFails(t *testing.T) {
182	if _, err := createUnsigned(nil, "HS256", nil, nil); err == nil {
183		t.Errorf("createUnsigned(rawJWT = nil) err = nil, want error")
184	}
185}
186
187func TestCreateUnsignedCustomAndTinkKIDFail(t *testing.T) {
188	rawJWT, err := NewRawJWT(&RawJWTOptions{WithoutExpiration: true})
189	if err != nil {
190		t.Fatalf("generating valid RawJWT: %v", err)
191	}
192	if _, err := createUnsigned(rawJWT, "HS256", refString("123"), refString("456")); err == nil {
193		t.Errorf("createUnsigned(tinkKID = 456, customKID = 123) err = nil, want error")
194	}
195}
196
197func TestCombineTokenAndSignature(t *testing.T) {
198	// https://datatracker.ietf.org/doc/html/rfc7515#appendix-A.2.1
199	payload := "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ"
200	signature := []byte{116, 24, 223, 180, 151, 153, 224, 37, 79, 250, 96, 125, 216, 173, 187, 186, 22, 212, 37, 77, 105, 214, 191, 240, 91, 88, 5, 88, 83, 132, 141, 121}
201	token := combineUnsignedAndSignature(payload, signature)
202	want := "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
203	if !cmp.Equal(token, want) {
204		t.Errorf("combineUnsignedAndSignature(%q, %q) = %q, want %q", payload, signature, token, want)
205	}
206}
207
208func TestSplitSignedCompactInvalidInputs(t *testing.T) {
209	type testCases struct {
210		tag   string
211		token string
212	}
213	for _, tc := range []testCases{
214		{
215			tag:   "empty payload",
216			token: "",
217		},
218		{
219			tag:   "not in compact serialization missing separators",
220			token: "Zm9vYmFyIVRpbms",
221		},
222		{
223			tag:   "not in compact serialization additional separators",
224			token: "Zm9vYmFyIVRpbms.Zm9vYmFyGVRpbms.Zm9vYmFyIVRpbms.Zm9vYmFyINRpbms",
225		},
226		{
227			tag:   "non web safe URL encoding character",
228			token: "Zm9vYmFyIVRpbms.m9vYmFy.Zm&mFyIVRpbms",
229		},
230		{
231			tag:   "no content",
232			token: ".Zm9vYmFyIVRpbms",
233		},
234		{
235			tag:   "no signature",
236			token: "Zm9vYmFyIVRpbms.Zm9vYmFyIVRpbms.",
237		},
238		{
239			tag:   "no signature and no content",
240			token: "..",
241		},
242	} {
243		t.Run(tc.tag, func(t *testing.T) {
244			if _, _, err := splitSignedCompact(tc.token); err == nil {
245				t.Errorf("splitSignedCompact(%q) err = nil, want error", tc.token)
246			}
247		})
248	}
249}
250
251func TestSplitSignedCompact(t *testing.T) {
252	// signed token from: https://datatracker.ietf.org/doc/html/rfc7515#appendix-A.1.1
253	signedToken := "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
254	wantSig := []byte{116, 24, 223, 180, 151, 153, 224, 37, 79, 250, 96, 125, 216, 173, 187, 186, 22, 212, 37, 77, 105, 214, 191, 240, 91, 88, 5, 88, 83, 132, 141, 121}
255	wantToken := "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ"
256	sig, token, err := splitSignedCompact(signedToken)
257	if err != nil {
258		t.Errorf("splitSignedCompact(%q) err = %v, want nil", signedToken, err)
259	}
260	if !cmp.Equal(sig, wantSig) {
261		t.Errorf("splitSignedCompact() sig = %q, want %q", sig, wantSig)
262	}
263	if token != wantToken {
264		t.Errorf("splitSignedCompact() token = %q, want %q", token, wantToken)
265	}
266}
267
268func TestDecodeValidateInvalidHeaderFailures(t *testing.T) {
269	type testCases struct {
270		tag       string
271		header    string
272		alg       string
273		tinkKID   *string
274		customKID *string
275	}
276	for _, tc := range []testCases{
277		{
278			tag:    "invalid JSON header",
279			header: `JiVeQCo`,
280		},
281		{
282			tag:    "contains line feed",
283			header: "eyJ0eXAiOiJKV1Qi\nLA0KICJhbGciOiJIUzI1NiJ9",
284			alg:    "HS256",
285		},
286		{
287			tag:    "header contains no fields",
288			header: base64Encode([]byte(`{}`)),
289		},
290		{
291			tag:    "type header not a string",
292			header: base64Encode([]byte(`{"alg":"HS256", "typ":5}`)),
293			alg:    "HS256",
294		},
295		{
296			tag:    "wrong algorithm",
297			header: base64Encode([]byte(`{"alg":"HS256"}`)),
298			alg:    "HS512",
299		},
300		{
301			tag:       "specyfing custom and tink kid",
302			header:    base64Encode([]byte(`{"alg":"HS256", "kid":"tink"}`)),
303			alg:       "HS256",
304			tinkKID:   refString("tink"),
305			customKID: refString("custom"),
306		},
307		{
308			tag:       "invalid custom kid",
309			header:    base64Encode([]byte(`{"alg":"HS256", "kid":"custom"}`)),
310			customKID: refString("notCustom"),
311			alg:       "HS256",
312		},
313		{
314			tag:     "invalid tink kid",
315			header:  base64Encode([]byte(`{"alg":"HS256", "kid":"tink"}`)),
316			tinkKID: refString("notTink"),
317			alg:     "HS256",
318		},
319		{
320			tag:     "specify tink kid and token without kig",
321			header:  base64Encode([]byte(`{"alg":"HS256"}`)),
322			tinkKID: refString("notTink"),
323			alg:     "HS256",
324		},
325		{
326			tag:    "crit header",
327			header: base64Encode([]byte(`{"alg":"HS256", "crit":"fooBar"}`)),
328			alg:    "HS256",
329		},
330		{
331			tag:    "no compact serialization",
332			header: "asd.asd",
333		},
334		{
335			tag:    "invalid UTF16 encoding",
336			header: base64Encode([]byte(`{"alg":"HS256", "typ":"\uD834"}`)),
337		},
338	} {
339		t.Run(tc.tag, func(t *testing.T) {
340			if _, err := decodeUnsignedTokenAndValidateHeader(dotConcat(tc.header, base64Encode([]byte("{}"))), tc.alg, tc.tinkKID, tc.customKID); err == nil {
341				t.Errorf("decodeUnsignedTokenAndValidateHeader() err = nil, want error")
342			}
343		})
344	}
345}
346
347func TestDecodeValidateKIDHeader(t *testing.T) {
348	type testCases struct {
349		tag       string
350		header    string
351		tinkKID   *string
352		customKID *string
353	}
354	for _, tc := range []testCases{
355		{
356			tag:    "not kid header field",
357			header: base64Encode([]byte(`{"alg":"HS256"}`)),
358		},
359		{
360			tag:       "validates custom kid",
361			header:    base64Encode([]byte(`{"alg":"HS256", "kid":"custom"}`)),
362			customKID: refString("custom"),
363		},
364		{
365			tag:     "validates tink kid",
366			header:  base64Encode([]byte(`{"alg":"HS256", "kid":"tink"}`)),
367			tinkKID: refString("tink"),
368		},
369		{
370			tag:    "ignores kid if exists and tink kid isn't specified",
371			header: base64Encode([]byte(`{"alg":"HS256", "kid":"random"}`)),
372		},
373		{
374			tag:    "unkown headers are accepted",
375			header: base64Encode([]byte(`{"alg":"HS256","unknown":"header"}`)),
376		},
377	} {
378		t.Run(tc.tag, func(t *testing.T) {
379			_, err := decodeUnsignedTokenAndValidateHeader(dotConcat(tc.header, base64Encode([]byte("{}"))), "HS256", tc.tinkKID, tc.customKID)
380			if err != nil {
381				t.Errorf("decodeUnsignedTokenAndValidateHeader() err = %v, want nil", err)
382			}
383		})
384	}
385}
386
387func TestDecodeVerifyTokenFixedValues(t *testing.T) {
388	header := "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9"                                                        // Header example from https://tools.ietf.org/html/rfc7519#section-3.1
389	payload := "eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ" // Payload example from https://tools.ietf.org/html/rfc7519#section-3.1
390	rawJWT, err := decodeUnsignedTokenAndValidateHeader(dotConcat(header, payload), "HS256", nil, nil)
391	if err != nil {
392		t.Errorf("decodeUnsignedTokenAndValidateHeader() err = %v, want nil", err)
393	}
394	iss, err := rawJWT.Issuer()
395	if err != nil {
396		t.Errorf("rawJWT.Issuer() err = %v, want nil", err)
397	}
398	if iss != "joe" {
399		t.Errorf("rawJWT.Issuer() = %q, want joe", iss)
400	}
401	exp, err := rawJWT.ExpiresAt()
402	if err != nil {
403		t.Errorf("rawJWT.ExpiresAt() err = %v, want nil", err)
404	}
405	wantExp := time.Unix(1300819380, 0)
406	if !exp.Equal(wantExp) {
407		t.Errorf("rawJWT.ExpiresAt() = %q, want %q", exp, wantExp)
408	}
409	cc, err := rawJWT.BooleanClaim("http://example.com/is_root")
410	if err != nil {
411		t.Errorf("rawJWT.BooleanClaim('http://example.com/is_root') err = %v want nil", err)
412	}
413	if cc != true {
414		t.Errorf("rawJWT.BooleanClaim('http://example.com/is_root') = %v, want true", cc)
415	}
416}
417
418func TestDecodeVerifyTokenPaylodWithInvalidEndcoding(t *testing.T) {
419	if _, err := decodeUnsignedTokenAndValidateHeader(dotConcat(base64Encode([]byte(`{"alg":"HS256"}`)), "_aSL&%"), "HS256", nil, nil); err == nil {
420		t.Errorf("decodeUnsignedTokenAndValidateHeader() err = nil, want error")
421	}
422}
423