Skip to content

Commit 12f30c8

Browse files
Fix for CVE-2024-33663 (forbid public key for HMAC) (mpdavis#369)
1 parent 638d047 commit 12f30c8

File tree

4 files changed

+100
-18
lines changed

4 files changed

+100
-18
lines changed

jose/backends/cryptography_backend.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,15 @@
1616

1717
from ..constants import ALGORITHMS
1818
from ..exceptions import JWEError, JWKError
19-
from ..utils import base64_to_long, base64url_decode, base64url_encode, ensure_binary, long_to_base64
19+
from ..utils import (
20+
base64_to_long,
21+
base64url_decode,
22+
base64url_encode,
23+
ensure_binary,
24+
is_pem_format,
25+
is_ssh_key,
26+
long_to_base64,
27+
)
2028
from .base import Key
2129

2230
_binding = None
@@ -555,14 +563,7 @@ def __init__(self, key, algorithm):
555563
if isinstance(key, str):
556564
key = key.encode("utf-8")
557565

558-
invalid_strings = [
559-
b"-----BEGIN PUBLIC KEY-----",
560-
b"-----BEGIN RSA PUBLIC KEY-----",
561-
b"-----BEGIN CERTIFICATE-----",
562-
b"ssh-rsa",
563-
]
564-
565-
if any(string_value in key for string_value in invalid_strings):
566+
if is_pem_format(key) or is_ssh_key(key):
566567
raise JWKError(
567568
"The specified key is an asymmetric key or x509 certificate and"
568569
" should not be used as an HMAC secret."

jose/backends/native.py

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from jose.backends.base import Key
66
from jose.constants import ALGORITHMS
77
from jose.exceptions import JWKError
8-
from jose.utils import base64url_decode, base64url_encode
8+
from jose.utils import base64url_decode, base64url_encode, is_pem_format, is_ssh_key
99

1010

1111
def get_random_bytes(num_bytes):
@@ -36,14 +36,7 @@ def __init__(self, key, algorithm):
3636
if isinstance(key, str):
3737
key = key.encode("utf-8")
3838

39-
invalid_strings = [
40-
b"-----BEGIN PUBLIC KEY-----",
41-
b"-----BEGIN RSA PUBLIC KEY-----",
42-
b"-----BEGIN CERTIFICATE-----",
43-
b"ssh-rsa",
44-
]
45-
46-
if any(string_value in key for string_value in invalid_strings):
39+
if is_pem_format(key) or is_ssh_key(key):
4740
raise JWKError(
4841
"The specified key is an asymmetric key or x509 certificate and"
4942
" should not be used as an HMAC secret."

jose/utils.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import base64
2+
import re
23
import struct
34

45
# Piggyback of the backends implementation of the function that converts a long
@@ -105,3 +106,60 @@ def ensure_binary(s):
105106
if isinstance(s, str):
106107
return s.encode("utf-8", "strict")
107108
raise TypeError(f"not expecting type '{type(s)}'")
109+
110+
111+
# The following was copied from PyJWT:
112+
# https://github.com/jpadilla/pyjwt/commit/9c528670c455b8d948aff95ed50e22940d1ad3fc
113+
# Based on:
114+
# https://github.com/hynek/pem/blob/7ad94db26b0bc21d10953f5dbad3acfdfacf57aa/src/pem/_core.py#L224-L252
115+
_PEMS = {
116+
b"CERTIFICATE",
117+
b"TRUSTED CERTIFICATE",
118+
b"PRIVATE KEY",
119+
b"PUBLIC KEY",
120+
b"ENCRYPTED PRIVATE KEY",
121+
b"OPENSSH PRIVATE KEY",
122+
b"DSA PRIVATE KEY",
123+
b"RSA PRIVATE KEY",
124+
b"RSA PUBLIC KEY",
125+
b"EC PRIVATE KEY",
126+
b"DH PARAMETERS",
127+
b"NEW CERTIFICATE REQUEST",
128+
b"CERTIFICATE REQUEST",
129+
b"SSH2 PUBLIC KEY",
130+
b"SSH2 ENCRYPTED PRIVATE KEY",
131+
b"X509 CRL",
132+
}
133+
_PEM_RE = re.compile(
134+
b"----[- ]BEGIN (" + b"|".join(re.escape(pem) for pem in _PEMS) + b")[- ]----",
135+
)
136+
137+
138+
def is_pem_format(key: bytes) -> bool:
139+
return bool(_PEM_RE.search(key))
140+
141+
142+
# Based on
143+
# https://github.com/pyca/cryptography/blob/bcb70852d577b3f490f015378c75cba74986297b
144+
# /src/cryptography/hazmat/primitives/serialization/ssh.py#L40-L46
145+
_CERT_SUFFIX = b"[email protected]"
146+
_SSH_PUBKEY_RC = re.compile(rb"\A(\S+)[ \t]+(\S+)")
147+
_SSH_KEY_FORMATS = [
148+
b"ssh-ed25519",
149+
b"ssh-rsa",
150+
b"ssh-dss",
151+
b"ecdsa-sha2-nistp256",
152+
b"ecdsa-sha2-nistp384",
153+
b"ecdsa-sha2-nistp521",
154+
]
155+
156+
157+
def is_ssh_key(key: bytes) -> bool:
158+
if any(string_value in key for string_value in _SSH_KEY_FORMATS):
159+
return True
160+
ssh_pubkey_match = _SSH_PUBKEY_RC.match(key)
161+
if ssh_pubkey_match:
162+
key_type = ssh_pubkey_match.group(1)
163+
if _CERT_SUFFIX == key_type[-len(_CERT_SUFFIX) :]:
164+
return True
165+
return False

tests/algorithms/test_EC.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import base64
12
import json
23
import re
34

5+
from jose import jwt
46
from jose.backends import ECKey
57
from jose.constants import ALGORITHMS
68
from jose.exceptions import JOSEError, JWKError
@@ -14,9 +16,11 @@
1416

1517
try:
1618
from cryptography.hazmat.backends import default_backend as CryptographyBackend
19+
from cryptography.hazmat.primitives import hashes, hmac, serialization
1720
from cryptography.hazmat.primitives.asymmetric import ec as CryptographyEc
1821

1922
from jose.backends.cryptography_backend import CryptographyECKey
23+
2024
except ImportError:
2125
CryptographyECKey = CryptographyEc = CryptographyBackend = None
2226

@@ -223,3 +227,29 @@ def test_to_dict(self):
223227
key = ECKey(private_key, ALGORITHMS.ES256)
224228
self.assert_parameters(key.to_dict(), private=True)
225229
self.assert_parameters(key.public_key().to_dict(), private=False)
230+
231+
232+
@pytest.mark.cryptography
233+
@pytest.mark.skipif(CryptographyECKey is None, reason="pyca/cryptography backend not available")
234+
def test_incorrect_public_key_hmac_signing():
235+
def b64(x):
236+
return base64.urlsafe_b64encode(x).replace(b"=", b"")
237+
238+
KEY = CryptographyEc.generate_private_key(CryptographyEc.SECP256R1)
239+
PUBKEY = KEY.public_key().public_bytes(
240+
encoding=serialization.Encoding.OpenSSH,
241+
format=serialization.PublicFormat.OpenSSH,
242+
)
243+
244+
# Create and sign the payload using a public key, but specify the "alg" in
245+
# the claims that a symmetric key was used.
246+
payload = b64(b'{"alg":"HS256"}') + b"." + b64(b'{"pwned":true}')
247+
hasher = hmac.HMAC(PUBKEY, hashes.SHA256())
248+
hasher.update(payload)
249+
evil_token = payload + b"." + b64(hasher.finalize())
250+
251+
# Verify and decode the token using the public key. The custom algorithm
252+
# field is left unspecified. Decoding using a public key should be
253+
# rejected raising a JWKError.
254+
with pytest.raises(JWKError):
255+
jwt.decode(evil_token, PUBKEY)

0 commit comments

Comments
 (0)