From 341bb2895e266ac4a728cf7b5b80888531642b17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Capon?= <46624375+FrancoisCapon@users.noreply.github.com> Date: Fri, 2 May 2025 13:38:03 +0200 Subject: [PATCH 1/4] feat: add non canonical base64 detection --- src/joserfc/util.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/joserfc/util.py b/src/joserfc/util.py index 4221031e..0394e0a0 100644 --- a/src/joserfc/util.py +++ b/src/joserfc/util.py @@ -4,7 +4,7 @@ import struct import binascii import json - +from .errors import DecodeError def to_bytes(x: Any, charset: str = "utf-8", errors: str = "strict") -> bytes: if isinstance(x, bytes): @@ -21,10 +21,23 @@ def to_str(x: bytes | str, charset: str = "utf-8") -> str: return x.decode(charset) return x +def __is_urlsafe_b64_encoding_non_canonical(s: bytes) -> bool: + # https://github.com/FrancoisCapon/Base64SteganographyTools/blob/main/tools/b64_print_regular_characters.sh + p = len(s) % 4 # padding? + if p == 0: + return False + p = 4 - p # number of padding characters + if p == 2 and s[-1] in b"AQgw": + return False + if p == 1 and s[-1] in b"AEIMQUYcgkosw048": + return False + return True def urlsafe_b64decode(s: bytes) -> bytes: if b"+" in s or b"/" in s: raise binascii.Error + if __is_urlsafe_b64_encoding_non_canonical(s): + raise DecodeError s += b"=" * (-len(s) % 4) return base64.b64decode(s, b"-_", validate=True) From 5c1a580ae45a26f0774af56702c1ef58b3644ee4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Capon?= <46624375+FrancoisCapon@users.noreply.github.com> Date: Fri, 2 May 2025 13:40:55 +0200 Subject: [PATCH 2/4] test: add util non canonical base64 detection --- tests/test_util.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_util.py b/tests/test_util.py index 42eff1a9..c834ff88 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -1,7 +1,7 @@ import binascii from unittest import TestCase from joserfc import util - +from joserfc import errors class TestUtil(TestCase): def test_to_bytes(self): @@ -23,3 +23,7 @@ def test_json_b64encode(self): def test_urlsafe_b64decode(self): self.assertEqual(util.urlsafe_b64decode(b"_foo123-"), b"\xfd\xfa(\xd7m\xfe") self.assertRaises(binascii.Error, util.urlsafe_b64decode, b"+foo123/") + for c in "RSTUVWXYZabdef": # A -> QQ== + self.assertRaises(errors.DecodeError, util.urlsafe_b64decode, b"Q" + c.encode()) + for c in "FGH": # AAAAAAAAAAAAAA -> QUFBQUFBQUFBQUFBQUE= + self.assertRaises(errors.DecodeError, util.urlsafe_b64decode, b"QUFBQUFBQUFBQUFBQU" + c.encode()) From 76a1f9fc729534bdfa827b7e74ce75c76b300788 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Capon?= <46624375+FrancoisCapon@users.noreply.github.com> Date: Fri, 2 May 2025 13:44:06 +0200 Subject: [PATCH 3/4] test: non canonical signature --- tests/jws/test_compact.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/jws/test_compact.py b/tests/jws/test_compact.py index 189fb8ea..322b556c 100644 --- a/tests/jws/test_compact.py +++ b/tests/jws/test_compact.py @@ -73,3 +73,12 @@ def test_strict_check_header(self): registry = JWSRegistry(strict_check_header=False) serialize_compact(header, b"hi", key, registry=registry) + + def test_non_canonical_signature_encoding(self): + text = "eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiYWRtaW4ifQ.VI29GgHzuh2xfF0bkRYvZIsSuQnbTXSIvuRyt7RDrwo"[:-1] + "p" + self.assertRaises( + DecodeError, + jws.deserialize_compact, + text, + self.key # no matter + ) From 420c0166d70bc066da76c2534b5bc93f55932b29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Capon?= <46624375+FrancoisCapon@users.noreply.github.com> Date: Fri, 2 May 2025 13:51:21 +0200 Subject: [PATCH 4/4] fix: non canonical signature test --- tests/jws/test_compact.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/jws/test_compact.py b/tests/jws/test_compact.py index 322b556c..7875d9a8 100644 --- a/tests/jws/test_compact.py +++ b/tests/jws/test_compact.py @@ -78,7 +78,7 @@ def test_non_canonical_signature_encoding(self): text = "eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiYWRtaW4ifQ.VI29GgHzuh2xfF0bkRYvZIsSuQnbTXSIvuRyt7RDrwo"[:-1] + "p" self.assertRaises( DecodeError, - jws.deserialize_compact, + deserialize_compact, text, - self.key # no matter + OctKey.import_key("secret") )