Skip to content

Commit b9d35cf

Browse files
authored
Merge pull request #22 from jschlyter/keyconv2
Improve key conversion tool
2 parents 6e36194 + 8630755 commit b9d35cf

23 files changed

+393
-11
lines changed

src/cryptojwt/tools/keyconv.py

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def jwk_from_file(filename: str, private: bool = True) -> JWK:
2525
return key_from_jwk_dict(jwk_dict, private=private)
2626

2727

28-
def pem2rsa(filename: str, kid: str = None, private: bool = False, passphrase: str = None) -> JWK:
28+
def pem2rsa(filename: str, kid: Optional[str] = None, private: bool = False, passphrase: Optional[str] = None) -> JWK:
2929
"""Convert RSA key from PEM to JWK"""
3030
if private:
3131
key = import_private_rsa_key_from_file(filename, passphrase)
@@ -36,7 +36,7 @@ def pem2rsa(filename: str, kid: str = None, private: bool = False, passphrase: s
3636
return jwk
3737

3838

39-
def pem2ec(filename: str, kid: str = None, private: bool = False, passphrase: str = None) -> JWK:
39+
def pem2ec(filename: str, kid: Optional[str] = None, private: bool = False, passphrase: Optional[str] = None) -> JWK:
4040
"""Convert EC key from PEM to JWK"""
4141
if private:
4242
key = import_private_key_from_file(filename, passphrase)
@@ -54,20 +54,35 @@ def bin2jwk(filename: str, kid: str) -> bytes:
5454
return SYMKey(kid=kid, key=content)
5555

5656

57-
def pem2jwk(filename: str, kid: str, private: bool = False) -> JWK:
57+
def pem2jwk(filename: str, kid: Optional[str] = None, kty: Optional[str] = None, private: bool = False, passphrase: Optional[str] = None) -> JWK:
5858
"""Read PEM from filename and return JWK"""
5959
with open(filename, 'rt') as file:
6060
content = file.readlines()
6161
header = content[0]
6262

6363
if private:
64-
passphrase = getpass('Private key passphrase: ')
64+
if passphrase is None:
65+
passphrase = getpass('Private key passphrase: ')
6566
if len(passphrase) == 0:
6667
passphrase = None
6768
else:
6869
passphrase = None
6970

70-
if 'BEGIN EC PRIVATE KEY' in header:
71+
if 'BEGIN PUBLIC KEY' in header:
72+
if kty is not None and kty == 'EC':
73+
jwk = pem2ec(filename, kid, private=False)
74+
elif kty is not None and kty == 'RSA':
75+
jwk = pem2rsa(filename, kid, private=False)
76+
else:
77+
raise ValueError("Unknown key type")
78+
elif 'BEGIN PRIVATE KEY' in header:
79+
if kty is not None and kty == 'EC':
80+
jwk = pem2ec(filename, kid, private=True, passphrase=passphrase)
81+
elif kty is not None and kty == 'RSA':
82+
jwk = pem2rsa(filename, kid, private=True, passphrase=passphrase)
83+
else:
84+
raise ValueError("Unknown key type")
85+
elif 'BEGIN EC PRIVATE KEY' in header:
7186
jwk = pem2ec(filename, kid, private=True, passphrase=passphrase)
7287
elif 'BEGIN EC PUBLIC KEY' in header:
7388
jwk = pem2ec(filename, kid, private=False)
@@ -81,18 +96,22 @@ def pem2jwk(filename: str, kid: str, private: bool = False) -> JWK:
8196
return jwk
8297

8398

84-
def export_jwk(jwk: JWK, private: bool = False) -> bytes:
99+
def export_jwk(jwk: JWK, private: bool = False, encrypt: bool = False, passphrase: Optional[str] = None) -> bytes:
85100
"""Export JWK as PEM/bin"""
86101

87102
if jwk.kty == 'oct':
88103
return jwk.key
89104

90105
if private:
91-
passphrase = getpass('Private key passphrase: ')
106+
if encrypt:
107+
if passphrase is None:
108+
passphrase = getpass('Private key passphrase: ')
109+
else:
110+
passphrase = None
92111
if passphrase:
93112
enc = serialization.BestAvailableEncryption(passphrase.encode())
94113
else:
95-
enc = serialization.NoEncryption
114+
enc = serialization.NoEncryption()
96115
serialized = jwk.priv_key.private_bytes(
97116
encoding=serialization.Encoding.PEM,
98117
format=serialization.PrivateFormat.PKCS8,
@@ -134,10 +153,18 @@ def main():
134153
dest='kid',
135154
metavar='key_id',
136155
help='Key ID')
156+
parser.add_argument('--kty',
157+
dest='kty',
158+
metavar='type',
159+
help='Key type')
137160
parser.add_argument('--private',
138161
dest='private',
139162
action='store_true',
140163
help="Output private key")
164+
parser.add_argument('--encrypt',
165+
dest='encrypt',
166+
action='store_true',
167+
help="Encrypt private key")
141168
parser.add_argument('--output',
142169
dest='output',
143170
metavar='filename',
@@ -149,13 +176,13 @@ def main():
149176

150177
if f.endswith('.json'):
151178
jwk = jwk_from_file(f, args.private)
152-
serialized = export_jwk(jwk, args.private)
179+
serialized = export_jwk(jwk, private=args.private, encrypt=args.encrypt)
153180
output_bytes(data=serialized, binary=(jwk.kty == 'oct'), filename=args.output)
154181
elif f.endswith('.bin'):
155-
jwk = bin2jwk(f, args.kid)
182+
jwk = bin2jwk(filename=f, kid=args.kid)
156183
output_jwk(jwk=jwk, private=True, filename=args.output)
157184
elif f.endswith('.pem'):
158-
jwk = pem2jwk(f, args.kid, args.private)
185+
jwk = pem2jwk(filename=f, kid=args.kid, private=args.private, kty=args.kty)
159186
output_jwk(jwk=jwk, private=args.private, filename=args.output)
160187
else:
161188
exit(-1)

tests/test_30_tools.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import uuid
2+
import os
3+
import pytest
4+
import json
5+
6+
from cryptojwt.jwk import JWK
7+
from cryptojwt.jwk.rsa import import_private_rsa_key_from_file, import_public_rsa_key_from_file
8+
from cryptojwt.jwk.ec import import_private_key_from_file, import_public_key_from_file
9+
import cryptojwt.tools.keyconv as keyconv
10+
from cryptojwt.jwx import key_from_jwk_dict
11+
12+
13+
BASEDIR = os.path.abspath(os.path.dirname(__file__))
14+
15+
import test_vector
16+
17+
18+
19+
def jwk_from_file(filename: str, private: bool = True) -> JWK:
20+
"""Read JWK from file"""
21+
with open(filename, mode='rt') as input_file:
22+
jwk_dict = json.loads(input_file.read())
23+
return key_from_jwk_dict(jwk_dict, private=private)
24+
25+
26+
def convert_rsa_pem2jwt(filename_jwk: str, filename_pem: str, private: bool):
27+
k1 = jwk_from_file(filename=filename_jwk, private=private)
28+
k2 = keyconv.pem2jwk(filename=filename_pem, kid=k1.kid, kty='RSA', private=private, passphrase='')
29+
if k1 != k2:
30+
raise Exception("Keys differ")
31+
32+
def convert_ec_pem2jwt(filename_jwk: str, filename_pem: str, private: bool):
33+
k1 = jwk_from_file(filename=filename_jwk, private=private)
34+
k2 = keyconv.pem2jwk(filename=filename_pem, kid=k1.kid, kty='EC', private=private, passphrase='')
35+
if k1 != k2:
36+
raise Exception("Keys differ")
37+
38+
def test_pem2rsa_public():
39+
convert_rsa_pem2jwt(BASEDIR+'/test_keys/rsa-1024.json', BASEDIR+'/test_keys/rsa-1024-public.pem', private=False)
40+
convert_rsa_pem2jwt(BASEDIR+'/test_keys/rsa-1280.json', BASEDIR+'/test_keys/rsa-1280-public.pem', private=False)
41+
convert_rsa_pem2jwt(BASEDIR+'/test_keys/rsa-2048.json', BASEDIR+'/test_keys/rsa-2048-public.pem', private=False)
42+
convert_rsa_pem2jwt(BASEDIR+'/test_keys/rsa-3072.json', BASEDIR+'/test_keys/rsa-3072-public.pem', private=False)
43+
convert_rsa_pem2jwt(BASEDIR+'/test_keys/rsa-4096.json', BASEDIR+'/test_keys/rsa-4096-public.pem', private=False)
44+
45+
def test_pem2rsa_private():
46+
convert_rsa_pem2jwt(BASEDIR+'/test_keys/rsa-1024.json', BASEDIR+'/test_keys/rsa-1024-private.pem', private=True)
47+
convert_rsa_pem2jwt(BASEDIR+'/test_keys/rsa-1280.json', BASEDIR+'/test_keys/rsa-1280-private.pem', private=True)
48+
convert_rsa_pem2jwt(BASEDIR+'/test_keys/rsa-2048.json', BASEDIR+'/test_keys/rsa-2048-private.pem', private=True)
49+
convert_rsa_pem2jwt(BASEDIR+'/test_keys/rsa-3072.json', BASEDIR+'/test_keys/rsa-3072-private.pem', private=True)
50+
convert_rsa_pem2jwt(BASEDIR+'/test_keys/rsa-4096.json', BASEDIR+'/test_keys/rsa-4096-private.pem', private=True)
51+
52+
def test_pem2ec_public():
53+
convert_ec_pem2jwt(BASEDIR+'/test_keys/ec-p256.json', BASEDIR+'/test_keys/ec-p256-public.pem', private=False)
54+
convert_ec_pem2jwt(BASEDIR+'/test_keys/ec-p384.json', BASEDIR+'/test_keys/ec-p384-public.pem', private=False)
55+
56+
def test_pem2ec_private():
57+
convert_ec_pem2jwt(BASEDIR+'/test_keys/ec-p256.json', BASEDIR+'/test_keys/ec-p256-private.pem', private=True)
58+
convert_ec_pem2jwt(BASEDIR+'/test_keys/ec-p384.json', BASEDIR+'/test_keys/ec-p384-private.pem', private=True)

tests/test_keys/ec-p256-private.pem

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
-----BEGIN PRIVATE KEY-----
2+
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgrp4E4eaFhHonpP/U
3+
8QRaFDTvCGy8mQrSfQod+QxmViehRANCAAQE1WQ7bdvJIkUESD9hc3GUv1+QGt+A
4+
cKvuzxfjPRqqzLtvlU5nUKOM2rgLCMVP0hiu4wtgREKUYLybH1UPwEuv
5+
-----END PRIVATE KEY-----
6+

tests/test_keys/ec-p256-public.pem

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-----BEGIN PUBLIC KEY-----
2+
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEBNVkO23bySJFBEg/YXNxlL9fkBrf
3+
gHCr7s8X4z0aqsy7b5VOZ1CjjNq4CwjFT9IYruMLYERClGC8mx9VD8BLrw==
4+
-----END PUBLIC KEY-----
5+

tests/test_keys/ec-p256.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"crv": "P-256",
3+
"d": "rp4E4eaFhHonpP_U8QRaFDTvCGy8mQrSfQod-QxmVic",
4+
"kid": "dHNuMWxLYnlrSjgxMk8wdVEwWVd3Z1o1UUNsbnNTVkZxcDdDUzFUaXhWTQ",
5+
"kty": "EC",
6+
"x": "BNVkO23bySJFBEg_YXNxlL9fkBrfgHCr7s8X4z0aqsw",
7+
"y": "u2-VTmdQo4zauAsIxU_SGK7jC2BEQpRgvJsfVQ_AS68"
8+
}

tests/test_keys/ec-p384-private.pem

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
-----BEGIN PRIVATE KEY-----
2+
MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDCUwKlpiMoGAPaoSfl5
3+
iVTFrhF1hVzR57YiMld16H7HcMzv6HNF/nHkiaITetKxCaKhZANiAATpTb3pcZ+C
4+
E9WYGiBho6391NUd5wx15qhGTWT0/2hl2DEvfxHEN/rXJUbG+mzCpvDSvz5IfDDF
5+
ryLtXb3fgSYxGA8VLPC24wT0PImF5L7u3Z4vjSmO38qkrIwBMkUKyTU=
6+
-----END PRIVATE KEY-----
7+

tests/test_keys/ec-p384-public.pem

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
-----BEGIN PUBLIC KEY-----
2+
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE6U296XGfghPVmBogYaOt/dTVHecMdeao
3+
Rk1k9P9oZdgxL38RxDf61yVGxvpswqbw0r8+SHwwxa8i7V2934EmMRgPFSzwtuME
4+
9DyJheS+7t2eL40pjt/KpKyMATJFCsk1
5+
-----END PUBLIC KEY-----
6+

tests/test_keys/ec-p384.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"crv": "P-384",
3+
"d": "lMCpaYjKBgD2qEn5eYlUxa4RdYVc0ee2IjJXdeh-x3DM7-hzRf5x5ImiE3rSsQmi",
4+
"kid": "UDdTWUdvVFZhR0RNVXFUV1pDWEJqT1pwN1BGSnpQSC1LNUMtNE5CUjBhaw",
5+
"kty": "EC",
6+
"x": "6U296XGfghPVmBogYaOt_dTVHecMdeaoRk1k9P9oZdgxL38RxDf61yVGxvpswqbw",
7+
"y": "0r8-SHwwxa8i7V2934EmMRgPFSzwtuME9DyJheS-7t2eL40pjt_KpKyMATJFCsk1"
8+
}

tests/test_keys/rsa-1024-private.pem

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
-----BEGIN PRIVATE KEY-----
2+
MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAN8MXPfNgdWPVofE
3+
OvRbhumygi+ttUEZ/Y+Ts/CIoB6neeSjQJZ2zgxQhGWKgKKKATopKPZ4TFK00AUN
4+
/eNyM59ikRWRpSzIabByUpfEA81EyvoYAl4qVsmpOzl5h3m2kGN15bauOQP9bpHv
5+
vOn4S25TiNj/NHZhZ6oHoZ1Od24rAgMBAAECgYEA0wOOPH12lETL9xuFLsIcS6Eu
6+
ms66yIE/KgLxW+DVosqMfeqYYwC4hFv0NWAnvB3VdWGVOD+s7R3UIsQO6ouTGzTT
7+
Jgye2gg9YbZbB2+STV9cbeUwTCI4ILzsTp8LVHpZTmAXWvp+ui4S+mL78rT3O+pV
8+
T1BQBwPMqvGOyoBuOTECQQD7xr1Lieex4a0Wi3WeW3ck1GCtIEU0MgIeZLTwc1Y+
9+
rhem0IQsx4axnV7His+iGikQm20i2EX9AOg2dFRRbJpjAkEA4spAGHgvd+cqUxus
10+
wQX7SfwNdrMSRko5qJIJXOL0zuUBgJ33APSLTWw2MSRND7cj7yh1b/o8eWkvnRZD
11+
AkADmQJBAMz29ZNRKPV+qtH3pkDMZSnuWuWVp8DeFSt5AHPe8Q8F2utKRM/Pfq+J
12+
VWdMccudUGDcpvP+7LsSyffKq/m9V9ECQE6xBtR2v2HHYDQ+Ig9H2A2v26wYLnsd
13+
Pixzn7QPPAqeA4txREeckslmhtc+VU7iqSFO1JDqLxmhmdfT5aReOeECQGKfe9gJ
14+
oiTy02VzeMQ/2FSdkHG7ZjZYl1+rwA1Y+2eKstg88xpk456ePp4PBJ5MQLEQKvyq
15+
wXuCdEjuK8ipOaQ=
16+
-----END PRIVATE KEY-----
17+

tests/test_keys/rsa-1024-public.pem

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
-----BEGIN PUBLIC KEY-----
2+
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDfDFz3zYHVj1aHxDr0W4bpsoIv
3+
rbVBGf2Pk7PwiKAep3nko0CWds4MUIRlioCiigE6KSj2eExStNAFDf3jcjOfYpEV
4+
kaUsyGmwclKXxAPNRMr6GAJeKlbJqTs5eYd5tpBjdeW2rjkD/W6R77zp+EtuU4jY
5+
/zR2YWeqB6GdTnduKwIDAQAB
6+
-----END PUBLIC KEY-----
7+

0 commit comments

Comments
 (0)