Skip to content

Commit 590fc50

Browse files
committed
ComparableOKPKey work
1 parent 6c2a6d7 commit 590fc50

11 files changed

+92
-97
lines changed

CHANGELOG.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Changelog
88
* Added support for Ed25519, Ed448, X25519 and X448 keys (see `RFC 8037 <https://tools.ietf.org/html/rfc8037>`_).
99
These are also known as Bernstein curves.
1010
* Added support for signing with Ed25519, Ed448, X25519 and X448 keys
11-
(see `RFC 8032 <https://datatracker.ietf.org/doc/html/rfc8032>`_).
11+
(see `RFC 8032 <https://datatracker.ietf.org/doc/html/rfc8032>`_). See JWA.
1212
* Minimum requirement of ``cryptography`` is now 2.6+.
1313

1414
1.8.0 (2021-03-15)

src/josepy/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
ES256,
6969
ES384,
7070
ES512,
71+
EdDSA,
7172
)
7273

7374
from josepy.jwk import (

src/josepy/json_util.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -439,7 +439,7 @@ def register(cls, type_cls, typ=None):
439439
def get_type_cls(cls, jobj):
440440
"""Get the registered class for ``jobj``."""
441441
if cls in cls.TYPES.values():
442-
if cls.type_field_name not in jobj:
442+
if cls.type_field_name not in jobj: # noqa
443443
raise errors.DeserializationError(
444444
"Missing type field ({0})".format(cls.type_field_name))
445445
# cls is already registered type_cls, force to use it

src/josepy/jwa.py

-34
Original file line numberDiff line numberDiff line change
@@ -224,31 +224,6 @@ def _verify(self, key, msg, asn1sig):
224224
return True
225225

226226

227-
class _JWAOKP(JWASignature):
228-
kty = jwk.JWKOKP
229-
230-
def __init__(self, name, hash_):
231-
super().__init__(name)
232-
self.hash = hash_()
233-
234-
@classmethod
235-
def register(cls, signature_cls):
236-
# might need to overwrite this, so I can get the argument in
237-
return super().register(signature_cls)
238-
239-
def sign(self, key, msg: bytes):
240-
return key.sign(msg)
241-
242-
def verify(self, key, msg: bytes, sig: bytes):
243-
try:
244-
key.verify(signature=sig, data=msg)
245-
except cryptography.exceptions.InvalidSignature as error:
246-
logger.debug(error, exc_info=True)
247-
return False
248-
else:
249-
return True
250-
251-
252227
#: HMAC using SHA-256
253228
HS256 = JWASignature.register(_JWAHS('HS256', hashes.SHA256))
254229
#: HMAC using SHA-384
@@ -276,12 +251,3 @@ def verify(self, key, msg: bytes, sig: bytes):
276251
ES384 = JWASignature.register(_JWAEC('ES384', hashes.SHA384))
277252
#: ECDSA using P-521 and SHA-512
278253
ES512 = JWASignature.register(_JWAEC('ES512', hashes.SHA512))
279-
280-
#: Ed25519 uses SHA512
281-
ES25519 = JWASignature.register(_JWAOKP('ES25519', hashes.SHA512))
282-
#: Ed448 uses SHA3/SHAKE256
283-
# ES448 = JWASignature.register(_JWAOKP('ES448', hashes.SHAKE256))
284-
# #: X25519 uses SHA3/SHAKE256
285-
# X22519 = JWASignature.register(_JWAOKP('X22519', hashes.SHAKE256))
286-
# #: X448 uses SHA3/SHAKE256
287-
# X448 = JWASignature.register(_JWAOKP('X448', hashes.SHAKE256))

src/josepy/jwa_test.py

-18
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,6 @@
1010
EC_P256_KEY = test_util.load_ec_private_key('ec_p256_key.pem')
1111
EC_P384_KEY = test_util.load_ec_private_key('ec_p384_key.pem')
1212
EC_P521_KEY = test_util.load_ec_private_key('ec_p521_key.pem')
13-
OKP_ED25519_KEY = test_util.load_ec_private_key('ed25519_key.pem')
14-
OKP_ED448_KEY = test_util.load_ec_private_key('ed448_key.pem')
15-
OKP_X25519_KEY = test_util.load_ec_private_key('x25519_key.pem')
16-
OKP_X448_KEY = test_util.load_ec_private_key('x448_key.pem')
1713

1814

1915
class JWASignatureTest(unittest.TestCase):
@@ -230,19 +226,5 @@ def test_signature_size(self):
230226
self.assertEqual(len(sig), 2 * 66)
231227

232228

233-
# class JWAOKPTests(JWASignatureTest):
234-
# # look up the signature sizes in the RFC
235-
#
236-
# def test_sign_no_private_part(self):
237-
# from josepy.jwa import ES25519
238-
# self.assertRaises(errors.Error, ES25519.sign, OKP_ED25519_KEY, b'foo')
239-
#
240-
# # def test_can_size_ed25519(self):
241-
# # ES25519.sign(b'foo'), OKP_ED25519_KEY,
242-
#
243-
# def test_signature_size(self):
244-
# pass
245-
246-
247229
if __name__ == '__main__':
248230
unittest.main() # pragma: no cover

src/josepy/jwk.py

+18-19
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""JSON Web Key."""
22
import abc
3+
import collections
34
import json
45
import logging
56
import math
@@ -257,7 +258,7 @@ def fields_to_partial_json(self):
257258

258259
@JWK.register
259260
class JWKEC(JWK):
260-
"""EC JWK.
261+
"""RSA JWK.
261262
262263
:ivar key: :class:`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey`
263264
or :class:`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey`
@@ -389,24 +390,25 @@ class JWKOKP(JWK):
389390
or :class:`~cryptography.hazmat.primitives.asymmetric.x448.X448PublicKey`
390391
or :class:`~cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey`
391392
or :class:`~cryptography.hazmat.primitives.asymmetric.x25519.X25519PublicKey`
393+
wrapped in :class:`~josepy.util.ComparableOKPKey`
392394
393395
This class requires ``cryptography>=2.6`` to be installed.
394396
"""
395397
typ = 'OKP'
396-
__slots__ = ('key', )
397-
398+
__slots__ = ('key',)
398399
cryptography_key_types = (
399400
ed25519.Ed25519PrivateKey, ed25519.Ed25519PrivateKey,
400401
ed448.Ed448PublicKey, ed448.Ed448PrivateKey,
401402
x25519.X25519PrivateKey, x25519.X25519PublicKey,
402403
x448.X448PrivateKey, x448.X448PublicKey,
403404
)
404405
required = ('crv', JWK.type_field_name, 'x')
406+
okp_curve = collections.namedtuple('okp_curve', 'pubkey privkey')
405407
crv_to_pub_priv = {
406-
"Ed25519": (ed25519.Ed25519PublicKey, ed25519.Ed25519PrivateKey),
407-
"Ed448": (ed448.Ed448PublicKey, ed448.Ed448PrivateKey),
408-
"X25519": (x25519.X25519PublicKey, x25519.X25519PrivateKey),
409-
"X448": (x448.X448PublicKey, x448.X448PrivateKey),
408+
"Ed25519": okp_curve(pubkey=ed25519.Ed25519PublicKey, privkey=ed25519.Ed25519PrivateKey),
409+
"Ed448": okp_curve(pubkey=ed448.Ed448PublicKey, privkey=ed448.Ed448PrivateKey),
410+
"X25519": okp_curve(pubkey=x25519.X25519PublicKey, privkey=x25519.X25519PrivateKey),
411+
"X448": okp_curve(pubkey=x448.X448PublicKey, privkey=x448.X448PrivateKey),
410412
}
411413

412414
def __init__(self, *args, **kwargs):
@@ -428,20 +430,20 @@ def _key_to_crv(self):
428430
return "X448"
429431
return NotImplemented
430432

431-
def fields_to_partial_json(self) -> Dict:
433+
def fields_to_partial_json(self):
432434
params = {}
433435
if self.key.is_private():
434-
params['d'] = json_util.encode_b64jose(self.key.private_bytes(
436+
params['d'] = json_util.encode_b64jose(self.key._wrapped.private_bytes(
435437
encoding=serialization.Encoding.Raw,
436438
format=serialization.PrivateFormat.Raw,
437439
encryption_algorithm=serialization.NoEncryption()
438440
))
439-
params['x'] = self.key.public_key().public_bytes(
441+
params['x'] = self.key._wrapped.public_key().public_bytes(
440442
encoding=serialization.Encoding.Raw,
441443
format=serialization.PublicFormat.Raw,
442444
)
443445
else:
444-
params['x'] = json_util.encode_b64jose(self.key.public_bytes(
446+
params['x'] = json_util.encode_b64jose(self.key._wrapped.public_bytes(
445447
encoding=serialization.Encoding.Raw,
446448
format=serialization.PublicFormat.Raw,
447449
))
@@ -460,16 +462,13 @@ def fields_from_json(cls, jobj):
460462
except ValueError:
461463
raise errors.DeserializationError("Key is not valid JSON")
462464

463-
if obj.get("kty") != "OKP":
464-
raise errors.DeserializationError("Not an Octet Key Pair")
465-
466-
curve = obj.get("crv")
465+
curve = obj["crv"]
467466
if curve not in cls.crv_to_pub_priv:
468467
raise errors.DeserializationError(f"Invalid curve: {curve}")
469468

470469
if "x" not in obj:
471470
raise errors.DeserializationError('OKP should have "x" parameter')
472-
x = json_util.decode_b64jose(jobj.get("x"))
471+
x = json_util.decode_b64jose(jobj["x"])
473472

474473
try:
475474
if "d" not in obj: # public key
@@ -478,16 +477,16 @@ def fields_from_json(cls, jobj):
478477
ed448.Ed448PublicKey,
479478
x25519.X25519PublicKey,
480479
x448.X448PublicKey,
481-
]] = cls.crv_to_pub_priv[curve][0]
480+
]] = cls.crv_to_pub_priv[curve].pubkey
482481
return cls(key=pub_class.from_public_bytes(x))
483482
else: # private key
484-
d = json_util.decode_b64jose(obj.get("d"))
483+
d = json_util.decode_b64jose(obj["d"])
485484
priv_key_class: Type[Union[
486485
ed25519.Ed25519PrivateKey,
487486
ed448.Ed448PrivateKey,
488487
x25519.X25519PrivateKey,
489488
x448.X448PrivateKey,
490-
]] = cls.crv_to_pub_priv[curve][1]
489+
]] = cls.crv_to_pub_priv[curve].privkey
491490
return cls(key=priv_key_class.from_private_bytes(d))
492491
except ValueError as err:
493492
raise errors.DeserializationError("Invalid key parameter") from err

src/josepy/jwk_test.py

+21
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,7 @@ def test_fields_to_json(self):
406406
key = JWK.load(data)
407407
data = key.fields_to_partial_json()
408408
self.assertEqual(data['crv'], "Ed25519")
409+
self.assertIsInstance(data['x'], bytes)
409410

410411
def test_init_auto_comparable(self):
411412
self.assertIsInstance(self.x448_key.key, util.ComparableOKPKey)
@@ -421,10 +422,30 @@ def test_unknown_crv_name(self):
421422
}
422423
)
423424

425+
def test_no_x_name(self):
426+
from josepy.jwk import JWK
427+
with self.assertRaises(errors.DeserializationError) as warn:
428+
JWK.from_json(
429+
{
430+
'kty': 'OKP',
431+
'crv': 'Ed448',
432+
}
433+
)
434+
self.assertEqual(
435+
warn.exception.__str__(),
436+
'Deserialization error: OKP should have "x" parameter'
437+
)
438+
424439
def test_from_json_hashable(self):
425440
from josepy.jwk import JWK
426441
hash(JWK.from_json(self.jwked25519json))
427442

443+
def test_deserialize_public_key(self):
444+
# should target jwk.py:474-484, but those lines are still marked as missing
445+
# in the coverage report
446+
from josepy.jwk import JWKOKP
447+
JWKOKP.fields_from_json(self.jwked25519json)
448+
428449

429450
if __name__ == '__main__':
430451
unittest.main() # pragma: no cover

src/josepy/test_util.py

+10-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from cryptography.hazmat.primitives import serialization
1212

1313
from josepy import ComparableRSAKey, ComparableX509
14-
from josepy.util import ComparableECKey
14+
from josepy.util import ComparableECKey, ComparableOKPKey
1515

1616

1717
def vector_path(*names):
@@ -77,6 +77,15 @@ def load_ec_private_key(*names):
7777
load_vector(*names), password=None, backend=default_backend()))
7878

7979

80+
def load_okp_private_key(*names):
81+
"""Load OKP private key."""
82+
loader = _guess_loader(
83+
names[-1], serialization.load_pem_private_key,
84+
serialization.load_der_private_key,
85+
)
86+
return ComparableOKPKey(loader(load_vector(*names), password=None, backend=default_backend()))
87+
88+
8089
def load_pyopenssl_private_key(*names):
8190
"""Load pyOpenSSL private key."""
8291
loader = _guess_loader(

src/josepy/util.py

+14-21
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,7 @@
44
import OpenSSL
55
from cryptography.hazmat.backends import default_backend
66
from cryptography.hazmat.primitives import serialization
7-
from cryptography.hazmat.primitives.asymmetric import (
8-
ec,
9-
ed25519, ed448,
10-
rsa,
11-
x25519, x448,
12-
)
7+
from cryptography.hazmat.primitives.asymmetric import ec, rsa
138

149

1510
class abstractclassmethod(classmethod):
@@ -167,7 +162,7 @@ def public_key(self):
167162
class ComparableOKPKey(ComparableKey):
168163
"""Wrapper for ``cryptography`` OKP keys.
169164
170-
Wraps around:
165+
Wraps around any of these available with the compilation
171166
- :class:`~cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey`
172167
- :class:`~cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey`
173168
- :class:`~cryptography.hazmat.primitives.asymmetric.ed448.Ed448PublicKey`
@@ -179,24 +174,22 @@ class ComparableOKPKey(ComparableKey):
179174
"""
180175

181176
def __hash__(self):
177+
# Computed using the thumbprint
178+
# https://datatracker.ietf.org/doc/html/rfc7638#section-3
182179
if self.is_private():
183-
priv = self._wrapped.private_bytes(
184-
encoding=serialization.Encoding.PEM,
185-
format=serialization.PrivateFormat.PKCS8,
186-
)
187-
pub = priv.public_key
188-
return hash((self.__class__, pub.curve.name, priv))
189-
else:
190180
pub = self._wrapped.public_key()
191-
return hash((self.__class__, pub.curve.name, pub))
181+
else:
182+
pub = self._wrapped
183+
return hash(pub.public_bytes(
184+
format=serialization.PublicFormat.Raw,
185+
encoding=serialization.Encoding.Raw,
186+
)[:32])
192187

193188
def is_private(self) -> bool:
194-
return isinstance(
195-
self._wrapped, (
196-
ed25519.Ed25519PrivateKey, ed448.Ed448PrivateKey,
197-
x25519.X25519PrivateKey, x448.X448PrivateKey
198-
)
199-
)
189+
# Not all of the curves may be available with OpenSSL,
190+
# so instead of doing instance checks against the private
191+
# key classes, we do this
192+
return hasattr(self._wrapped, "private_bytes")
200193

201194

202195
class ImmutableMap(Mapping, Hashable):

src/josepy/util_test.py

+25-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import functools
33
import unittest
44

5-
65
from josepy import test_util
76

87

@@ -136,6 +135,31 @@ def test_public_key(self):
136135
self.assertIsInstance(self.p256_key.public_key(), ComparableECKey)
137136

138137

138+
class ComparableOKPKeyTests(unittest.TestCase):
139+
def setUp(self):
140+
# test_utl.load_ec_private_key return ComparableECKey
141+
self.ed25519_key = test_util.load_okp_private_key('ed25519_key.pem')
142+
self.ed25519_key_same = test_util.load_okp_private_key('ed25519_key.pem')
143+
self.ed448_key = test_util.load_okp_private_key('ed448_key.pem')
144+
self.x25519_key = test_util.load_okp_private_key('x25519_key.pem')
145+
self.x448_key = test_util.load_okp_private_key('x448_key.pem')
146+
147+
def test_repr(self):
148+
self.assertIs(repr(self.ed25519_key).startswith(
149+
'<ComparableOKPKey(<cryptography.hazmat.'), True)
150+
151+
def test_public_key(self):
152+
from josepy.util import ComparableOKPKey
153+
self.assertIsInstance(self.ed25519_key.public_key(), ComparableOKPKey)
154+
155+
def test_hash(self):
156+
self.assertIsInstance(hash(self.ed25519_key), int)
157+
self.assertEqual(hash(self.ed25519_key), hash(self.ed25519_key_same))
158+
self.assertNotEqual(hash(self.ed25519_key), hash(self.ed448_key))
159+
self.assertNotEqual(hash(self.ed25519_key), hash(self.x25519_key))
160+
self.assertNotEqual(hash(self.x25519_key), hash(self.ed448_key))
161+
162+
139163
class ImmutableMapTest(unittest.TestCase):
140164
"""Tests for josepy.util.ImmutableMap."""
141165

tox.ini

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ envlist =
44

55
[testenv]
66
commands =
7-
py.test {posargs}
7+
py.test -s {posargs}
88
deps =
99
-cconstraints.txt
1010
-e .[tests]

0 commit comments

Comments
 (0)