diff --git a/jose/backends/cryptography_backend.py b/jose/backends/cryptography_backend.py index ec836b4..26309db 100644 --- a/jose/backends/cryptography_backend.py +++ b/jose/backends/cryptography_backend.py @@ -20,6 +20,7 @@ base64url_decode, base64url_encode, ensure_binary, + is_der_format, is_pem_format, is_ssh_key, long_to_base64, @@ -540,7 +541,7 @@ def __init__(self, key, algorithm): if isinstance(key, str): key = key.encode("utf-8") - if is_pem_format(key) or is_ssh_key(key): + if is_pem_format(key) or is_ssh_key(key) or is_der_format(key): raise JWKError( "The specified key is an asymmetric key or x509 certificate and" " should not be used as an HMAC secret." diff --git a/jose/backends/native.py b/jose/backends/native.py index 8cc77da..304b993 100644 --- a/jose/backends/native.py +++ b/jose/backends/native.py @@ -5,7 +5,7 @@ from jose.backends.base import Key from jose.constants import ALGORITHMS from jose.exceptions import JWKError -from jose.utils import base64url_decode, base64url_encode, is_pem_format, is_ssh_key +from jose.utils import base64url_decode, base64url_encode, is_der_format, is_pem_format, is_ssh_key def get_random_bytes(num_bytes): @@ -36,7 +36,7 @@ def __init__(self, key, algorithm): if isinstance(key, str): key = key.encode("utf-8") - if is_pem_format(key) or is_ssh_key(key): + if is_pem_format(key) or is_ssh_key(key) or is_der_format(key): raise JWKError( "The specified key is an asymmetric key or x509 certificate and" " should not be used as an HMAC secret." diff --git a/jose/utils.py b/jose/utils.py index d62cafb..d046388 100644 --- a/jose/utils.py +++ b/jose/utils.py @@ -163,3 +163,70 @@ def is_ssh_key(key: bytes) -> bool: if _CERT_SUFFIX == key_type[-len(_CERT_SUFFIX) :]: return True return False + + +_DER_INTEGER = 0x02 +_DER_SEQUENCE = 0x30 +_DER_CONSTRUCTED = 0x20 + + +def _read_der_element(key: bytes, pos: int): + """Parse the DER element at ``pos``. + + Returns its ``(tag, content_start, end)`` offsets, or ``None`` if the + element is not well-formed DER. + """ + if pos + 2 > len(key): + return None + tag = key[pos] + length = key[pos + 1] + content_start = pos + 2 + if length & 0x80: + # Long form: the low bits hold the number of subsequent length bytes. + # Zero of them means the indefinite length that DER forbids. + length_bytes = length & 0x7F + if not 1 <= length_bytes <= 4 or content_start + length_bytes > len(key): + return None + length = int.from_bytes(key[content_start : content_start + length_bytes], "big") + content_start += length_bytes + end = content_start + length + if end > len(key): + return None + return tag, content_start, end + + +def _are_der_elements_well_formed(key: bytes, pos: int, end: int) -> bool: + """Whether ``key[pos:end]`` is a sequence of DER elements that tile it exactly.""" + while pos < end: + element = _read_der_element(key, pos) + if element is None: + return False + tag, content_start, element_end = element + if element_end > end: + return False + # Only constructed elements hold further elements; the contents of a + # primitive one (a BIT STRING wrapping a key, say) are opaque bytes. + if tag & _DER_CONSTRUCTED and not _are_der_elements_well_formed(key, content_start, element_end): + return False + pos = element_end + return True + + +def is_der_format(key: bytes) -> bool: + """Whether ``key`` is a DER-encoded asymmetric key or x509 certificate. + + DER has no armor or prefix to match on, so it is recognized by its ASN.1 + structure instead: a single SEQUENCE spanning the whole input, opening with + the SEQUENCE (an AlgorithmIdentifier or TBSCertificate) or INTEGER (a + version or modulus) that every such encoding starts with. + """ + element = _read_der_element(key, 0) + if element is None: + return False + tag, content_start, end = element + if tag != _DER_SEQUENCE or end != len(key): + return False + first_child = _read_der_element(key, content_start) + if first_child is None or first_child[0] not in (_DER_SEQUENCE, _DER_INTEGER): + return False + return _are_der_elements_well_formed(key, content_start, end) diff --git a/tests/algorithms/test_EC.py b/tests/algorithms/test_EC.py index d8602a2..100006b 100644 --- a/tests/algorithms/test_EC.py +++ b/tests/algorithms/test_EC.py @@ -235,7 +235,7 @@ def test_incorrect_public_key_hmac_signing(): def b64(x): return base64.urlsafe_b64encode(x).replace(b"=", b"") - KEY = CryptographyEc.generate_private_key(CryptographyEc.SECP256R1) + KEY = CryptographyEc.generate_private_key(CryptographyEc.SECP256R1()) PUBKEY = KEY.public_key().public_bytes( encoding=serialization.Encoding.OpenSSH, format=serialization.PublicFormat.OpenSSH, diff --git a/tests/algorithms/test_HMAC.py b/tests/algorithms/test_HMAC.py index 2b0859e..e7edb08 100644 --- a/tests/algorithms/test_HMAC.py +++ b/tests/algorithms/test_HMAC.py @@ -1,11 +1,32 @@ +import base64 import json import pytest +try: + from jose.backends.cryptography_backend import CryptographyHMACKey +except ImportError: + CryptographyHMACKey = None + from jose.backends.native import HMACKey from jose.constants import ALGORITHMS from jose.exceptions import JOSEError +HMAC_KEY_CLASSES = [pytest.param(HMACKey, id="native")] +if CryptographyHMACKey is not None: + HMAC_KEY_CLASSES.append(pytest.param(CryptographyHMACKey, id="pyca/cryptography")) + +# An EC P-256 key, as a DER SubjectPublicKeyInfo and a DER PKCS#8 private key. +der_public_key = base64.b64decode( + b"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERxw+dYxJBChbun5TEY7Q9SSt6wdX0lvS+Oew1236" + b"cUzdUg96yoqLkXrMN/Ud6PDJu+OthYOC5wLcJaEtCfeoWA==" +) +der_private_key = base64.b64decode( + b"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgEjRWeJCrze8SNFZ4kKvN7xI0VniQ" + b"q83vEjRWeJCrze+hRANCAARHHD51jEkEKFu6flMRjtD1JK3rB1fSW9L457DXbfpxTN1SD3rKiouR" + b"esw39R3o8Mm7462Fg4LnAtwloS0J96hY" +) + class TestHMACAlgorithm: def test_non_string_key(self): @@ -29,6 +50,15 @@ def test_RSA_key(self): with pytest.raises(JOSEError): HMACKey(key, ALGORITHMS.HS256) + @pytest.mark.parametrize("key_class", HMAC_KEY_CLASSES) + @pytest.mark.parametrize( + "key", + (pytest.param(der_public_key, id="public"), pytest.param(der_private_key, id="private")), + ) + def test_DER_key(self, key_class, key): + with pytest.raises(JOSEError): + key_class(key, ALGORITHMS.HS256) + def test_to_dict(self): passphrase = "The quick brown fox jumps over the lazy dog" encoded = "VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZw" diff --git a/tests/test_utils.py b/tests/test_utils.py index 2fbb08d..ecf9f8e 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,7 +1,21 @@ +import base64 from datetime import timedelta +import pytest + from jose import utils +# An EC P-256 key, as a DER SubjectPublicKeyInfo and a DER PKCS#8 private key. +der_public_key = base64.b64decode( + b"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERxw+dYxJBChbun5TEY7Q9SSt6wdX0lvS+Oew1236" + b"cUzdUg96yoqLkXrMN/Ud6PDJu+OthYOC5wLcJaEtCfeoWA==" +) +der_private_key = base64.b64decode( + b"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgEjRWeJCrze8SNFZ4kKvN7xI0VniQ" + b"q83vEjRWeJCrze+hRANCAARHHD51jEkEKFu6flMRjtD1JK3rB1fSW9L457DXbfpxTN1SD3rKiouR" + b"esw39R3o8Mm7462Fg4LnAtwloS0J96hY" +) + class TestUtils: def test_total_seconds(self): @@ -12,3 +26,30 @@ def test_total_seconds(self): def test_long_to_base64(self): assert utils.long_to_base64(0xDEADBEEF) == b"3q2-7w" assert utils.long_to_base64(0xCAFED00D, size=10) == b"AAAAAAAAyv7QDQ" + + @pytest.mark.parametrize( + "key", + (pytest.param(der_public_key, id="public"), pytest.param(der_private_key, id="private")), + ) + def test_is_der_format(self, key): + assert utils.is_der_format(key) + + @pytest.mark.parametrize( + "key", + ( + b"", + b"0", + b"00", + b"secret", + b"0123456789", + b"The quick brown fox jumps over the lazy dog", + # A SEQUENCE tag (0x30 is also ASCII "0") does not make a secret DER. + b"0" + b"x" * 121, + b"0" * 64, + # Truncated and over-long DER are both malformed. + der_public_key[:-1], + der_public_key + b"\x00", + ), + ) + def test_is_not_der_format(self, key): + assert not utils.is_der_format(key)