Skip to content

Commit 1e5d144

Browse files
committed
1 parent 638d047 commit 1e5d144

File tree

4 files changed

+86
-18
lines changed

4 files changed

+86
-18
lines changed

jose/backends/cryptography_backend.py

+2-9
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
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 base64_to_long, base64url_decode, base64url_encode, ensure_binary, long_to_base64, is_pem_format, is_ssh_key
2020
from .base import Key
2121

2222
_binding = None
@@ -555,14 +555,7 @@ def __init__(self, key, algorithm):
555555
if isinstance(key, str):
556556
key = key.encode("utf-8")
557557

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):
558+
if is_pem_format(key) or is_ssh_key(key):
566559
raise JWKError(
567560
"The specified key is an asymmetric key or x509 certificate and"
568561
" should not be used as an HMAC secret."

jose/backends/native.py

+2-9
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

+51
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,53 @@ 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 https://github.com/hynek/pem/blob/7ad94db26b0bc21d10953f5dbad3acfdfacf57aa/src/pem/_core.py#L224-L252
114+
_PEMS = {
115+
b"CERTIFICATE",
116+
b"TRUSTED CERTIFICATE",
117+
b"PRIVATE KEY",
118+
b"PUBLIC KEY",
119+
b"ENCRYPTED PRIVATE KEY",
120+
b"OPENSSH PRIVATE KEY",
121+
b"DSA PRIVATE KEY",
122+
b"RSA PRIVATE KEY",
123+
b"RSA PUBLIC KEY",
124+
b"EC PRIVATE KEY",
125+
b"DH PARAMETERS",
126+
b"NEW CERTIFICATE REQUEST",
127+
b"CERTIFICATE REQUEST",
128+
b"SSH2 PUBLIC KEY",
129+
b"SSH2 ENCRYPTED PRIVATE KEY",
130+
b"X509 CRL",
131+
}
132+
_PEM_RE = re.compile(
133+
b"----[- ]BEGIN ("
134+
+ b"|".join(re.escape(pem) for pem in _PEMS)
135+
+ b")[- ]----",
136+
)
137+
def is_pem_format(key: bytes) -> bool:
138+
return bool(_PEM_RE.search(key))
139+
# Based on https://github.com/pyca/cryptography/blob/bcb70852d577b3f490f015378c75cba74986297b/src/cryptography/hazmat/primitives/serialization/ssh.py#L40-L46
140+
_CERT_SUFFIX = b"[email protected]"
141+
_SSH_PUBKEY_RC = re.compile(br"\A(\S+)[ \t]+(\S+)")
142+
_SSH_KEY_FORMATS = [
143+
b"ssh-ed25519",
144+
b"ssh-rsa",
145+
b"ssh-dss",
146+
b"ecdsa-sha2-nistp256",
147+
b"ecdsa-sha2-nistp384",
148+
b"ecdsa-sha2-nistp521",
149+
]
150+
def is_ssh_key(key: bytes) -> bool:
151+
if any(string_value in key for string_value in _SSH_KEY_FORMATS):
152+
return True
153+
ssh_pubkey_match = _SSH_PUBKEY_RC.match(key)
154+
if ssh_pubkey_match:
155+
key_type = ssh_pubkey_match.group(1)
156+
if _CERT_SUFFIX == key_type[-len(_CERT_SUFFIX) :]:
157+
return True
158+
return False

tests/algorithms/test_EC.py

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

0 commit comments

Comments
 (0)