| From cafc100f9777b9b214c2f58898ba90d207470120 Mon Sep 17 00:00:00 2001 |
| From: Oleksandr Tymoshenko <ovt@google.com> |
| Date: Tue, 28 Jun 2022 20:40:18 +0000 |
| Subject: [PATCH] Fix GHSA-ffqj-6fqr-9h24 |
| |
| Backport fix for GHSA-ffqj-6fqr-9h24 |
| --- |
| jwt/algorithms.py | 11 ++---- |
| jwt/utils.py | 62 ++++++++++++++++++++++++++++++++ |
| tests/test_advisory.py | 81 ++++++++++++++++++++++++++++++++++++++++++ |
| 3 files changed, 145 insertions(+), 9 deletions(-) |
| create mode 100644 tests/test_advisory.py |
| |
| diff --git a/jwt/algorithms.py b/jwt/algorithms.py |
| index 1343688..8e4d739 100644 |
| --- a/jwt/algorithms.py |
| +++ b/jwt/algorithms.py |
| @@ -8,7 +8,7 @@ from .exceptions import InvalidKeyError |
| from .utils import ( |
| base64url_decode, base64url_encode, der_to_raw_signature, |
| force_bytes, force_unicode, from_base64url_uint, raw_to_der_signature, |
| - to_base64url_uint |
| + to_base64url_uint, is_pem_format, is_ssh_key |
| ) |
| |
| try: |
| @@ -139,14 +139,7 @@ class HMACAlgorithm(Algorithm): |
| def prepare_key(self, key): |
| key = force_bytes(key) |
| |
| - invalid_strings = [ |
| - b'-----BEGIN PUBLIC KEY-----', |
| - b'-----BEGIN CERTIFICATE-----', |
| - b'-----BEGIN RSA PUBLIC KEY-----', |
| - b'ssh-rsa' |
| - ] |
| - |
| - if any([string_value in key for string_value in invalid_strings]): |
| + if is_pem_format(key) or is_ssh_key(key): |
| raise InvalidKeyError( |
| 'The specified key is an asymmetric key or x509 certificate and' |
| ' should not be used as an HMAC secret.') |
| diff --git a/jwt/utils.py b/jwt/utils.py |
| index b33c7a2..a765bea 100644 |
| --- a/jwt/utils.py |
| +++ b/jwt/utils.py |
| @@ -1,5 +1,6 @@ |
| import base64 |
| import binascii |
| +import re |
| import struct |
| |
| from .compat import binary_type, bytes_from_int, text_type |
| @@ -111,3 +112,64 @@ def raw_to_der_signature(raw_sig, curve): |
| s = bytes_to_number(raw_sig[num_bytes:]) |
| |
| return encode_dss_signature(r, s) |
| + |
| + |
| + |
| +# Based on https://github.com/hynek/pem/blob/7ad94db26b0bc21d10953f5dbad3acfdfacf57aa/src/pem/_core.py#L224-L252 |
| +_PEMS = { |
| + b"CERTIFICATE", |
| + b"TRUSTED CERTIFICATE", |
| + b"PRIVATE KEY", |
| + b"PUBLIC KEY", |
| + b"ENCRYPTED PRIVATE KEY", |
| + b"OPENSSH PRIVATE KEY", |
| + b"DSA PRIVATE KEY", |
| + b"RSA PRIVATE KEY", |
| + b"RSA PUBLIC KEY", |
| + b"EC PRIVATE KEY", |
| + b"DH PARAMETERS", |
| + b"NEW CERTIFICATE REQUEST", |
| + b"CERTIFICATE REQUEST", |
| + b"SSH2 PUBLIC KEY", |
| + b"SSH2 ENCRYPTED PRIVATE KEY", |
| + b"X509 CRL", |
| +} |
| + |
| +_PEM_RE = re.compile( |
| + b"----[- ]BEGIN (" |
| + + b"|".join(_PEMS) |
| + + b""")[- ]----\r? |
| +.+?\r? |
| +----[- ]END \\1[- ]----\r?\n?""", |
| + re.DOTALL, |
| +) |
| + |
| + |
| +def is_pem_format(key): |
| + return bool(_PEM_RE.search(key)) |
| + |
| + |
| +# Based on https://github.com/pyca/cryptography/blob/bcb70852d577b3f490f015378c75cba74986297b/src/cryptography/hazmat/primitives/serialization/ssh.py#L40-L46 |
| +_CERT_SUFFIX = b"-cert-v01@openssh.com" |
| +_SSH_PUBKEY_RC = re.compile(br"\A(\S+)[ \t]+(\S+)") |
| +_SSH_KEY_FORMATS = [ |
| + b"ssh-ed25519", |
| + b"ssh-rsa", |
| + b"ssh-dss", |
| + b"ecdsa-sha2-nistp256", |
| + b"ecdsa-sha2-nistp384", |
| + b"ecdsa-sha2-nistp521", |
| +] |
| + |
| + |
| +def is_ssh_key(key): |
| + if any(string_value in key for string_value in _SSH_KEY_FORMATS): |
| + return True |
| + |
| + ssh_pubkey_match = _SSH_PUBKEY_RC.match(key) |
| + if ssh_pubkey_match: |
| + key_type = ssh_pubkey_match.group(1) |
| + if _CERT_SUFFIX == key_type[-len(_CERT_SUFFIX) :]: |
| + return True |
| + |
| + return False |
| diff --git a/tests/test_advisory.py b/tests/test_advisory.py |
| new file mode 100644 |
| index 0000000..68a4437 |
| --- /dev/null |
| +++ b/tests/test_advisory.py |
| @@ -0,0 +1,81 @@ |
| +import jwt |
| +import pytest |
| +from jwt.exceptions import InvalidKeyError |
| + |
| +priv_key_bytes = b'''-----BEGIN PRIVATE KEY----- |
| +MC4CAQAwBQYDK2VwBCIEIIbBhdo2ah7X32i50GOzrCr4acZTe6BezUdRIixjTAdL |
| +-----END PRIVATE KEY-----''' |
| + |
| +pub_key_bytes = b'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPL1I9oiq+B8crkmuV4YViiUnhdLjCp3hvy1bNGuGfNL' |
| + |
| +ssh_priv_key_bytes = b"""-----BEGIN EC PRIVATE KEY----- |
| +MHcCAQEEIOWc7RbaNswMtNtc+n6WZDlUblMr2FBPo79fcGXsJlGQoAoGCCqGSM49 |
| +AwEHoUQDQgAElcy2RSSSgn2RA/xCGko79N+7FwoLZr3Z0ij/ENjow2XpUDwwKEKk |
| +Ak3TDXC9U8nipMlGcY7sDpXp2XyhHEM+Rw== |
| +-----END EC PRIVATE KEY-----""" |
| + |
| +ssh_key_bytes = b"""ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJXMtkUkkoJ9kQP8QhpKO/TfuxcKC2a92dIo/xDY6MNl6VA8MChCpAJN0w1wvVPJ4qTJRnGO7A6V6dl8oRxDPkc=""" |
| + |
| + |
| +class TestAdvisory: |
| + def test_ghsa_ffqj_6fqr_9h24(self): |
| + # Using HMAC with the public key to trick the receiver to think that the |
| + # public key is a HMAC secret |
| + encoded_bad = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0ZXN0IjoxMjM0fQ.6ulDpqSlbHmQ8bZXhZRLFko9SwcHrghCwh8d-exJEE4' |
| + |
| + with pytest.raises(InvalidKeyError): |
| + jwt.decode( |
| + encoded_bad, |
| + pub_key_bytes, |
| + algorithms=jwt.algorithms.get_default_algorithms(), |
| + ) |
| + |
| + # Of course the receiver should specify ed25519 algorithm to be used if |
| + # they specify ed25519 public key. However, if other algorithms are used, |
| + # the POC does not work |
| + # HMAC specifies illegal strings for the HMAC secret in jwt/algorithms.py |
| + # |
| + # invalid_str ings = [ |
| + # b"-----BEGIN PUBLIC KEY-----", |
| + # b"-----BEGIN CERTIFICATE-----", |
| + # b"-----BEGIN RSA PUBLIC KEY-----", |
| + # b"ssh-rsa", |
| + # ] |
| + # |
| + # However, OKPAlgorithm (ed25519) accepts the following in jwt/algorithms.py: |
| + # |
| + # if "-----BEGIN PUBLIC" in str_key: |
| + # return load_pem_public_key(key) |
| + # if "-----BEGIN PRIVATE" in str_key: |
| + # return load_pem_private_key(key, password=None) |
| + # if str_key[0:4] == "ssh-": |
| + # return load_ssh_public_key(key) |
| + # |
| + # These should most likely made to match each other to prevent this behavior |
| + |
| + # POC for the ecdsa-sha2-nistp256 format. |
| + # openssl ecparam -genkey -name prime256v1 -noout -out ec256-key-priv.pem |
| + # openssl ec -in ec256-key-priv.pem -pubout > ec256-key-pub.pem |
| + # ssh-keygen -y -f ec256-key-priv.pem > ec256-key-ssh.pub |
| + |
| + # Making a good jwt token that should work by signing it with the private key |
| + # encoded_good = jwt.encode({"test": 1234}, ssh_priv_key_bytes, algorithm="ES256") |
| + encoded_good = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoxMjM0fQ.NX42mS8cNqYoL3FOW9ZcKw8Nfq2mb6GqJVADeMA1-kyHAclilYo_edhdM_5eav9tBRQTlL0XMeu_WFE_mz3OXg" |
| + |
| + # Using HMAC with the ssh public key to trick the receiver to think that the public key is a HMAC secret |
| + # encoded_bad = jwt.encode({"test": 1234}, ssh_key_bytes, algorithm="HS256") |
| + encoded_bad = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoxMjM0fQ.5eYfbrbeGYmWfypQ6rMWXNZ8bdHcqKng5GPr9MJZITU" |
| + |
| + # Both of the jwt tokens are validated as valid |
| + jwt.decode( |
| + encoded_good, |
| + ssh_key_bytes, |
| + algorithms=jwt.algorithms.get_default_algorithms() |
| + ) |
| + |
| + with pytest.raises(InvalidKeyError): |
| + jwt.decode( |
| + encoded_bad, |
| + ssh_key_bytes, |
| + algorithms=jwt.algorithms.get_default_algorithms() |
| + ) |
| -- |
| 2.37.0.rc0.161.g10f37bed90-goog |
| |