Skip to content

Commit 48a3a71

Browse files
authored
feat: Export import_key and generate_key methods
1 parent 8cf217d commit 48a3a71

10 files changed

Lines changed: 102 additions & 25 deletions

File tree

src/joserfc/_keys.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"Key",
1818
"KeySet",
1919
"JWKRegistry",
20+
"KeySetSerialization",
2021
]
2122

2223
Key = t.Union[OctKey, RSAKey, ECKey, OKPKey]
@@ -75,7 +76,7 @@ def import_key(cls, data: AnyKey, key_type: str | None = None, parameters: KeyPa
7576
def generate_key(
7677
cls,
7778
key_type: str,
78-
crv_or_size: str | int,
79+
crv_or_size: str | int | None = None,
7980
parameters: KeyParameters | None = None,
8081
private: bool = True,
8182
auto_kid: bool = False,

src/joserfc/jwk.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
JWKRegistry,
66
KeySet,
77
Key,
8+
KeySetSerialization,
89
)
10+
from .rfc7517.types import AnyKey, KeyParameters
911
from .rfc7518.oct_key import OctKey as OctKey
1012
from .rfc7518.rsa_key import RSAKey as RSAKey
1113
from .rfc7518.ec_key import ECKey as ECKey
@@ -19,6 +21,7 @@
1921
"Key",
2022
"KeyCallable",
2123
"KeyFlexible",
24+
"KeySetSerialization",
2225
"OctKey",
2326
"RSAKey",
2427
"ECKey",
@@ -27,6 +30,8 @@
2730
"KeyBase",
2831
"GuestProtocol",
2932
"guess_key",
33+
"import_key",
34+
"generate_key",
3035
]
3136

3237
register_secp256k1()
@@ -87,3 +92,35 @@ def _normalize_key(key: KeyBase) -> Key | KeySet:
8792
)
8893
return OctKey.import_key(key)
8994
return key
95+
96+
97+
def import_key(
98+
data: AnyKey,
99+
key_type: str | None = None,
100+
parameters: KeyParameters | None = None
101+
) -> Key:
102+
"""Importing a key from bytes, string, and dict. When ``value`` is a dict,
103+
this method can tell the key type automatically, otherwise, developers
104+
SHOULD pass the ``key_type`` themselves.
105+
106+
:param data: the key data in bytes, string, or dict.
107+
:param key_type: an optional key type in string.
108+
:param parameters: extra key parameters
109+
:return: OctKey, RSAKey, ECKey, or OKPKey
110+
"""
111+
return JWKRegistry.import_key(data, key_type, parameters)
112+
113+
114+
def generate_key(
115+
key_type: str,
116+
crv_or_size: str | int | None = None,
117+
parameters: KeyParameters | None = None,
118+
private: bool = True,
119+
auto_kid: bool = False,
120+
) -> Key:
121+
"""Generating key according to the given key type. When ``key_type`` is
122+
"oct" and "RSA", the second parameter SHOULD be a key size in bits.
123+
When ``key_type`` is "EC" and "OKP", the second
124+
parameter SHOULD be a "crv" string.
125+
"""
126+
return JWKRegistry.generate_key(key_type, crv_or_size, parameters, private, auto_kid)

src/joserfc/rfc7518/ec_key.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,11 @@ def curve_key_size(self) -> int:
144144

145145
@classmethod
146146
def generate_key(
147-
cls, crv: str = "P-256", parameters: KeyParameters | None = None, private: bool = True, auto_kid: bool = False
147+
cls,
148+
crv: str | None = "P-256",
149+
parameters: KeyParameters | None = None,
150+
private: bool = True,
151+
auto_kid: bool = False,
148152
) -> "ECKey":
149153
"""Generate a ``ECKey`` with the given "crv" value.
150154
@@ -153,6 +157,9 @@ def generate_key(
153157
:param private: generate a private key or public key
154158
:param auto_kid: add ``kid`` automatically
155159
"""
160+
if crv is None:
161+
crv = "P-256"
162+
156163
raw_key = cls.binding.generate_private_key(crv)
157164
if private:
158165
key = cls(raw_key, raw_key, parameters)

src/joserfc/rfc7518/oct_key.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,11 @@ class OctKey(SymmetricKey):
5151

5252
@classmethod
5353
def generate_key(
54-
cls, key_size: int = 256, parameters: KeyParameters | None = None, private: bool = True, auto_kid: bool = False
54+
cls,
55+
key_size: int | None = 256,
56+
parameters: KeyParameters | None = None,
57+
private: bool = True,
58+
auto_kid: bool = False,
5559
) -> "OctKey":
5660
"""Generate a ``OctKey`` with the given bit size (not bytes).
5761
@@ -63,6 +67,9 @@ def generate_key(
6367
if not private:
6468
raise ValueError("oct key can not be generated as public")
6569

70+
if key_size is None:
71+
key_size = 256
72+
6673
if key_size % 8 != 0:
6774
raise ValueError("Invalid bit size for oct key")
6875

src/joserfc/rfc7518/rsa_key.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import typing as t
1+
from __future__ import annotations
2+
from typing import TypedDict
23
from functools import cached_property
34
from cryptography.hazmat.primitives.asymmetric.rsa import (
45
generate_private_key,
@@ -19,7 +20,7 @@
1920
from ..util import int_to_base64, base64_to_int
2021

2122

22-
RSADictKey = t.TypedDict(
23+
RSADictKey = TypedDict(
2324
"RSADictKey",
2425
{
2526
"n": str,
@@ -91,7 +92,7 @@ def import_public_key(obj: RSADictKey) -> RSAPublicKey:
9192
return numbers.public_key(default_backend())
9293

9394
@staticmethod
94-
def export_public_key(key: RSAPublicKey) -> t.Dict[str, str]:
95+
def export_public_key(key: RSAPublicKey) -> dict[str, str]:
9596
numbers = key.public_numbers()
9697
return {"n": int_to_base64(numbers.n), "e": int_to_base64(numbers.e)}
9798

@@ -124,16 +125,16 @@ def public_key(self) -> RSAPublicKey:
124125
return self.raw_value
125126

126127
@property
127-
def private_key(self) -> t.Optional[RSAPrivateKey]:
128+
def private_key(self) -> RSAPrivateKey | None:
128129
if isinstance(self.raw_value, RSAPrivateKey):
129130
return self.raw_value
130131
return None
131132

132133
@classmethod
133134
def generate_key(
134135
cls,
135-
key_size: int = 2048,
136-
parameters: t.Optional[KeyParameters] = None,
136+
key_size: int | None = 2048,
137+
parameters: KeyParameters | None = None,
137138
private: bool = True,
138139
auto_kid: bool = False,
139140
) -> "RSAKey":
@@ -144,10 +145,15 @@ def generate_key(
144145
:param private: generate a private key or public key
145146
:param auto_kid: add ``kid`` automatically
146147
"""
148+
if key_size is None:
149+
key_size = 2048
150+
147151
if key_size < 512:
148152
raise ValueError("key_size must not be less than 512")
153+
149154
if key_size % 8 != 0:
150155
raise ValueError("Invalid key_size for RSAKey")
156+
151157
raw_key = generate_private_key(
152158
public_exponent=65537,
153159
key_size=key_size,

src/joserfc/rfc8037/okp_key.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
import typing as t
24
from functools import cached_property
35
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey, Ed25519PrivateKey
@@ -109,7 +111,7 @@ def public_key(self) -> PublicOKPKey:
109111
return self.raw_value
110112

111113
@property
112-
def private_key(self) -> t.Optional[PrivateOKPKey]:
114+
def private_key(self) -> PrivateOKPKey | None:
113115
if isinstance(self.raw_value, PrivateKeyTypes):
114116
return self.raw_value
115117
return None
@@ -121,8 +123,8 @@ def curve_name(self) -> str:
121123
@classmethod
122124
def generate_key(
123125
cls,
124-
crv: str = "Ed25519",
125-
parameters: t.Optional[KeyParameters] = None,
126+
crv: str | None = "Ed25519",
127+
parameters: KeyParameters | None = None,
126128
private: bool = True,
127129
auto_kid: bool = False,
128130
) -> "OKPKey":
@@ -133,6 +135,9 @@ def generate_key(
133135
:param private: generate a private key or public key
134136
:param auto_kid: add ``kid`` automatically
135137
"""
138+
if crv is None:
139+
crv = "Ed25519"
140+
136141
if crv not in PRIVATE_KEYS_MAP:
137142
raise ValueError("Invalid crv value: '{}'".format(crv))
138143

tests/base.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import json
22
import typing as t
3-
from joserfc.jwk import JWKRegistry
3+
from joserfc.jwk import import_key
44
from unittest import TestCase
55
from pathlib import Path
66

@@ -18,10 +18,10 @@ def load_key(filename: str, parameters=None):
1818

1919
if filename.endswith(".json"):
2020
data = json.loads(content)
21-
return JWKRegistry.import_key(data, parameters=parameters)
21+
return import_key(data, parameters=parameters)
2222

2323
kty = filename.split("-", 1)[0]
24-
return JWKRegistry.import_key(content, kty.upper(), parameters)
24+
return import_key(content, kty.upper(), parameters)
2525

2626

2727
class TestFixture(TestCase):

tests/jwk/test_key_methods.py

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from unittest import TestCase
22
from joserfc.jws import register_key_set
3-
from joserfc.jwk import JWKRegistry, guess_key
4-
from joserfc.jwk import KeySet, OctKey, RSAKey
3+
from joserfc.jwk import guess_key, import_key, generate_key
4+
from joserfc.jwk import KeySet, OctKey, RSAKey, ECKey, OKPKey
55
from joserfc.errors import (
66
UnsupportedKeyAlgorithmError,
77
UnsupportedKeyUseError,
@@ -95,24 +95,34 @@ def test_invalid_key(self):
9595

9696
def test_import_key(self):
9797
# test bytes
98-
key = JWKRegistry.import_key(b"secret", "oct")
98+
key = import_key(b"secret", "oct")
9999
self.assertIsInstance(key, OctKey)
100100

101101
# test string
102-
key = JWKRegistry.import_key("secret", "oct")
102+
key = import_key("secret", "oct")
103103
self.assertIsInstance(key, OctKey)
104104

105105
# test dict
106106
data = key.as_dict()
107-
key = JWKRegistry.import_key(data)
107+
key = import_key(data)
108108
self.assertIsInstance(key, OctKey)
109109

110-
self.assertRaises(InvalidKeyTypeError, JWKRegistry.import_key, "secret", "invalid")
110+
self.assertRaises(InvalidKeyTypeError, import_key, "secret", "invalid")
111111

112112
def test_generate_key(self):
113-
key = JWKRegistry.generate_key("oct", 8)
113+
key = generate_key("oct")
114114
self.assertIsInstance(key, OctKey)
115-
self.assertRaises(InvalidKeyTypeError, JWKRegistry.generate_key, "invalid", 8)
115+
116+
key = generate_key("RSA")
117+
self.assertIsInstance(key, RSAKey)
118+
119+
key = generate_key("EC")
120+
self.assertIsInstance(key, ECKey)
121+
122+
key = generate_key("OKP")
123+
self.assertIsInstance(key, OKPKey)
124+
125+
self.assertRaises(InvalidKeyTypeError, generate_key, "invalid", 8)
116126

117127
def test_check_use(self):
118128
key = OctKey.import_key("secret", {"use": "sig"})
@@ -141,4 +151,4 @@ def test_check_ops(self):
141151
self.assertRaises(UnsupportedKeyOperationError, key.check_key_op, "sign")
142152

143153
def test_import_without_kty(self):
144-
self.assertRaises(MissingKeyTypeError, JWKRegistry.import_key, {})
154+
self.assertRaises(MissingKeyTypeError, import_key, {})

tests/jwk/test_oct_key.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ def test_generate_key(self):
8181
self.assertEqual(len(key.raw_value), 32)
8282
self.assertIsNone(key.kid)
8383

84+
key = OctKey.generate_key(None)
85+
self.assertEqual(len(key.raw_value), 32)
86+
self.assertIsNone(key.kid)
87+
8488
self.assertRaises(ValueError, OctKey.generate_key, private=False)
8589
self.assertRaises(ValueError, OctKey.generate_key, 251)
8690

tests/jwk/test_rsa_key.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ def test_generate_key(self):
119119
self.assertRaises(ValueError, RSAKey.generate_key, 8)
120120
self.assertRaises(ValueError, RSAKey.generate_key, 601)
121121

122-
key: RSAKey = RSAKey.generate_key(private=False)
122+
key = RSAKey.generate_key(private=False)
123123
self.assertFalse(key.is_private)
124124
self.assertIsNone(key.kid)
125125

0 commit comments

Comments
 (0)