From b58314e0037787c8a392236ff983d1aa22b813c8 Mon Sep 17 00:00:00 2001 From: Hsiaoming Yang Date: Sat, 19 Apr 2025 15:40:12 +0900 Subject: [PATCH] fix: add "none" algorithm for JWS --- docs/changelog.rst | 1 + src/joserfc/errors.py | 4 ++++ src/joserfc/jws.py | 27 ++++++++++++++++++---- src/joserfc/rfc7518/jws_algs.py | 2 +- tests/jws/test_errors.py | 41 ++++++++++++++++++--------------- 5 files changed, 51 insertions(+), 24 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index f6e64d5e..e2dba864 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -17,6 +17,7 @@ Unreleased - Use "import as" to prioritize the modules for editors. - Added parameter ``encoder_cls`` for ``jwt.encode`` and ``decoder_cls`` for ``jwt.decode``. +- Added ``none`` algorithm for JWS. **Breaking changes**: diff --git a/src/joserfc/errors.py b/src/joserfc/errors.py index 8f274f30..ceb3eaba 100644 --- a/src/joserfc/errors.py +++ b/src/joserfc/errors.py @@ -72,6 +72,10 @@ class UnsupportedAlgorithmError(JoseError): error = "unsupported_algorithm" +class MissingKeyError(JoseError): + error = "missing_key" + + class MissingEncryptionError(JoseError): error = "missing_encryption" description = "Missing 'enc' value in header" diff --git a/src/joserfc/jws.py b/src/joserfc/jws.py index b819d0c1..23648e77 100644 --- a/src/joserfc/jws.py +++ b/src/joserfc/jws.py @@ -34,7 +34,7 @@ from .rfc7518.jws_algs import JWS_ALGORITHMS from .rfc8037.jws_eddsa import EdDSA from .rfc8812 import ES256K -from .errors import BadSignatureError +from .errors import BadSignatureError, MissingKeyError from .jwk import Key, KeyFlexible, KeySet, guess_key from .util import to_bytes from .registry import Header @@ -84,7 +84,7 @@ def register_algorithms() -> None: def serialize_compact( protected: Header, payload: bytes | str, - private_key: KeyFlexible, + private_key: KeyFlexible | None, algorithms: list[str] | None = None, registry: JWSRegistry | None = None, ) -> str: @@ -111,6 +111,15 @@ def serialize_compact( registry.check_header(protected) obj = CompactSignature(protected, to_bytes(payload)) alg: JWSAlgModel = registry.get_alg(protected["alg"]) + + # "none" algorithm requires no key + if alg.name == "none": + out = sign_compact(obj, alg, None) + return out.decode("utf-8") + + if private_key is None: + raise MissingKeyError() + key: Key = guess_key(private_key, obj, True) key.check_use("sig") alg.check_key_type(key) @@ -121,7 +130,7 @@ def serialize_compact( def validate_compact( obj: CompactSignature, - public_key: KeyFlexible, + public_key: KeyFlexible | None, algorithms: list[str] | None = None, registry: JWSRegistry | None = None, ) -> bool: @@ -138,16 +147,24 @@ def validate_compact( headers = obj.headers() registry.check_header(headers) + alg: JWSAlgModel = registry.get_alg(headers["alg"]) + + # "none" algorithm requires no key + if headers["alg"] == "none": + return verify_compact(obj, alg, None) + + if public_key is None: + raise MissingKeyError() + key: Key = guess_key(public_key, obj) key.check_use("sig") - alg: JWSAlgModel = registry.get_alg(headers["alg"]) alg.check_key_type(key) return verify_compact(obj, alg, key) def deserialize_compact( value: bytes | str, - public_key: KeyFlexible, + public_key: KeyFlexible | None, algorithms: list[str] | None = None, registry: JWSRegistry | None = None, ) -> CompactSignature: diff --git a/src/joserfc/rfc7518/jws_algs.py b/src/joserfc/rfc7518/jws_algs.py index b4f4c00b..7ed271a3 100644 --- a/src/joserfc/rfc7518/jws_algs.py +++ b/src/joserfc/rfc7518/jws_algs.py @@ -35,7 +35,7 @@ def sign(self, msg: bytes, key: t.Any) -> bytes: return b"" def verify(self, msg: bytes, sig: bytes, key: t.Any) -> bool: - return False + return sig == b"" class HMACAlgModel(JWSAlgModel): diff --git a/tests/jws/test_errors.py b/tests/jws/test_errors.py index a02d01fb..e22c8bf3 100644 --- a/tests/jws/test_errors.py +++ b/tests/jws/test_errors.py @@ -4,6 +4,7 @@ from joserfc.registry import HeaderParameter from joserfc.errors import ( BadSignatureError, + MissingKeyError, UnsupportedKeyUseError, UnsupportedKeyAlgorithmError, UnsupportedKeyOperationError, @@ -14,27 +15,33 @@ class TestJWSErrors(TestCase): + key = OctKey.import_key("secret") + def test_without_alg(self): - key = OctKey.import_key("secret") # missing alg - self.assertRaises(ValueError, jws.serialize_compact, {"kid": "123"}, "i", key) + self.assertRaises(ValueError, jws.serialize_compact, {"kid": "123"}, "i", self.key) + + def test_without_key(self): + self.assertRaises(MissingKeyError, jws.serialize_compact, {"alg": "HS256"}, "i", None) def test_none_alg(self): header = {"alg": "none"} - key = OctKey.import_key("secret") - text = jws.serialize_compact(header, "i", key, algorithms=["none"]) - self.assertRaises(BadSignatureError, jws.deserialize_compact, text, key, algorithms=["none"]) + text = jws.serialize_compact(header, "i", None, algorithms=["none"]) + obj = jws.deserialize_compact(text, None, algorithms=["none"]) + self.assertEqual(obj.payload, b"i") + # none alg has no signature + text += 'aQ' + self.assertRaises(BadSignatureError, jws.deserialize_compact, text, None, algorithms=["none"]) def test_header_invalid_type(self): # kid should be a string header = {"alg": "HS256", "kid": 123} - key = OctKey.import_key("secret") self.assertRaises( ValueError, jws.serialize_compact, header, "i", - key, + self.key, ) # jwk should be a dict @@ -44,7 +51,7 @@ def test_header_invalid_type(self): jws.serialize_compact, header, "i", - key, + self.key, ) # jku should be a URL @@ -54,7 +61,7 @@ def test_header_invalid_type(self): jws.serialize_compact, header, "i", - key, + self.key, ) # x5c should be a chain of string @@ -64,7 +71,7 @@ def test_header_invalid_type(self): jws.serialize_compact, header, "i", - key, + self.key, ) header = {"alg": "HS256", "x5c": [1, 2]} self.assertRaises( @@ -72,42 +79,40 @@ def test_header_invalid_type(self): jws.serialize_compact, header, "i", - key, + self.key, ) def test_crit_header(self): header = {"alg": "HS256", "crit": ["kid"]} - key = OctKey.import_key("secret") # missing kid header self.assertRaises( ValueError, jws.serialize_compact, header, "i", - key, + self.key, ) header = {"alg": "HS256", "kid": "1", "crit": ["kid"]} - jws.serialize_compact(header, "i", key) + jws.serialize_compact(header, "i", self.key) def test_extra_header(self): header = {"alg": "HS256", "extra": "hi"} - key = OctKey.import_key("secret") self.assertRaises( ValueError, jws.serialize_compact, header, "i", - key, + self.key, ) # bypass extra header registry = jws.JWSRegistry(strict_check_header=False) - jws.serialize_compact(header, "i", key, registry=registry) + jws.serialize_compact(header, "i", self.key, registry=registry) # or use a header registry extra_header = {"extra": HeaderParameter("Extra header", "str", False)} registry = jws.JWSRegistry(header_registry=extra_header) - jws.serialize_compact(header, "i", key, registry=registry) + jws.serialize_compact(header, "i", self.key, registry=registry) def test_rsa_invalid_signature(self): key1 = RSAKey.generate_key()