Skip to content

Commit 3297c58

Browse files
committed
More tests
1 parent eace947 commit 3297c58

File tree

5 files changed

+201
-53
lines changed

5 files changed

+201
-53
lines changed

README.md

Lines changed: 29 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -136,15 +136,15 @@ jwt-rsa keygen [options]
136136

137137
**Options:**
138138

139-
- `-b, --bits`: Number of bits for the RSA key (default: 2048). Choices: 1024, 2048, 4096, 8192.
139+
- `-b`, `--bits`: Number of bits for the RSA key (default: 2048). Choices: 1024, 2048, 4096, 8192.
140140
- `--kid`: Key ID. If not provided, one will be generated.
141-
- `-a, --algorithm`: Algorithm to use (`RS256`, `RS384`, `RS512`). Default: `RS512`.
142-
- `-u, --use`: Key usage (`sig` for signature, `enc` for encryption). Default: `sig`.
143-
- `-o, --format`: Output format (`pem`, `jwk`, `base64`). Default: `jwk`.
144-
- `-r, --raw`: Output raw JSON without indentation.
145-
- `-k, --save-public`: Path to save the public key.
146-
- `-K, --save-private`: Path to save the private key.
147-
- `-f, --force`: Overwrite existing keys if they exist.
141+
- `-a`, `--algorithm`: Algorithm to use (`RS256`, `RS384`, `RS512`). Default: `RS512`.
142+
- `-u`, `--use`: Key usage (`sig` for signature, `enc` for encryption). Default: `sig`.
143+
- `-o`, `--format`: Output format (`pem`, `jwk`, `base64`). Default: `jwk`.
144+
- `-r`, `--raw`: Output raw JSON without indentation.
145+
- `-k`, `--save-public`: Path to save the public key.
146+
- `-K`, `--save-private`: Path to save the private key.
147+
- `-f`, `--force`: Overwrite existing keys if they exist.
148148

149149
**Examples:**
150150

@@ -212,8 +212,8 @@ jwt-rsa testkey -K PRIVATE_KEY_PATH -k PUBLIC_KEY_PATH
212212

213213
**Options:**
214214

215-
- `-K, --private-key`: Path to the private key (required).
216-
- `-k, --public-key`: Path to the public key (required).
215+
- `-K`, `--private-key`: Path to the private key (required).
216+
- `-k`, `--public-key`: Path to the public key (required).
217217

218218
**Examples:**
219219

@@ -238,9 +238,9 @@ jwt-rsa pubkey -K PRIVATE_KEY_PATH [options]
238238

239239
**Options:**
240240

241-
- `-K, --private-key`: Path to the private key (required).
242-
- `-o, --format`: Output format (`pem`, `jwk`, `base64`). Default: `jwk`.
243-
- `-r, --raw`: Output raw JSON without indentation.
241+
- `-K`, `--private-key`: Path to the private key (required).
242+
- `-o`, `--format`: Output format (`pem`, `jwk`, `base64`). Default: `jwk`.
243+
- `-r`, `--raw`: Output raw JSON without indentation.
244244

245245
**Examples:**
246246

@@ -263,11 +263,11 @@ jwt-rsa issue -K PRIVATE_KEY_PATH [options]
263263

264264
**Options:**
265265

266-
- `-K, --private-key`: Path to the private JWT key (required).
266+
- `-K`, `--private-key`: Path to the private JWT key (required).
267267
- `--expired`: Token expiration time in seconds (default: `2678400` seconds, which is 31 days).
268268
- `--nbf`: "Not Before" claim in seconds (default: `-30`).
269-
- `-I, --no-interactive`: Disable interactive mode. By default, interactive mode is enabled.
270-
- `-e, --editor`: Editor to use in interactive mode. Defaults to the `EDITOR` environment variable or `vim`.
269+
- `-I`, `--no-interactive`: Disable interactive mode. By default, interactive mode is enabled.
270+
- `-e`, `--editor`: Editor to use in interactive mode. Defaults to the `EDITOR` environment variable or `vim`.
271271

272272
**Examples:**
273273

@@ -277,7 +277,8 @@ Issue a JWT token with default expiration and interactive mode:
277277
jwt-rsa issue -K ./private.pem
278278
```
279279

280-
By default will be opened the default editor to edit the claims, the format is python dictionary, with comments and pre-filled values:
280+
By default will be opened the default editor to edit the claims, the format is python dictionary, with comments and
281+
pre-filled values:
281282

282283
```python
283284
# This modules functions and constants are available:
@@ -323,6 +324,8 @@ $ echo '{"foo": "bar"}' | jwt-rsa issue -K /tmp/key -I --expired 3600
323324
eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJleHAiOjE3Mzg2MzAwNDcsIm5iZiI6MTczNTk1MTYyN30.HRCQ
324325
```
325326

327+
In non interactive mode, the input must be a JSON object with the claims to issue the token.
328+
326329
#### `verify`
327330

328331
Parse and verify a JWT token.
@@ -335,10 +338,10 @@ jwt-rsa verify [options] TOKEN
335338

336339
**Options:**
337340

338-
- `-K, --private-key`: Path to the private key.
339-
- `-k, --public-key`: Path to the public key. If ommited, the public key will be extracted from the private key.
340-
- `-V, --no-verify`: Do not verify the token's signature.
341-
- `-I, --no-interactive`: Disable interactive mode. By default, interactive mode is enabled.
341+
- `-K`, `--private-key`: Path to the private key.
342+
- `-k`, `--public-key`: Path to the public key. If ommited, the public key will be extracted from the private key.
343+
- `-V`, `--no-verify`: Do not verify the token's signature.
344+
- `-I`, `--no-interactive`: Disable interactive mode. By default, interactive mode is enabled.
342345
343346
**Examples:**
344347
@@ -369,12 +372,12 @@ jwt-rsa convert PRIVATE_KEY_PATH [options]
369372
**Options:**
370373

371374
- `private_key`: Path to the source private key (positional argument).
372-
- `-k, --save-public`: Path to save the converted public key. If omitted,
375+
- `-k`, `--save-public`: Path to save the converted public key. If omitted,
373376
the public key will be saved to the same directory as the private key with a `.pub` extension.
374-
- `-K, --save-private`: Path to save the converted private key.
375-
- `-o, --format`: Output format (`pem`, `jwk`, `base64`). Default: `jwk`.
376-
- `-f, --force`: Overwrite existing keys if they exist.
377-
- `-r, --raw`: Output raw JSON without indentation.
377+
- `-K`, `--save-private`: Path to save the converted private key.
378+
- `-o`, `--format`: Output format (`pem`, `jwk`, `base64`). Default: `jwk`.
379+
- `-f`, `--force`: Overwrite existing keys if they exist.
380+
- `-r`, `--raw`: Output raw JSON without indentation.
378381

379382
**Examples:**
380383

jwt_rsa/rsa.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -80,13 +80,19 @@ def load_jwk_private_key(jwk: RSAJWKPrivateKey) -> RSAPrivateKey:
8080
return private_numbers.private_key(default_backend())
8181

8282

83-
def load_jwk(jwk_str: str) -> KeyPair:
84-
jwk = json.loads(jwk_str)
85-
if "d" in jwk: # Private key
86-
private_key = load_jwk_private_key(jwk)
83+
def load_jwk(jwk: Union[RSAJWKPublicKey, RSAJWKPrivateKey, str]) -> KeyPair:
84+
jwk_dict: Union[RSAJWKPublicKey, RSAJWKPrivateKey]
85+
86+
if isinstance(jwk, str):
87+
jwk_dict = json.loads(jwk)
88+
else:
89+
jwk_dict = jwk
90+
91+
if "d" in jwk_dict: # Private key
92+
private_key = load_jwk_private_key(jwk_dict) # type: ignore
8793
public_key = private_key.public_key()
8894
else: # Public key
89-
public_key = load_jwk_public_key(jwk)
95+
public_key = load_jwk_public_key(jwk_dict) # type: ignore
9096
private_key = None
9197

9298
return KeyPair(private=private_key, public=public_key)
@@ -107,8 +113,7 @@ def rsa_to_jwk(
107113
@overload
108114
def rsa_to_jwk( # type: ignore[overload-cannot-match]
109115
key: RSAPrivateKey, *, kid: str = "", alg: AlgorithmType = "RS256", use: str = "sig",
110-
) -> RSAJWKPrivateKey:
111-
...
116+
) -> RSAJWKPrivateKey: ...
112117

113118

114119
def rsa_to_jwk(

jwt_rsa/token.py

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,25 +34,24 @@ class JWT:
3434

3535
def __init__(
3636
self,
37-
private_key: Optional[RSAPrivateKey] = None,
38-
public_key: Optional[RSAPublicKey] = None,
39-
expires: Optional[int] = None,
37+
key: Optional[Union[RSAPrivateKey, RSAPublicKey]],
38+
*, expires: Optional[int] = None,
4039
nbf_delta: Optional[int] = None,
4140
algorithm: AlgorithmType = "RS512",
4241
algorithms: Sequence[AlgorithmType] = ALGORITHMS,
4342
options: Optional[Dict[str, Any]] = None,
4443
):
45-
4644
self.__public_key: RSAPublicKey
47-
self.__private_key: Optional[RSAPrivateKey] = private_key
48-
49-
if public_key is None:
50-
if isinstance(self.__private_key, RSAPrivateKey):
51-
self.__public_key = self.__private_key.public_key()
52-
else:
53-
raise ValueError("You must provide either a public or private key")
45+
self.__private_key: Optional[RSAPrivateKey]
46+
47+
if isinstance(key, RSAPrivateKey):
48+
self.__public_key = key.public_key()
49+
self.__private_key = key
50+
elif isinstance(key, RSAPublicKey):
51+
self.__public_key = key
52+
self.__private_key = None
5453
else:
55-
self.__public_key = public_key
54+
raise ValueError("You must provide either a public or private key")
5655

5756
self.__jwt = PyJWT(options)
5857
self.__expires = expires or self.DEFAULT_EXPIRATION
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,73 @@ def test_pem_format(capsys):
9393
)
9494

9595

96+
def test_keygen_no_force(capsys, tmp_path):
97+
private_path = tmp_path / "private.pem"
98+
public_path = tmp_path / "public.pem"
99+
100+
keygen(
101+
parser.parse_args([
102+
"keygen", "-o", "pem",
103+
"-K", str(private_path), "-k", str(public_path),
104+
])
105+
)
106+
107+
assert private_path.exists()
108+
assert public_path.exists()
109+
110+
public_content = public_path.read_text()
111+
private_content = private_path.read_text()
112+
113+
assert public_content
114+
assert private_content
115+
116+
# Try to generate keys again buy don't overwrite existing
117+
keygen(
118+
parser.parse_args([
119+
"keygen", "-o", "pem",
120+
"-K", str(private_path), "-k", str(public_path),
121+
])
122+
)
123+
124+
assert public_content == public_path.read_text()
125+
assert private_content == private_path.read_text()
126+
127+
keygen(
128+
parser.parse_args([
129+
"keygen", "-o", "pem", "-f",
130+
"-K", str(private_path), "-k", str(public_path),
131+
])
132+
)
133+
134+
assert public_content != public_path.read_text()
135+
assert private_content != private_path.read_text()
136+
137+
138+
def test_keygen_public_key_auto_naming(capsys, tmp_path):
139+
private_path = tmp_path / "key"
140+
public_path = tmp_path / "key.pub"
141+
keygen(parser.parse_args(["keygen", "-o", "pem", "-K", str(private_path)]))
142+
143+
assert private_path.exists()
144+
assert public_path.exists()
145+
146+
public_content = public_path.read_text()
147+
private_content = private_path.read_text()
148+
149+
assert public_content
150+
assert private_content
151+
152+
private_path.unlink()
153+
154+
keygen(parser.parse_args(["keygen", "-o", "pem", "-K", str(private_path)]))
155+
156+
assert private_path.exists()
157+
assert public_path.exists()
158+
159+
assert public_content == public_path.read_text()
160+
assert private_content != private_path.read_text()
161+
162+
96163
@pytest.mark.skip(reason="TODO")
97164
def test_rsa_verify(capsys):
98165
with mock.patch("sys.argv", ["jwt-rsa", "keygen"]):
Lines changed: 82 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import base64
22
import datetime
3+
import json
34
import os
45
import time
56
from itertools import product
@@ -10,6 +11,7 @@
1011
from jwt.exceptions import InvalidAlgorithmError, InvalidSignatureError
1112

1213
from jwt_rsa import rsa
14+
from jwt_rsa.types import serialization
1315
from jwt_rsa.token import JWT
1416

1517

@@ -67,9 +69,7 @@ def test_rsa_sign():
6769
@pytest.mark.parametrize("expired,nbf", product(expires, nbfs))
6870
def test_jwt_token(expired, nbf):
6971
bits = 2048
70-
key, public = rsa.generate_rsa(bits)
71-
72-
jwt = JWT(key, public)
72+
jwt = JWT(rsa.generate_rsa(bits).private)
7373

7474
token = jwt.encode(foo="bar", expired=expired, nbf=nbf)
7575

@@ -91,17 +91,15 @@ def test_jwt_token(expired, nbf):
9191

9292
def test_jwt_token_invalid_expiration():
9393
bits = 2048
94-
key, public = rsa.generate_rsa(bits)
95-
96-
jwt = JWT(key, public)
94+
jwt = JWT(rsa.generate_rsa(bits).private)
9795

9896
with pytest.raises(ValueError):
9997
jwt.encode(foo="bar", expired=None, nbf=None)
10098

10199

102100
def test_encode_and_decode_with_private_key():
103101
bits = 2048
104-
key, public = rsa.generate_rsa(bits)
102+
key, _ = rsa.generate_rsa(bits)
105103

106104
jwt = JWT(key)
107105
token = jwt.encode(foo="bar")
@@ -115,8 +113,84 @@ def test_decode_only_ability():
115113

116114
token = JWT(key).encode(foo="bar")
117115

118-
jwt = JWT(None, public)
116+
jwt = JWT(public)
119117
assert "foo" in jwt.decode(token)
120118

121119
with pytest.raises(RuntimeError):
122120
jwt.encode(foo=None)
121+
122+
123+
def test_jwt_init():
124+
bits = 2048
125+
key, public = rsa.generate_rsa(bits)
126+
127+
assert JWT(key)
128+
129+
assert JWT(public)
130+
131+
with pytest.raises(ValueError):
132+
JWT(None)
133+
134+
135+
def test_load_jwk():
136+
bits = 2048
137+
key, public = rsa.generate_rsa(bits)
138+
139+
jwk = rsa.rsa_to_jwk(key)
140+
assert rsa.load_jwk(jwk).private
141+
assert rsa.load_jwk(jwk).public
142+
143+
jwk = json.dumps(rsa.rsa_to_jwk(key))
144+
assert rsa.load_jwk(jwk).private
145+
assert rsa.load_jwk(jwk).public
146+
147+
jwk = rsa.rsa_to_jwk(public)
148+
assert not rsa.load_jwk(jwk).private
149+
assert rsa.load_jwk(jwk).public
150+
151+
bad_jwk = rsa.rsa_to_jwk(key)
152+
bad_jwk["kty"] = "bad"
153+
154+
with pytest.raises(ValueError):
155+
rsa.load_jwk(bad_jwk)
156+
157+
bad_jwk = rsa.rsa_to_jwk(public)
158+
bad_jwk["kty"] = "bad"
159+
160+
with pytest.raises(ValueError):
161+
rsa.load_jwk(bad_jwk)
162+
163+
with pytest.raises(ValueError):
164+
# noinspection PyTypeChecker
165+
rsa.rsa_to_jwk(None) # type: ignore
166+
167+
168+
def test_load_public_key(tmp_path):
169+
bits = 2048
170+
key, public = rsa.generate_rsa(bits)
171+
172+
public_path = tmp_path / "public.pem"
173+
private_path = tmp_path / "private.pem"
174+
175+
with open(public_path, "wb") as fp:
176+
fp.write(public.public_bytes(
177+
encoding=serialization.Encoding.PEM,
178+
format=serialization.PublicFormat.SubjectPublicKeyInfo,
179+
))
180+
181+
with open(private_path, "wb") as fp:
182+
fp.write(key.private_bytes(
183+
encoding=serialization.Encoding.PEM,
184+
format=serialization.PrivateFormat.PKCS8,
185+
encryption_algorithm=serialization.NoEncryption(),
186+
))
187+
188+
assert rsa.load_public_key(public_path)
189+
assert rsa.load_public_key(public_path.read_text())
190+
assert rsa.load_public_key(rsa.rsa_to_jwk(public))
191+
assert rsa.load_public_key(json.dumps(rsa.rsa_to_jwk(public)))
192+
193+
assert rsa.load_private_key(private_path)
194+
assert rsa.load_private_key(private_path.read_text())
195+
assert rsa.load_private_key(rsa.rsa_to_jwk(key))
196+
assert rsa.load_private_key(json.dumps(rsa.rsa_to_jwk(key)))

0 commit comments

Comments
 (0)