Skip to content

Commit 734121d

Browse files
committed
feat(jwk): add derive_key method for ECKey
1 parent 04f596f commit 734121d

File tree

4 files changed

+128
-25
lines changed

4 files changed

+128
-25
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name = "joserfc"
33
description = "The ultimate Python library for JOSE RFCs, including JWS, JWE, JWK, JWA, JWT"
44
authors = [{name = "Hsiaoming Yang", email="[email protected]"}]
55
dependencies = [
6-
"cryptography",
6+
"cryptography>=45.0.1",
77
]
88
license = {text = "BSD-3-Clause"}
99
requires-python = ">=3.9"

src/joserfc/_rfc7518/ec_key.py

Lines changed: 94 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
from __future__ import annotations
22
import typing as t
33
from functools import cached_property
4+
from cryptography.hazmat.primitives import hashes
45
from cryptography.hazmat.primitives.asymmetric.ec import (
56
generate_private_key,
7+
derive_private_key,
68
ECDH,
79
EllipticCurvePublicKey,
810
EllipticCurvePrivateKey,
@@ -13,12 +15,13 @@
1315
SECP384R1,
1416
SECP521R1,
1517
)
16-
from cryptography.hazmat.backends import default_backend
18+
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
19+
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
1720
from ..errors import InvalidExchangeKeyError, InvalidKeyCurveError
1821
from .._rfc7517.models import CurveKey
1922
from .._rfc7517.pem import CryptographyBinding
2023
from .._rfc7517.types import KeyParameters, AnyKey
21-
from ..util import base64_to_int, int_to_base64
24+
from ..util import base64_to_int, int_to_base64, to_bytes
2225
from ..registry import KeyParameter
2326

2427
__all__ = ["ECKey"]
@@ -51,14 +54,10 @@ def register_curve(cls, name: str, curve: t.Type[EllipticCurve]) -> None:
5154
@classmethod
5255
def generate_private_key(cls, name: str) -> EllipticCurvePrivateKey:
5356
if name not in cls._dss_curves:
54-
raise InvalidKeyCurveError("Invalid crv value: '{}'".format(name))
57+
raise InvalidKeyCurveError(f"Invalid crv value: '{name}'")
5558

5659
curve = cls._dss_curves[name]()
57-
raw_key = generate_private_key(
58-
curve=curve,
59-
backend=default_backend(),
60-
)
61-
return raw_key
60+
return generate_private_key(curve=curve)
6261

6362
@classmethod
6463
def import_private_key(cls, obj: ECDictKey) -> EllipticCurvePrivateKey:
@@ -70,7 +69,7 @@ def import_private_key(cls, obj: ECDictKey) -> EllipticCurvePrivateKey:
7069
)
7170
d = base64_to_int(obj["d"])
7271
private_numbers = EllipticCurvePrivateNumbers(d, public_numbers)
73-
return private_numbers.private_key(default_backend())
72+
return private_numbers.private_key()
7473

7574
@classmethod
7675
def export_private_key(cls, key: EllipticCurvePrivateKey) -> ECDictKey:
@@ -90,7 +89,7 @@ def import_public_key(cls, obj: ECDictKey) -> EllipticCurvePublicKey:
9089
base64_to_int(obj["y"]),
9190
curve,
9291
)
93-
return public_numbers.public_key(default_backend())
92+
return public_numbers.public_key()
9493

9594
@classmethod
9695
def export_public_key(cls, key: EllipticCurvePublicKey) -> ECDictKey:
@@ -102,6 +101,13 @@ def export_public_key(cls, key: EllipticCurvePublicKey) -> ECDictKey:
102101
}
103102

104103

104+
# register default curves with their DSS (Digital Signature Standard) names
105+
# https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf
106+
ECBinding.register_curve("P-256", SECP256R1)
107+
ECBinding.register_curve("P-384", SECP384R1)
108+
ECBinding.register_curve("P-521", SECP521R1)
109+
110+
105111
class ECKey(CurveKey[EllipticCurvePrivateKey, EllipticCurvePublicKey]):
106112
key_type = "EC"
107113
#: Registry definition for EC Key
@@ -175,20 +181,85 @@ def generate_key(
175181
"""
176182
if crv is None:
177183
crv = "P-256"
178-
179184
raw_key = cls.binding.generate_private_key(crv)
180-
if private:
181-
key = cls(raw_key, raw_key, parameters)
185+
return _wrap_key(cls, raw_key, private, auto_kid, parameters)
186+
187+
@classmethod
188+
def derive_key(
189+
cls: t.Type["ECKey"],
190+
secret: bytes | str,
191+
crv: str = "P-256",
192+
parameters: KeyParameters | None = None,
193+
private: bool = True,
194+
auto_kid: bool = False,
195+
kdf_name: t.Literal["HKDF", "PBKDF2"] = "HKDF",
196+
kdf_options: dict[str, t.Any] | None = None,
197+
) -> "ECKey":
198+
"""
199+
Generate an elliptic curve cryptographic key derived from a secret input using a key
200+
derivation function (KDF). This allows the creation of deterministic elliptic curve
201+
keys based on given input data, curve specification, and KDF options.
202+
203+
:param secret: The input secret used for key derivation
204+
:param crv: ECKey curve name
205+
:param parameters: extra parameter in JWK
206+
:param private: generate a private key or public key
207+
:param auto_kid: add ``kid`` automatically
208+
:param kdf_name: Key derivation function name
209+
:param kdf_options: Additional options for the KDF
210+
"""
211+
try:
212+
curve_class = cls.binding._dss_curves[crv]
213+
except KeyError:
214+
raise InvalidKeyCurveError(f"Invalid crv value: '{crv}'")
215+
216+
curve = curve_class()
217+
length = (curve.group_order.bit_length() + 7) // 8 * 2
218+
219+
if kdf_options is None:
220+
kdf_options = {}
221+
222+
algorithm = kdf_options.pop("algorithm", None)
223+
if algorithm is None:
224+
algorithm = hashes.SHA256()
225+
226+
kdf_options.setdefault("salt", to_bytes(f"joserfc:EC:{kdf_name}:{crv}"))
227+
if kdf_name == "HKDF":
228+
kdf_options.setdefault("info", b"")
229+
hkdf = HKDF(
230+
algorithm=algorithm,
231+
length=length,
232+
**kdf_options,
233+
)
234+
seed = hkdf.derive(to_bytes(secret))
235+
elif kdf_name == "PBKDF2":
236+
kdf_options.setdefault("iterations", 100000)
237+
pbkdf2 = PBKDF2HMAC(
238+
algorithm=algorithm,
239+
length=length,
240+
**kdf_options,
241+
)
242+
seed = pbkdf2.derive(to_bytes(secret))
182243
else:
183-
pub_key = raw_key.public_key()
184-
key = cls(pub_key, pub_key, parameters)
185-
if auto_kid:
186-
key.ensure_kid()
187-
return key
244+
raise ValueError(f"Invalid kdf value: '{kdf_name}'")
188245

246+
d = int.from_bytes(seed, "big") % curve.group_order
247+
raw_key = derive_private_key(d, curve)
248+
return _wrap_key(cls, raw_key, private, auto_kid, parameters)
189249

190-
# register default curves with their DSS (Digital Signature Standard) names
191-
# https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf
192-
ECBinding.register_curve("P-256", SECP256R1)
193-
ECBinding.register_curve("P-384", SECP384R1)
194-
ECBinding.register_curve("P-521", SECP521R1)
250+
251+
def _wrap_key(
252+
cls: t.Type["ECKey"],
253+
raw_key: EllipticCurvePrivateKey,
254+
private: bool,
255+
auto_kid: bool,
256+
parameters: KeyParameters | None = None,
257+
) -> ECKey:
258+
if private:
259+
key = cls(raw_key, raw_key, parameters)
260+
else:
261+
pub_key = raw_key.public_key()
262+
key = cls(pub_key, pub_key, parameters)
263+
if auto_kid:
264+
key.ensure_kid()
265+
return key

src/joserfc/_rfc8037/okp_key.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ def derive_key(
261261
}
262262
)
263263
264-
:param secret: The input secret used for key derivation.
264+
:param secret: The input secret used for key derivation
265265
:param crv: OKPKey curve name
266266
:param parameters: extra parameter in JWK
267267
:param private: generate a private key or public key

tests/jwk/test_ec_key.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from unittest import TestCase
2+
from cryptography.hazmat.primitives import hashes
23
from joserfc.jwk import ECKey, OctKey
34
from joserfc.errors import (
45
InvalidExchangeKeyError,
@@ -97,3 +98,34 @@ def test_key_eq(self):
9798

9899
def test_key_eq_with_different_types(self):
99100
self.assertNotEqual(self.default_key, OctKey.generate_key())
101+
102+
def test_derive_key_errors(self):
103+
self.assertRaises(InvalidKeyCurveError, ECKey.derive_key, "secret", "invalid")
104+
self.assertRaises(ValueError, ECKey.derive_key, "secret", kdf_name="invalid")
105+
106+
def test_derive_key_with_default_kwargs(self):
107+
key1 = ECKey.derive_key("ec-secret-key")
108+
key2 = ECKey.derive_key("ec-secret-key")
109+
self.assertEqual(key1, key2)
110+
111+
for crv in ["P-256", "P-384", "P-521", "secp256k1"]:
112+
key1 = ECKey.derive_key("ec-secret-key", crv)
113+
key2 = ECKey.derive_key("ec-secret-key", crv)
114+
self.assertEqual(key1, key2)
115+
116+
for crv in ["P-256", "P-384", "P-521", "secp256k1"]:
117+
key1 = ECKey.derive_key("ec-secret-key", crv, kdf_name="PBKDF2")
118+
key2 = ECKey.derive_key("ec-secret-key", crv, kdf_name="PBKDF2")
119+
self.assertEqual(key1, key2)
120+
121+
def test_derive_key_with_new_salt(self):
122+
curves = ["P-256", "P-384", "P-521", "secp256k1"]
123+
for crv in curves:
124+
key1 = ECKey.derive_key("ec-secret-key", crv, kdf_options={"salt": b"salt"})
125+
key2 = ECKey.derive_key("ec-secret-key", crv, kdf_options={"salt": b"salt"})
126+
self.assertEqual(key1, key2)
127+
128+
def test_derive_key_with_different_hash(self):
129+
key1 = ECKey.derive_key("ec-secret-key", "P-256", kdf_options={"algorithm": hashes.SHA256()})
130+
key2 = ECKey.derive_key("ec-secret-key", "P-256", kdf_options={"algorithm": hashes.SHA512()})
131+
self.assertNotEqual(key1, key2)

0 commit comments

Comments
 (0)