Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion jose/backends/cryptography_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
base64url_decode,
base64url_encode,
ensure_binary,
is_der_format,
is_pem_format,
is_ssh_key,
long_to_base64,
Expand Down Expand Up @@ -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."
Expand Down
4 changes: 2 additions & 2 deletions jose/backends/native.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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."
Expand Down
67 changes: 67 additions & 0 deletions jose/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
2 changes: 1 addition & 1 deletion tests/algorithms/test_EC.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
30 changes: 30 additions & 0 deletions tests/algorithms/test_HMAC.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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"
Expand Down
41 changes: 41 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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)