-
Notifications
You must be signed in to change notification settings - Fork 30
Add support for ES256 #51
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
8100733
a5fb321
2c3cabe
2f8bebe
6e8d041
f5dd050
901291e
40b7fc8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,9 @@ | |
| RSA256_KEY = test_util.load_rsa_private_key('rsa256_key.pem') | ||
| RSA512_KEY = test_util.load_rsa_private_key('rsa512_key.pem') | ||
| RSA1024_KEY = test_util.load_rsa_private_key('rsa1024_key.pem') | ||
| EC_P256_KEY = test_util.load_ec_private_key('ec_p256_key.pem') | ||
| EC_P384_KEY = test_util.load_ec_private_key('ec_p384_key.pem') | ||
| EC_P521_KEY = test_util.load_ec_private_key('ec_p521_key.pem') | ||
|
|
||
|
|
||
| class JWASignatureTest(unittest.TestCase): | ||
|
|
@@ -133,5 +136,81 @@ def test_verify_old_api(self): | |
| verifier.verify.called])) | ||
|
|
||
|
|
||
| class JWAECTest(unittest.TestCase): | ||
|
|
||
| def test_sign_no_private_part(self): | ||
| from josepy.jwa import ES256 | ||
| self.assertRaises( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this can be on one line.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed by a5fb321 |
||
| errors.Error, ES256.sign, EC_P256_KEY.public_key(), b'foo') | ||
|
|
||
| def test_es256_sign_and_verify(self): | ||
| from josepy.jwa import ES256 | ||
| message = b'foo' | ||
| signature = ES256.sign(EC_P256_KEY, message) | ||
| self.assertTrue(ES256.verify(EC_P256_KEY.public_key(), message, signature)) | ||
|
|
||
| def test_es384_sign_and_verify(self): | ||
| from josepy.jwa import ES384 | ||
| message = b'foo' | ||
| signature = ES384.sign(EC_P384_KEY, message) | ||
| self.assertTrue(ES384.verify(EC_P384_KEY.public_key(), message, signature)) | ||
|
|
||
| def test_es512_sign_and_verify(self): | ||
| from josepy.jwa import ES512 | ||
| message = b'foo' | ||
| signature = ES512.sign(EC_P521_KEY, message) | ||
| self.assertTrue(ES512.verify(EC_P521_KEY.public_key(), message, signature)) | ||
|
|
||
| def test_verify_with_wrong_jwa(self): | ||
| from josepy.jwa import ES256, ES384 | ||
| message = b'foo' | ||
| signature = ES256.sign(EC_P256_KEY, message) | ||
| self.assertFalse(ES384.verify(EC_P384_KEY.public_key(), message, signature)) | ||
|
|
||
| def test_verify_with_different_key(self): | ||
| from josepy.jwa import ES256 | ||
| from cryptography.hazmat.primitives.asymmetric import ec | ||
| from cryptography.hazmat.backends import default_backend | ||
|
|
||
| message = b'foo' | ||
| signature = ES256.sign(EC_P256_KEY, message) | ||
| different_key = ec.generate_private_key(ec.SECP256R1, default_backend()) | ||
| self.assertFalse(ES256.verify(different_key.public_key(), message, signature)) | ||
|
|
||
| def test_sign_new_api(self): | ||
| from josepy.jwa import ES256 | ||
| key = mock.MagicMock() | ||
| ES256.sign(key, "message") | ||
| self.assertTrue(key.sign.called) | ||
|
|
||
| def test_sign_old_api(self): | ||
| from josepy.jwa import ES256 | ||
| key = mock.MagicMock(spec=[u'signer']) | ||
| signer = mock.MagicMock() | ||
| key.signer.return_value = signer | ||
| ES256.sign(key, "message") | ||
| self.assertTrue(all([ | ||
| key.signer.called, | ||
| signer.update.called, | ||
| signer.finalize.called])) | ||
|
|
||
| def test_verify_new_api(self): | ||
| from josepy.jwa import ES256 | ||
| key = mock.MagicMock() | ||
| ES256.verify(key, "message", "signature") | ||
| self.assertTrue(key.verify.called) | ||
|
|
||
| def test_verify_old_api(self): | ||
| from josepy.jwa import ES256 | ||
| key = mock.MagicMock(spec=[u'verifier']) | ||
| verifier = mock.MagicMock() | ||
| key.verifier.return_value = verifier | ||
| ES256.verify(key, "message", "signature") | ||
| self.assertTrue(all([ | ||
| key.verifier.called, | ||
| verifier.update.called, | ||
| verifier.verify.called])) | ||
|
|
||
|
|
||
| if __name__ == '__main__': | ||
| unittest.main() # pragma: no cover | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -120,30 +120,6 @@ def load(cls, data, password=None, backend=None): | |
| raise errors.Error('Unsupported algorithm: {0}'.format(key.__class__)) | ||
|
|
||
|
|
||
| @JWK.register | ||
| class JWKES(JWK): # pragma: no cover | ||
| # pylint: disable=abstract-class-not-used | ||
| """ES JWK. | ||
|
|
||
| .. warning:: This is not yet implemented! | ||
|
|
||
| """ | ||
| typ = 'ES' | ||
| cryptography_key_types = ( | ||
| ec.EllipticCurvePublicKey, ec.EllipticCurvePrivateKey) | ||
| required = ('crv', JWK.type_field_name, 'x', 'y') | ||
|
|
||
| def fields_to_partial_json(self): | ||
| raise NotImplementedError() | ||
|
|
||
| @classmethod | ||
| def fields_from_json(cls, jobj): | ||
| raise NotImplementedError() | ||
|
|
||
| def public_key(self): | ||
| raise NotImplementedError() | ||
|
|
||
|
|
||
| @JWK.register | ||
| class JWKOct(JWK): | ||
| """Symmetric JWK.""" | ||
|
|
@@ -194,6 +170,7 @@ def _encode_param(cls, data): | |
| :rtype: unicode | ||
|
|
||
| """ | ||
|
|
||
| def _leading_zeros(arg): | ||
| if len(arg) % 2: | ||
| return '0' + arg | ||
|
|
@@ -248,7 +225,7 @@ def fields_from_json(cls, jobj): | |
|
|
||
| key = rsa.RSAPrivateNumbers( | ||
| p, q, d, dp, dq, qi, public_numbers).private_key( | ||
| default_backend()) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the reason for this change?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My IDE must have auto-formatted this. I will revert this change.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed by 2c3cabe |
||
| default_backend()) | ||
|
|
||
| return cls(key=key) | ||
|
|
||
|
|
@@ -275,3 +252,130 @@ def fields_to_partial_json(self): | |
| } | ||
| return dict((key, self._encode_param(value)) | ||
| for key, value in six.iteritems(params)) | ||
|
|
||
|
|
||
| @JWK.register | ||
| class JWKEC(JWK): | ||
| """EC JWK. | ||
|
|
||
| :ivar key: :class:`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey` | ||
| or :class:`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey` wrapped | ||
| in :class:`~josepy.util.ComparableRSAKey` | ||
|
|
||
| """ | ||
| typ = 'EC' | ||
| __slots__ = ('key',) | ||
| cryptography_key_types = ( | ||
| ec.EllipticCurvePublicKey, ec.EllipticCurvePrivateKey) | ||
| required = ('crv', JWK.type_field_name, 'x', 'y') | ||
|
|
||
| def __init__(self, *args, **kwargs): | ||
| if 'key' in kwargs and not isinstance( | ||
| kwargs['key'], util.ComparableECKey): | ||
| kwargs['key'] = util.ComparableECKey(kwargs['key']) | ||
| super(JWKEC, self).__init__(*args, **kwargs) | ||
|
|
||
| @classmethod | ||
| def _encode_param(cls, data): | ||
| """Encode Base64urlUInt. | ||
| :type data: long | ||
| :rtype: unicode | ||
| """ | ||
|
|
||
| def _leading_zeros(arg): | ||
| if len(arg) % 2: | ||
| return '0' + arg | ||
| return arg | ||
|
|
||
| return json_util.encode_b64jose(binascii.unhexlify( | ||
| _leading_zeros(hex(data)[2:].rstrip('L')))) | ||
|
|
||
| @classmethod | ||
| def _decode_param(cls, data, name, valid_lengths): | ||
| """Decode Base64urlUInt.""" | ||
| try: | ||
| binary = json_util.decode_b64jose(data) | ||
| if len(binary) not in valid_lengths: | ||
| raise errors.DeserializationError( | ||
| 'Expected parameter "{name}" to be {valid_lengths} bytes ' | ||
| 'after base64-decoding; got {length} bytes instead'.format( | ||
| name=name, valid_lengths=valid_lengths, length=len(binary)) | ||
| ) | ||
| return int(binascii.hexlify(binary), 16) | ||
| except ValueError: # invalid literal for long() with base 16 | ||
| raise errors.DeserializationError() | ||
|
|
||
| @classmethod | ||
| def _curve_name_to_crv(cls, curve_name): | ||
| if curve_name == 'secp256r1': | ||
| return 'P-256' | ||
| if curve_name == 'secp384r1': | ||
| return 'P-384' | ||
| if curve_name == 'secp521r1': | ||
| return 'P-521' | ||
| raise errors.SerializationError() | ||
|
|
||
| @classmethod | ||
| def _crv_to_curve(cls, crv): | ||
| # crv is case-sensitive | ||
| if crv == 'P-256': | ||
| return ec.SECP256R1() | ||
| if crv == 'P-384': | ||
| return ec.SECP384R1() | ||
| if crv == 'P-521': | ||
| return ec.SECP521R1() | ||
| raise errors.DeserializationError() | ||
|
|
||
| @classmethod | ||
| def _expected_length_for_curve(cls, curve): | ||
| if isinstance(curve, ec.SECP256R1): | ||
| return range(32, 33) | ||
| elif isinstance(curve, ec.SECP384R1): | ||
| return range(48, 49) | ||
| elif isinstance(curve, ec.SECP521R1): | ||
| return range(63, 67) | ||
|
|
||
| def fields_to_partial_json(self): | ||
| params = {} | ||
| if isinstance(self.key._wrapped, ec.EllipticCurvePublicKey): | ||
| public = self.key.public_numbers() | ||
| elif isinstance(self.key._wrapped, ec.EllipticCurvePrivateKey): | ||
| private = self.key.private_numbers() | ||
| public = self.key.public_key().public_numbers() | ||
| params.update({ | ||
|
||
| 'd': private.private_value, | ||
| }) | ||
| else: | ||
| raise errors.SerializationError( | ||
| 'Supplied key is neither of type EllipticCurvePublicKey nor EllipticCurvePrivateKey') | ||
| params.update({ | ||
| 'x': public.x, | ||
| 'y': public.y, | ||
| }) | ||
| params = dict((key, self._encode_param(value)) | ||
|
||
| for key, value in six.iteritems(params)) | ||
| params['crv'] = self._curve_name_to_crv(public.curve.name) | ||
| return params | ||
|
|
||
| @classmethod | ||
| def fields_from_json(cls, jobj): | ||
| # pylint: disable=invalid-name | ||
| curve = cls._crv_to_curve(jobj['crv']) | ||
| expected_length = cls._expected_length_for_curve(curve) | ||
| x, y = (cls._decode_param(jobj[n], n, expected_length) for n in ('x', 'y')) | ||
| public_numbers = ec.EllipticCurvePublicNumbers(x=x, y=y, curve=curve) | ||
| if 'd' not in jobj: # public key | ||
| key = public_numbers.public_key(default_backend()) | ||
| else: # private key | ||
| d = cls._decode_param(jobj['d'], 'd', expected_length) | ||
| key = ec.EllipticCurvePrivateNumbers(d, public_numbers).private_key( | ||
| default_backend()) | ||
| return cls(key=key) | ||
|
|
||
| def public_key(self): | ||
| # Unlike RSAPrivateKey, EllipticCurvePrivateKey does not contain public_key() | ||
| if hasattr(self.key, 'public_key'): | ||
| key = self.key.public_key() | ||
| else: | ||
| key = self.key.public_numbers().public_key(default_backend()) | ||
| return type(self)(key=key) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not just inline this?