Skip to content

Commit ea7d219

Browse files
committed
better fields_from_json
1 parent 85cc184 commit ea7d219

File tree

5 files changed

+77
-50
lines changed

5 files changed

+77
-50
lines changed

src/josepy/jwa.py

+3
Original file line numberDiff line numberDiff line change
@@ -250,3 +250,6 @@ def _verify(self, key, msg, asn1sig):
250250
ES384 = JWASignature.register(_JWAEC('ES384', hashes.SHA384))
251251
#: ECDSA using P-521 and SHA-512
252252
ES512 = JWASignature.register(_JWAEC('ES512', hashes.SHA512))
253+
254+
# Also implement RFC 8037, signing for OKP key type
255+
# hashes.BLAKE2b

src/josepy/jwa_test.py

+4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
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')
1317

1418

1519
class JWASignatureTest(unittest.TestCase):

src/josepy/jwk.py

+36-23
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import logging
55
import math
66

7-
from typing import Dict, Optional, Sequence, Type, Union
7+
from typing import Dict, Optional, Sequence, Tuple, Type, Union
88

99
import cryptography.exceptions
1010
from cryptography.hazmat.backends import default_backend
@@ -18,7 +18,6 @@
1818
)
1919

2020
from josepy import errors, json_util, util
21-
from josepy.util import ComparableOKPKey
2221

2322
logger = logging.getLogger(__name__)
2423

@@ -341,9 +340,7 @@ def fields_to_partial_json(self):
341340
params['d'] = private.private_value
342341
else:
343342
raise errors.SerializationError(
344-
'Supplied key is neither of type EllipticCurvePublicKey '
345-
'nor EllipticCurvePrivateKey'
346-
)
343+
'Supplied key is neither of type EllipticCurvePublicKey nor EllipticCurvePrivateKey')
347344
params['x'] = public.x
348345
params['y'] = public.y
349346
params = {
@@ -388,7 +385,7 @@ class JWKOKP(JWK):
388385
or :class:`~cryptography.hazmat.primitives.asymmetric.ed448.Ed448PublicKey`
389386
or :class:`~cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey`
390387
or :class:`~cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey`
391-
or :ivar: :key :class:`~cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey`
388+
or :class:`~cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey`
392389
or :class:`~cryptography.hazmat.primitives.asymmetric.x448.X448PublicKey`
393390
or :class:`~cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey`
394391
or :class:`~cryptography.hazmat.primitives.asymmetric.x25519.X25519PublicKey`
@@ -405,6 +402,12 @@ class JWKOKP(JWK):
405402
x448.X448PrivateKey, x448.X448PublicKey,
406403
)
407404
required = ('crv', JWK.type_field_name, 'x')
405+
crv_to_pub_priv: Dict[str, Tuple] = {
406+
"Ed25519": (ed25519.Ed25519PublicKey, ed25519.Ed25519PrivateKey),
407+
"Ed448": (ed448.Ed448PublicKey, ed448.Ed448PrivateKey),
408+
"X25519": (x25519.X25519PublicKey, x25519.X25519PrivateKey),
409+
"X448": (x448.X448PublicKey, x448.X448PrivateKey),
410+
}
408411

409412
def __init__(self, *args, **kwargs):
410413
if 'key' in kwargs and not isinstance(kwargs['key'], util.ComparableOKPKey):
@@ -415,41 +418,38 @@ def public_key(self):
415418
return self.key._wrapped.__class__.public_key()
416419

417420
def _key_to_crv(self):
418-
if isinstance(self.key._wrapped, (ed25519.Ed25519PrivateKey, ed25519.Ed25519PrivateKey)):
421+
if isinstance(self.key._wrapped, (ed25519.Ed25519PublicKey, ed25519.Ed25519PrivateKey)):
419422
return "Ed25519"
420-
elif isinstance(self.key._wrapped, (ed448.Ed448PrivateKey, ed448.Ed448PrivateKey)):
423+
elif isinstance(self.key._wrapped, (ed448.Ed448PublicKey, ed448.Ed448PrivateKey)):
421424
return "Ed448"
422-
elif isinstance(self.key._wrapped, (x25519.X25519PrivateKey, x25519.X25519PrivateKey)):
425+
elif isinstance(self.key._wrapped, (x25519.X25519PublicKey, x25519.X25519PrivateKey)):
423426
return "X25519"
424-
elif isinstance(self.key._wrapped, (x448.X448PrivateKey, x448.X448PrivateKey)):
427+
elif isinstance(self.key._wrapped, (x448.X448PublicKey, x448.X448PrivateKey)):
425428
return "X448"
426429
return NotImplemented
427430

428431
def fields_to_partial_json(self) -> Dict:
429432
params = {}
430-
print(dir(self))
431433
if self.key.is_private():
432434
params['d'] = json_util.encode_b64jose(self.key.private_bytes(
433-
encoding=serialization.Encoding.PEM,
434-
format=serialization.PrivateFormat.PKCS8,
435+
encoding=serialization.Encoding.Raw,
436+
format=serialization.PrivateFormat.Raw,
435437
encryption_algorithm=serialization.NoEncryption()
436438
))
437439
params['x'] = self.key.public_key().public_bytes(
438-
encoding=serialization.Encoding.PEM,
439-
format=serialization.PublicFormat.SubjectPublicKeyInfo,
440+
encoding=serialization.Encoding.Raw,
441+
format=serialization.PublicFormat.Raw,
440442
)
441443
else:
442444
params['x'] = json_util.encode_b64jose(self.key.public_bytes(
443445
serialization.Encoding.Raw,
444446
serialization.PublicFormat.Raw,
445-
serialization.NoEncryption(),
446447
))
447448
params['crv'] = self._key_to_crv()
448449
return params
449450

450451
@classmethod
451-
def fields_from_json(cls, jobj) -> ComparableOKPKey:
452-
# this was mostly copy/pasted from some source. Find out which.
452+
def fields_from_json(cls, jobj):
453453
try:
454454
if isinstance(jobj, str):
455455
obj = json.loads(jobj)
@@ -464,17 +464,30 @@ def fields_from_json(cls, jobj) -> ComparableOKPKey:
464464
raise errors.DeserializationError("Not an Octet Key Pair")
465465

466466
curve = obj.get("crv")
467-
if curve not in ("Ed25519", "Ed448", "X25519", "X448"):
467+
if curve not in cls.crv_to_pub_priv:
468468
raise errors.DeserializationError(f"Invalid curve: {curve}")
469469

470470
if "x" not in obj:
471471
raise errors.DeserializationError('OKP should have "x" parameter')
472472
x = json_util.decode_b64jose(jobj.get("x"))
473473

474474
try:
475-
if "d" not in obj:
476-
return jobj["key"]._wrapped.__class__.from_public_bytes(x) # noqa
477-
d = json_util.decode_b64jose(obj.get("d"))
478-
return jobj["key"]._wrapped.__class__.from_private_bytes(d) # noqa
475+
if "d" not in obj: # public key
476+
pub_class: Union[
477+
ed25519.Ed25519PublicKey,
478+
ed448.Ed448PublicKey,
479+
x25519.X25519PublicKey,
480+
x448.X448PublicKey,
481+
] = cls.crv_to_pub_priv[curve][0]
482+
return cls(key=pub_class.from_public_bytes(x))
483+
else: # private key
484+
d = json_util.decode_b64jose(obj.get("d"))
485+
priv_key_class: Union[
486+
ed25519.Ed25519PrivateKey,
487+
ed448.Ed448PrivateKey,
488+
x25519.X25519PrivateKey,
489+
x448.X448PrivateKey,
490+
] = cls.crv_to_pub_priv[curve][1]
491+
return cls(key=priv_key_class.from_private_bytes(d))
479492
except ValueError as err:
480493
raise errors.DeserializationError("Invalid key parameter") from err

src/josepy/jwk_test.py

+29-14
Original file line numberDiff line numberDiff line change
@@ -229,8 +229,7 @@ def setUp(self):
229229
self.jwk = self.private
230230

231231
def test_init_auto_comparable(self):
232-
self.assertIsInstance(
233-
self.jwk256_not_comparable.key, util.ComparableECKey)
232+
self.assertIsInstance(self.jwk256_not_comparable.key, util.ComparableECKey)
234233
self.assertEqual(self.jwk256, self.jwk256_not_comparable)
235234

236235
def test_encode_param_zero(self):
@@ -328,9 +327,9 @@ class JWKOKPTest(unittest.TestCase):
328327
"""Tests for josepy.jwk.JWKOKP."""
329328
# pylint: disable=too-many-instance-attributes
330329

331-
# What to put in the thumbprint
330+
# TODO: write the thumbprint
332331
thumbprint = (
333-
332+
b'kPrK_qmxVWaYVA9wwBF6Iuo3vVzz7TxHCTwXBygrS4k'
334333
)
335334

336335
def setUp(self):
@@ -341,14 +340,14 @@ def setUp(self):
341340
self.x448_key = JWKOKP(key=X448_KEY.public_key())
342341
self.private = self.x448_key
343342
self.jwk = self.private
344-
# Test vectors taken from
343+
# Test vectors taken from RFC 8037, A.2
345344
self.jwked25519json = {
346345
'kty': 'OKP',
347346
'crv': 'Ed25519',
348-
'x': '',
347+
'x': '11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo',
349348
}
350349
self.jwked448json = {
351-
'kty': 'EC',
350+
'kty': 'OKP',
352351
'crv': 'Ed448',
353352
'x':
354353
"9b08f7cc31b7e3e67d22d5aea121074a273bd2b83de09c63faa73d2c"
@@ -357,27 +356,25 @@ def setUp(self):
357356
# Test vectors taken from
358357
# https://datatracker.ietf.org/doc/html/rfc7748#section-6.1
359358
self.jwkx25519json = {
360-
'kty': 'EC',
359+
'kty': 'OKP',
361360
'crv': 'X25519',
362361
'x': '8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a',
363362
}
364363
self.jwkx448json = {
365-
'kty': 'EC',
364+
'kty': 'OKP',
366365
'crv': 'X448',
367366
'x': 'jjQtV-fA7J_tK8dPzYq7jRPNjF8r5p6LW2R25S2Gw5U',
368367
}
369368

370369
def test_encode_ed448(self):
371370
from josepy.jwk import JWKOKP
372-
import josepy
373371
data = b"""-----BEGIN PRIVATE KEY-----
374372
MEcCAQAwBQYDK2VxBDsEOfqsAFWdop10FFPW7Ha2tx2AZh0Ii+jfL2wFXU/dY/fe
375373
iU7/vrGmQ+ux26NkgzfploOHZjEmltLJ9w==
376374
-----END PRIVATE KEY-----"""
377375
key = JWKOKP.load(data)
378-
data = key.to_partial_json()
379-
x = josepy.json_util.encode_b64jose(data['x'])
380-
self.assertEqual(len(x), 195)
376+
partial = key.to_partial_json()
377+
self.assertEqual(partial['crv'], 'Ed448')
381378

382379
def test_encode_ed25519(self):
383380
import josepy
@@ -388,7 +385,25 @@ def test_encode_ed25519(self):
388385
key = JWKOKP.load(data)
389386
data = key.to_partial_json()
390387
x = josepy.json_util.encode_b64jose(data['x'])
391-
self.assertEqual(len(x), 151)
388+
self.assertEqual(x, "9ujoz88QZL05w2lhaqUbBaBpwmM12Y7Y8Ybfwjibk-I")
389+
390+
def test_from_json_ed25519(self):
391+
from josepy.jwk import JWK
392+
key = JWK.from_json(self.jwked25519json)
393+
with self.subTest(key=[
394+
self.jwked448json, self.jwked25519json,
395+
self.jwkx25519json, self.jwkx448json,
396+
]):
397+
self.assertIsInstance(key.key, util.ComparableOKPKey)
398+
399+
def test_fields_to_json(self):
400+
from josepy.jwk import JWK
401+
data = b"""-----BEGIN PRIVATE KEY-----
402+
MC4CAQAwBQYDK2VwBCIEIPIAha9VqyHHpY1GtEW8JXWqLU5mrPRhXPwJqCtL3bWZ
403+
-----END PRIVATE KEY-----"""
404+
key = JWK.load(data)
405+
data = key.fields_to_partial_json()
406+
self.assertEqual(data['crv'], "Ed25519")
392407

393408
def test_init_auto_comparable(self):
394409
self.assertIsInstance(self.x448_key.key, util.ComparableOKPKey)

src/josepy/util.py

+5-13
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
"""JOSE utilities."""
22
from collections.abc import Hashable, Mapping
3-
from typing import Union
43

54
import OpenSSL
65
from cryptography.hazmat.backends import default_backend
@@ -168,34 +167,27 @@ class ComparableOKPKey(ComparableKey):
168167
"""Wrapper for ``cryptography`` OKP keys.
169168
170169
Wraps around:
171-
- :class:`~cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey`
172170
- :class:`~cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey`
173-
- :class:`~cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey`
171+
- :class:`~cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey`
174172
- :class:`~cryptography.hazmat.primitives.asymmetric.ed448.Ed448PublicKey`
175-
- :class:`~cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey`
173+
- :class:`~cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey`
176174
- :class:`~cryptography.hazmat.primitives.asymmetric.x25519.X25519PublicKey`
177-
- :class:`~cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey`
175+
- :class:`~cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey`
178176
- :class:`~cryptography.hazmat.primitives.asymmetric.x448.X448PublicKey`
177+
- :class:`~cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey`
179178
"""
180179

181180
def __hash__(self):
182181
return hash((self.__class__, self._wrapped.curve.name, self._wrapped.x))
183182

184-
def is_private(self):
183+
def is_private(self) -> bool:
185184
return isinstance(
186185
self._wrapped, (
187186
ed25519.Ed25519PrivateKey, ed448.Ed448PrivateKey,
188187
x25519.X25519PrivateKey, x448.X448PrivateKey
189188
)
190189
)
191190

192-
def public_key(self) -> Union[
193-
ed25519.Ed25519PublicKey, ed448.Ed448PublicKey,
194-
x25519.X25519PublicKey, x448.X448PublicKey,
195-
]:
196-
"""Get wrapped public key."""
197-
return self._wrapped.public_key()
198-
199191

200192
class ImmutableMap(Mapping, Hashable):
201193
# pylint: disable=too-few-public-methods

0 commit comments

Comments
 (0)