Skip to content
Merged
10 changes: 5 additions & 5 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ matrix:
- python: 2.7
env: TOXENV=py27-base
- python: 2.7
env: TOXENV=py27-cryptography
env: TOXENV=py27-cryptography-only
- python: 2.7
env: TOXENV=py27-pycryptodome
- python: 2.7
Expand All @@ -25,7 +25,7 @@ matrix:
- python: 3.4
env: TOXENV=py34-base
- python: 3.4
env: TOXENV=py34-cryptography
env: TOXENV=py34-cryptography-only
- python: 3.4
env: TOXENV=py34-pycryptodome
- python: 3.4
Expand All @@ -36,7 +36,7 @@ matrix:
- python: 3.5
env: TOXENV=py35-base
- python: 3.5
env: TOXENV=py35-cryptography
env: TOXENV=py35-cryptography-only
- python: 3.5
env: TOXENV=py35-pycryptodome
- python: 3.5
Expand All @@ -47,7 +47,7 @@ matrix:
- python: 3.5
env: TOXENV=py35-base
- python: 3.5
env: TOXENV=py35-cryptography
env: TOXENV=py35-cryptography-only
- python: 3.5
env: TOXENV=py35-pycryptodome
- python: 3.5
Expand All @@ -58,7 +58,7 @@ matrix:
- python: pypy-5.3.1
env: TOXENV=pypy-base
- python: pypy-5.3.1
env: TOXENV=pypy-cryptography
env: TOXENV=pypy-cryptography-only
- python: pypy-5.3.1
env: TOXENV=pypy-pycryptodome
- python: pypy-5.3.1
Expand Down
46 changes: 39 additions & 7 deletions jose/backends/cryptography_backend.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
from __future__ import division

import math

import six
import ecdsa
from ecdsa.util import sigdecode_string, sigencode_string, sigdecode_der, sigencode_der

try:
from ecdsa import SigningKey as EcdsaSigningKey, VerifyingKey as EcdsaVerifyingKey
except ImportError:
EcdsaSigningKey = EcdsaVerifyingKey = None

from jose.backends.base import Key
from jose.utils import base64_to_long, long_to_base64
Expand All @@ -11,7 +18,9 @@
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec, rsa, padding
from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature, encode_dss_signature
from cryptography.hazmat.primitives.serialization import load_pem_private_key, load_pem_public_key
from cryptography.utils import int_from_bytes, int_to_bytes
from cryptography.x509 import load_pem_x509_certificate


Expand All @@ -37,7 +46,7 @@ def __init__(self, key, algorithm, cryptography_backend=default_backend):
self.prepared_key = key
return

if isinstance(key, (ecdsa.SigningKey, ecdsa.VerifyingKey)):
if None not in (EcdsaSigningKey, EcdsaVerifyingKey) and isinstance(key, (EcdsaSigningKey, EcdsaVerifyingKey)):
# convert to PEM and let cryptography below load it as PEM
key = key.to_pem().decode('utf-8')

Expand Down Expand Up @@ -90,19 +99,42 @@ def _process_jwk(self, jwk_dict):
else:
return public.public_key(self.cryptography_backend())

def _sig_component_length(self):
"""Determine the correct serialization length for an encoded signature component.

This is the number of bytes required to encode the maximum key value.
"""
return int(math.ceil(self.prepared_key.key_size / 8.0))

def _der_to_raw(self, der_signature):
"""Convert signature from DER encoding to RAW encoding."""
r, s = decode_dss_signature(der_signature)
component_length = self._sig_component_length()
return int_to_bytes(r, component_length) + int_to_bytes(s, component_length)

def _raw_to_der(self, raw_signature):
"""Convert signature from RAW encoding to DER encoding."""
component_length = self._sig_component_length()
if len(raw_signature) != int(2 * component_length):
raise ValueError("Invalid signature")

r_bytes = raw_signature[:component_length]
s_bytes = raw_signature[component_length:]
r = int_from_bytes(r_bytes, "big")
s = int_from_bytes(s_bytes, "big")
return encode_dss_signature(r, s)

def sign(self, msg):
if self.hash_alg.digest_size * 8 > self.prepared_key.curve.key_size:
raise TypeError("this curve (%s) is too short "
"for your digest (%d)" % (self.prepared_key.curve.name,
8 * self.hash_alg.digest_size))
signature = self.prepared_key.sign(msg, ec.ECDSA(self.hash_alg()))
order = (2 ** self.prepared_key.curve.key_size) - 1
return sigencode_string(*sigdecode_der(signature, order), order=order)
return self._der_to_raw(signature)

def verify(self, msg, sig):
order = (2 ** self.prepared_key.curve.key_size) - 1
signature = sigencode_der(*sigdecode_string(sig, order), order=order)
try:
signature = self._raw_to_der(sig)
self.prepared_key.verify(signature, msg, ec.ECDSA(self.hash_alg()))
return True
except Exception:
Expand Down
51 changes: 50 additions & 1 deletion tests/algorithms/test_EC.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@

try:
from jose.backends.cryptography_backend import CryptographyECKey
from cryptography.hazmat.primitives.asymmetric import ec as CryptographyEc
from cryptography.hazmat.backends import default_backend as CryptographyBackend
except ImportError:
CryptographyECKey = None
CryptographyECKey = CryptographyEc = CryptographyBackend = None

import pytest

Expand All @@ -31,6 +33,18 @@
-----END EC PRIVATE KEY-----
"""

# ES256 signatures generated to test conversion logic
DER_SIGNATURE = (
b"0F\x02!\x00\x89yG\x81W\x01\x11\x9b0\x08\xa4\xd0\xe3g([\x07\xb5\x01\xb3"
b"\x9d\xdf \xd1\xbc\xedK\x01\x87:}\xf2\x02!\x00\xb2shTA\x00\x1a\x13~\xba"
b"J\xdb\xeem\x12\x1e\xfeMO\x04\xb2[\x86A\xbd\xc6hu\x953X\x1e"
)
RAW_SIGNATURE = (
b"\x89yG\x81W\x01\x11\x9b0\x08\xa4\xd0\xe3g([\x07\xb5\x01\xb3\x9d\xdf "
b"\xd1\xbc\xedK\x01\x87:}\xf2\xb2shTA\x00\x1a\x13~\xbaJ\xdb\xeem\x12\x1e"
b"\xfeMO\x04\xb2[\x86A\xbd\xc6hu\x953X\x1e"
)


def _backend_exception_types():
"""Build the backend exception types based on available backends."""
Expand All @@ -51,6 +65,41 @@ def test_key_from_ecdsa():
assert not ECKey(key, ALGORITHMS.ES256).is_public()


@pytest.mark.cryptography
@pytest.mark.skipif(CryptographyECKey is None, reason="pyca/cryptography backend not available")
@pytest.mark.parametrize("algorithm, expected_length", (
(ALGORITHMS.ES256, 32),
(ALGORITHMS.ES384, 48),
(ALGORITHMS.ES512, 66)
))
def test_cryptography_sig_component_length(algorithm, expected_length):
# Put mapping inside here to avoid more complex handling for test runs that do not have pyca/cryptography
mapping = {
ALGORITHMS.ES256: CryptographyEc.SECP256R1,
ALGORITHMS.ES384: CryptographyEc.SECP384R1,
ALGORITHMS.ES512: CryptographyEc.SECP521R1,
}
key = CryptographyECKey(
CryptographyEc.generate_private_key(mapping[algorithm](), backend=CryptographyBackend()),
algorithm
)
assert key._sig_component_length() == expected_length


@pytest.mark.cryptography
@pytest.mark.skipif(CryptographyECKey is None, reason="pyca/cryptography backend not available")
def test_cryptograhy_der_to_raw():
key = CryptographyECKey(private_key, ALGORITHMS.ES256)
assert key._der_to_raw(DER_SIGNATURE) == RAW_SIGNATURE


@pytest.mark.cryptography
@pytest.mark.skipif(CryptographyECKey is None, reason="pyca/cryptography backend not available")
def test_cryptograhy_raw_to_der():
key = CryptographyECKey(private_key, ALGORITHMS.ES256)
assert key._raw_to_der(RAW_SIGNATURE) == DER_SIGNATURE


class TestECAlgorithm:

def test_key_from_pem(self):
Expand Down
8 changes: 7 additions & 1 deletion tox.ini
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
[tox]
envlist = py{27,34,35,36,py}-{base,cryptography,pycryptodome,pycrypto,compatibility},flake8
minversion = 3.4.0
envlist =
py{27,34,35,36,py}-{base,cryptography-only,pycryptodome,pycrypto,compatibility},
flake8
skip_missing_interpreters = True

[testenv:basecommand]
Expand All @@ -19,6 +22,9 @@ deps =
pytest-cov
pytest-runner
compatibility: {[testenv:compatibility]deps}
commands_pre =
# Remove the python-rsa backend
only: pip uninstall -y ecdsa rsa
commands =
# Test the python-rsa backend
base: {[testenv:basecommand]commands} -m "not (cryptography or pycryptodome or pycrypto or backend_compatibility)"
Expand Down