Skip to content

Commit 487c918

Browse files
authored
Merge pull request #79 from bpicolo/fix_base64_padding_for_kconfig
Fix base64 padding for kube config
2 parents 1d5231c + 72a02cc commit 487c918

File tree

2 files changed

+114
-8
lines changed

2 files changed

+114
-8
lines changed

config/kube_config.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -277,18 +277,31 @@ def _load_oid_token(self, provider):
277277
if 'config' not in provider:
278278
return
279279

280-
parts = provider['config']['id-token'].split('.')
280+
reserved_characters = frozenset(["=", "+", "/"])
281+
token = provider['config']['id-token']
281282

283+
if any(char in token for char in reserved_characters):
284+
# Invalid jwt, as it contains url-unsafe chars
285+
return
286+
287+
parts = token.split('.')
282288
if len(parts) != 3: # Not a valid JWT
283-
return None
289+
return
290+
291+
padding = (4 - len(parts[1]) % 4) * '='
292+
if len(padding) == 3:
293+
# According to spec, 3 padding characters cannot occur
294+
# in a valid jwt
295+
# https://tools.ietf.org/html/rfc7515#appendix-C
296+
return
284297

285298
if PY3:
286299
jwt_attributes = json.loads(
287-
base64.b64decode(parts[1]).decode('utf-8')
300+
base64.b64decode(parts[1] + padding).decode('utf-8')
288301
)
289302
else:
290303
jwt_attributes = json.loads(
291-
base64.b64decode(parts[1] + "==")
304+
base64.b64decode(parts[1] + padding)
292305
)
293306

294307
expire = jwt_attributes.get('exp')

config/kube_config_test.py

Lines changed: 97 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ def _base64(string):
5050
return base64.standard_b64encode(string.encode()).decode()
5151

5252

53+
def _urlsafe_unpadded_b64encode(string):
54+
return base64.urlsafe_b64encode(string.encode()).decode().rstrip('=')
55+
56+
5357
def _format_expiry_datetime(dt):
5458
return dt.strftime(EXPIRY_DATETIME_FORMAT)
5559

@@ -97,12 +101,33 @@ def _raise_exception(st):
97101

98102
TEST_OIDC_TOKEN = "test-oidc-token"
99103
TEST_OIDC_INFO = "{\"name\": \"test\"}"
100-
TEST_OIDC_BASE = _base64(TEST_OIDC_TOKEN) + "." + _base64(TEST_OIDC_INFO)
101-
TEST_OIDC_LOGIN = TEST_OIDC_BASE + "." + TEST_CLIENT_CERT_BASE64
104+
TEST_OIDC_BASE = ".".join([
105+
_urlsafe_unpadded_b64encode(TEST_OIDC_TOKEN),
106+
_urlsafe_unpadded_b64encode(TEST_OIDC_INFO)
107+
])
108+
TEST_OIDC_LOGIN = ".".join([
109+
TEST_OIDC_BASE,
110+
_urlsafe_unpadded_b64encode(TEST_CLIENT_CERT_BASE64)
111+
])
102112
TEST_OIDC_TOKEN = "Bearer %s" % TEST_OIDC_LOGIN
103113
TEST_OIDC_EXP = "{\"name\": \"test\",\"exp\": 536457600}"
104-
TEST_OIDC_EXP_BASE = _base64(TEST_OIDC_TOKEN) + "." + _base64(TEST_OIDC_EXP)
105-
TEST_OIDC_EXPIRED_LOGIN = TEST_OIDC_EXP_BASE + "." + TEST_CLIENT_CERT_BASE64
114+
TEST_OIDC_EXP_BASE = _urlsafe_unpadded_b64encode(
115+
TEST_OIDC_TOKEN) + "." + _urlsafe_unpadded_b64encode(TEST_OIDC_EXP)
116+
TEST_OIDC_EXPIRED_LOGIN = ".".join([
117+
TEST_OIDC_EXP_BASE,
118+
_urlsafe_unpadded_b64encode(TEST_CLIENT_CERT)
119+
])
120+
TEST_OIDC_CONTAINS_RESERVED_CHARACTERS = ".".join([
121+
_urlsafe_unpadded_b64encode(TEST_OIDC_TOKEN),
122+
_urlsafe_unpadded_b64encode(TEST_OIDC_INFO).replace("a", "+"),
123+
_urlsafe_unpadded_b64encode(TEST_CLIENT_CERT)
124+
])
125+
TEST_OIDC_INVALID_PADDING_LENGTH = ".".join([
126+
_urlsafe_unpadded_b64encode(TEST_OIDC_TOKEN),
127+
"aaaaa",
128+
_urlsafe_unpadded_b64encode(TEST_CLIENT_CERT)
129+
])
130+
106131
TEST_OIDC_CA = _base64(TEST_CERTIFICATE_AUTH)
107132

108133

@@ -409,6 +434,22 @@ class TestKubeConfigLoader(BaseTestCase):
409434
"user": "expired_oidc_nocert"
410435
}
411436
},
437+
{
438+
"name": "oidc_contains_reserved_character",
439+
"context": {
440+
"cluster": "default",
441+
"user": "oidc_contains_reserved_character"
442+
443+
}
444+
},
445+
{
446+
"name": "oidc_invalid_padding_length",
447+
"context": {
448+
"cluster": "default",
449+
"user": "oidc_invalid_padding_length"
450+
451+
}
452+
},
412453
{
413454
"name": "user_pass",
414455
"context": {
@@ -595,6 +636,38 @@ class TestKubeConfigLoader(BaseTestCase):
595636
}
596637
}
597638
},
639+
{
640+
"name": "oidc_contains_reserved_character",
641+
"user": {
642+
"auth-provider": {
643+
"name": "oidc",
644+
"config": {
645+
"client-id": "tectonic-kubectl",
646+
"client-secret": "FAKE_SECRET",
647+
"id-token": TEST_OIDC_CONTAINS_RESERVED_CHARACTERS,
648+
"idp-issuer-url": "https://example.org/identity",
649+
"refresh-token":
650+
"lucWJjEhlxZW01cXI3YmVlcYnpxNGhzk"
651+
}
652+
}
653+
}
654+
},
655+
{
656+
"name": "oidc_invalid_padding_length",
657+
"user": {
658+
"auth-provider": {
659+
"name": "oidc",
660+
"config": {
661+
"client-id": "tectonic-kubectl",
662+
"client-secret": "FAKE_SECRET",
663+
"id-token": TEST_OIDC_INVALID_PADDING_LENGTH,
664+
"idp-issuer-url": "https://example.org/identity",
665+
"refresh-token":
666+
"lucWJjEhlxZW01cXI3YmVlcYnpxNGhzk"
667+
}
668+
}
669+
}
670+
},
598671
{
599672
"name": "user_pass",
600673
"user": {
@@ -793,6 +866,26 @@ def test_oidc_with_refresh_nocert(
793866
self.assertTrue(loader._load_auth_provider_token())
794867
self.assertEqual("Bearer abc123", loader.token)
795868

869+
def test_oidc_fails_if_contains_reserved_chars(self):
870+
loader = KubeConfigLoader(
871+
config_dict=self.TEST_KUBE_CONFIG,
872+
active_context="oidc_contains_reserved_character",
873+
)
874+
self.assertEqual(
875+
loader._load_oid_token("oidc_contains_reserved_character"),
876+
None,
877+
)
878+
879+
def test_oidc_fails_if_invalid_padding_length(self):
880+
loader = KubeConfigLoader(
881+
config_dict=self.TEST_KUBE_CONFIG,
882+
active_context="oidc_invalid_padding_length",
883+
)
884+
self.assertEqual(
885+
loader._load_oid_token("oidc_invalid_padding_length"),
886+
None,
887+
)
888+
796889
def test_user_pass(self):
797890
expected = FakeConfig(host=TEST_HOST, token=TEST_BASIC_TOKEN)
798891
actual = FakeConfig()

0 commit comments

Comments
 (0)